How it works
OpenSim Moco is a C++ framework for solving optimal control problems on musculoskeletal models. You define cost functions (goals), state constraints, and a solver, and it finds a physically plausible motion trajectory. Adding a new goal normally means modifying OpenSim's source and rebuilding the whole library — a significant barrier for labs that want to iterate quickly on novel objectives. MocoExtendProblem is a framework I wrote to get around that, and it's backed by a JOSS paper.
The mechanism that makes it possible is a chain of interop that OpenSim itself already provides. OpenSim uses SWIG to compile its C++ API into Java bindings. MATLAB runs on a JVM and has near-100% Java interop — you can instantiate Java objects and call Java methods directly from MATLAB scripts, no additional wrappers needed. So MATLAB is already talking to OpenSim through Java. The SWIG-generated Java classes include a getCPtr() method that returns the raw C++ pointer to the underlying object. MEP uses that pointer as the bridge: grab the live MocoProblem pointer from MATLAB, pass it as a uint64 to a MEX function, and use it to inject custom goals directly into the running C++ object without touching OpenSim's source.
The build script handles the rest automatically. Write a custom goal in C++, run build.m, and it compiles the goal into a shared library, generates the MEX interface, and regenerates extend_problem.m — a MATLAB class with methods for each goal, extracted by parsing the C++ headers with regex. The C++ header is the source of truth; the MATLAB side generates itself.
Direct collocation — the solver Moco uses — is worth appreciating on its own. You might think of it as curve fitting. You discretize the state and control trajectories across a mesh of time points, then solve a large nonlinear program where the equations of motion are enforced as equality constraints between adjacent mesh points. Rather than integrating forward through time and hoping the dynamics work out (shooting), you're fitting the entire trajectory at once so it satisfies the physics everywhere. It scales much better for long time horizons and stiff systems, and the formulation makes the problem structure more transparent than it sounds in the optimal control literature.
Architecture
| Component | Purpose | Key Features |
|---|---|---|
build.m |
Main build orchestrator | CMake integration, MEX compilation, procedural class generation |
custom_goals/ |
C++ goal implementations | Template-based goal development, OpenSim 4.5+ |
custom_goals_compat/ |
Legacy goal implementations | OpenSim 4.2–4.4 compatibility |
utils/ |
Build utilities | Regex parsing of C++ headers, MATLAB class generation, MEX interface creation |
extend_problem.m |
Auto-generated MATLAB wrapper | One method per goal, regenerated on every build |
Each custom goal requires implementing four methods: constructProperties(), initializeOnModelImpl(), calcIntegrandImpl(), and calcGoalImpl(). Everything else — the MATLAB interface, the MEX bridge — is generated automatically.
Pre-built goals: MocoActivationGoal, MocoActivationSquaredGoal, MocoBOSGoal (base of support), MocoCOPGoal (center of pressure), MocoZMPGoal (zero-tilting moment point), MocoCoordinateAccelerationGoal, MocoCustomOutputGoal, MocoMarkerAccelerationGoal, MocoMaxCoordinateGoal, MocoMuscleStrainGoal.
Usage
Build
% Configure MEX once
mex -setup C++
% Build from repo root — regenerates extend_problem.m and MEX binaries
build
Using custom goals in MATLAB
% Get the raw C++ pointer via SWIG's getCPtr()
cptr = uint64(problem.getCPtr(problem));
% Wrap it — MEP injects goals directly into the live C++ object
ep = extend_problem(cptr);
% Add goals — methods generated from C++ header signatures
ep.addMocoBOSGoal('bos_goal', 0.5, true, true, false);
ep.addMocoActivationGoal('activation_goal', 0.3, true, false, false);
solution = study.solve();
Complete example
model = Model('gait2392.osim');
study = MocoStudy();
problem = study.updProblem();
problem.setModel(model);
problem.setTimeBounds(0, 1.0);
problem.addGoal(MocoControlGoal('control_goal'));
cptr = uint64(problem.getCPtr(problem));
ep = extend_problem(cptr);
ep.addMocoBOSGoal('base_of_support', 1.0, true, true, false);
ep.addMocoActivationSquaredGoal('activation_squared', 0.5, true, false, false);
solver = study.initCasADiSolver();
solver.set_num_mesh_intervals(50);
solution = study.solve();
solution.write('custom_goals_solution.sto');
Technical details
The code generation pipeline in build.m:
| Step | Implementation | Output |
|---|---|---|
| Header parsing | Regex-based setter extraction | Method signatures and parameter lists |
| Class generation | Template-based code generation | extend_problem.m |
| MEX interface | Automated MEX compilation via CMake + MSVC | C++ to MATLAB bridge binaries |
| Library linking | Dynamic library management | Runtime goal loading from bin\RelWithDebInfo |
Dependencies: OpenSim 4.2–4.5 with MATLAB scripting support, MATLAB 2022a+, Visual Studio 2019+ with Desktop C++ workload, CMake 3.23.3+. Windows only.