API  4.5.1
For C++ developers
exampleMocoInverse.py

This is an example using the MocoInverse tool with a complex model to prescribe walking. This example also shows how to track electromyography data.

1 # -------------------------------------------------------------------------- #
2 # OpenSim Moco: exampleMocoInverse.py #
3 # -------------------------------------------------------------------------- #
4 # Copyright (c) 2023 Stanford University and the Authors #
5 # #
6 # Author(s): Christopher Dembia, Nicholas Bianco #
7 # #
8 # Licensed under the Apache License, Version 2.0 (the "License") you may #
9 # not use this file except in compliance with the License. You may obtain a #
10 # copy of the License at http://www.apache.org/licenses/LICENSE-2.0 #
11 # #
12 # Unless required by applicable law or agreed to in writing, software #
13 # distributed under the License is distributed on an "AS IS" BASIS, #
14 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
15 # See the License for the specific language governing permissions and #
16 # limitations under the License. #
17 # -------------------------------------------------------------------------- #
18 
19 # This example shows how to use the MocoInverse tool to exactly prescribe a
20 # motion and estimate muscle behavior for walking. The first example does not
21 # rely on electromyography data, while the second example penalizes deviation
22 # from electromyography data for a subset of muscles. The third example
23 # extracts muscle synergies from the muscle excitaitons from the first example
24 # and uses them to solve the inverse problem using SynergyControllers.
25 #
26 # All examples use the Python utility osim.report to automatically generate a
27 # PDF that includes the trajectories of all states and controls in the solution.
28 # This utility requires a Python environment with Matplotlib and NumPy installed.
29 #
30 # See the README.txt next to this file for more information.
31 
32 import opensim as osim
33 
34 def solveMocoInverse():
35 
36  # Construct the MocoInverse tool.
37  inverse = osim.MocoInverse()
38 
39  # Construct a ModelProcessor and set it on the tool. The default
40  # muscles in the model are replaced with optimization-friendly
41  # DeGrooteFregly2016Muscles, and adjustments are made to the default muscle
42  # parameters.
43  modelProcessor = osim.ModelProcessor('subject_walk_scaled.osim')
44  modelProcessor.append(osim.ModOpAddExternalLoads('grf_walk.xml'))
45  modelProcessor.append(osim.ModOpIgnoreTendonCompliance())
46  modelProcessor.append(osim.ModOpReplaceMusclesWithDeGrooteFregly2016())
47  # Only valid for DeGrooteFregly2016Muscles.
48  modelProcessor.append(osim.ModOpIgnorePassiveFiberForcesDGF())
49  # Only valid for DeGrooteFregly2016Muscles.
50  modelProcessor.append(osim.ModOpScaleActiveFiberForceCurveWidthDGF(1.5))
51  # Use a function-based representation for the muscle paths. This is
52  # recommended to speed up convergence, but if you would like to use
53  # the original GeometryPath muscle wrapping instead, simply comment out
54  # this line. To learn how to create a set of function-based paths for
55  # your model, see the example 'examplePolynomialPathFitter.py'.
56  modelProcessor.append(osim.ModOpReplacePathsWithFunctionBasedPaths(
57  "subject_walk_scaled_FunctionBasedPathSet.xml"))
58  modelProcessor.append(osim.ModOpAddReserves(1.0))
59  inverse.setModel(modelProcessor)
60 
61  # Construct a TableProcessor of the coordinate data and pass it to the
62  # inverse tool. TableProcessors can be used in the same way as
63  # ModelProcessors by appending TableOperators to modify the base table.
64  # A TableProcessor with no operators, as we have here, simply returns the
65  # base table.
66  inverse.setKinematics(osim.TableProcessor('coordinates.sto'))
67 
68  # Initial time, final time, and mesh interval.
69  inverse.set_initial_time(0.48)
70  inverse.set_final_time(1.61)
71  inverse.set_mesh_interval(0.02)
72 
73  # By default, Moco gives an error if the kinematics contains extra columns.
74  # Here, we tell Moco to allow (and ignore) those extra columns.
75  inverse.set_kinematics_allow_extra_columns(True)
76 
77  # Solve the problem and write the solution to a Storage file.
78  solution = inverse.solve()
79  solution.getMocoSolution().write('example3DWalking_MocoInverse_solution.sto')
80 
81  # Generate a PDF with plots for the solution trajectory.
82  model = modelProcessor.process()
83  report = osim.report.Report(model,
84  'example3DWalking_MocoInverse_solution.sto',
85  bilateral=True)
86  # The PDF is saved to the working directory.
87  report.generate()
88 
89 def solveMocoInverseWithEMG():
90 
91  # This initial block of code is identical to the code above.
92  inverse = osim.MocoInverse()
93  modelProcessor = osim.ModelProcessor('subject_walk_scaled.osim')
94  modelProcessor.append(osim.ModOpAddExternalLoads('grf_walk.xml'))
95  modelProcessor.append(osim.ModOpIgnoreTendonCompliance())
96  modelProcessor.append(osim.ModOpReplaceMusclesWithDeGrooteFregly2016())
97  modelProcessor.append(osim.ModOpIgnorePassiveFiberForcesDGF())
98  modelProcessor.append(osim.ModOpScaleActiveFiberForceCurveWidthDGF(1.5))
99  modelProcessor.append(osim.ModOpReplacePathsWithFunctionBasedPaths(
100  "subject_walk_scaled_FunctionBasedPathSet.xml"))
101  modelProcessor.append(osim.ModOpAddReserves(1.0))
102  inverse.setModel(modelProcessor)
103  inverse.setKinematics(osim.TableProcessor('coordinates.sto'))
104  inverse.set_initial_time(0.48)
105  inverse.set_final_time(1.61)
106  inverse.set_mesh_interval(0.02)
107  inverse.set_kinematics_allow_extra_columns(True)
108 
109  study = inverse.initialize()
110  problem = study.updProblem()
111 
112  # Add electromyography tracking.
113  emgTracking = osim.MocoControlTrackingGoal('emg_tracking')
114  emgTracking.setWeight(50.0)
115  # Each column in electromyography.sto is normalized so the maximum value in
116  # each column is 1.0.
117  controlsRef = osim.TimeSeriesTable('electromyography.sto')
118  # Scale the tracked muscle activity based on peak levels from
119  # "Gait Analysis: Normal and Pathological Function" by
120  # Perry and Burnfield, 2010 (digitized by Carmichael Ong).
121  soleus = controlsRef.updDependentColumn('soleus')
122  gasmed = controlsRef.updDependentColumn('gastrocnemius')
123  tibant = controlsRef.updDependentColumn('tibialis_anterior')
124  for t in range(0, controlsRef.getNumRows()):
125  soleus[t] = 0.77 * soleus[t]
126  gasmed[t] = 0.87 * gasmed[t]
127  tibant[t] = 0.37 * tibant[t]
128  emgTracking.setReference(osim.TableProcessor(controlsRef))
129  # Associate actuators in the model with columns in electromyography.sto.
130  emgTracking.setReferenceLabel('/forceset/soleus_r', 'soleus')
131  emgTracking.setReferenceLabel('/forceset/gasmed_r', 'gastrocnemius')
132  emgTracking.setReferenceLabel('/forceset/gaslat_r', 'gastrocnemius')
133  emgTracking.setReferenceLabel('/forceset/tibant_r', 'tibialis_anterior')
134  problem.addGoal(emgTracking)
135 
136  # Solve the problem and write the solution to a Storage file.
137  solution = study.solve()
138  solution.write('example3DWalking_MocoInverseWithEMG_solution.sto')
139 
140  # Write the reference data in a way that's easy to compare to the solution.
141  controlsRef.removeColumn('medial_hamstrings')
142  controlsRef.removeColumn('biceps_femoris')
143  controlsRef.removeColumn('vastus_lateralis')
144  controlsRef.removeColumn('vastus_medius')
145  controlsRef.removeColumn('rectus_femoris')
146  controlsRef.removeColumn('gluteus_maximus')
147  controlsRef.removeColumn('gluteus_medius')
148  controlsRef.setColumnLabels(['/forceset/soleus_r', '/forceset/gasmed_r',
149  '/forceset/tibant_r'])
150  controlsRef.appendColumn('/forceset/gaslat_r', gasmed)
151  osim.STOFileAdapter.write(controlsRef, 'controls_reference.sto')
152 
153  # Generate a report comparing MocoInverse solutions without and with EMG
154  # tracking.
155  model = modelProcessor.process()
156  output = 'example3DWalking_MocoInverseWithEMG_report.pdf'
157  ref_files = [
158  'controls_reference.sto',
159  'example3DWalking_MocoInverseWithEMG_solution.sto']
160  report = osim.report.Report(model,
161  'example3DWalking_MocoInverse_solution.sto',
162  output=output, bilateral=True,
163  ref_files=ref_files,
164  colors=['black', 'blue', 'red'])
165  # The PDF is saved to the working directory.
166  report.generate()
167 
168 
169 # This problem extracts muscle synergies from the muscle excitations from the
170 # first example and uses them to solve the inverse problem using
171 # SynergyControllers.
172 def solveMocoInverseWithSynergies(numSynergies=5):
173 
174  # Construct the base model using a ModelProcessor as in the previous
175  # examples, with the exception that we ignore activation dynamics to
176  # simplify the problem given the muscle synergy controllers.
177  modelProcessor = osim.ModelProcessor('subject_walk_scaled.osim')
178  modelProcessor.append(osim.ModOpAddExternalLoads('grf_walk.xml'))
179  modelProcessor.append(osim.ModOpIgnoreTendonCompliance())
180  modelProcessor.append(osim.ModOpIgnoreActivationDynamics())
181  modelProcessor.append(osim.ModOpReplaceMusclesWithDeGrooteFregly2016())
182  modelProcessor.append(osim.ModOpIgnorePassiveFiberForcesDGF())
183  modelProcessor.append(osim.ModOpScaleActiveFiberForceCurveWidthDGF(1.5))
184  modelProcessor.append(osim.ModOpReplacePathsWithFunctionBasedPaths(
185  "subject_walk_scaled_FunctionBasedPathSet.xml"))
186  modelProcessor.append(osim.ModOpAddReserves(1.0))
187  model = modelProcessor.process()
188 
189  # Load the solution from solveMocoInverse() to extract the muscle
190  # control variable names and excitations for the left and right legs.
191  prevSolution = osim.MocoTrajectory('example3DWalking_MocoInverse_solution.sto')
192  leftControlNames = list()
193  rightControlNames = list()
194  model.initSystem()
195  for actu in model.getComponentsList():
196  if actu.getConcreteClassName().endswith('Muscle'):
197  name = actu.getName()
198  if name.endswith('_r'):
199  rightControlNames.append(actu.getAbsolutePathString())
200  elif name.endswith('_l'):
201  leftControlNames.append(actu.getAbsolutePathString())
202 
203  controls = prevSolution.exportToControlsTable()
204  leftControls = osim.TimeSeriesTable(controls.getIndependentColumn())
205  rightControls = osim.TimeSeriesTable(controls.getIndependentColumn())
206  for name in leftControlNames:
207  leftControls.appendColumn(name, controls.getDependentColumn(name))
208  for name in rightControlNames:
209  rightControls.appendColumn(name, controls.getDependentColumn(name))
210 
211  # SynergyController
212  # -----------------
213  # SynergyController defines the controls for actuators connected to the
214  # controller using a linear combination of time-varying synergy control
215  # signals (i.e., "synergy excitations") and a set of vectors containing
216  # weights for each actuator representing the contribution of each synergy
217  # excitation to the total control signal for that actuator
218  # (i.e., "synergy vectors").
219  #
220  # If 'N' is the number of time points in the trajectory, 'M' is the number
221  # of actuators connected to the controller, and 'K' is the number of
222  # synergies in the controller, then:
223  # - The synergy excitations are a matrix 'W' of size N x K.
224  # - The synergy vectors are a matrix 'H' of size K x M.
225  # - The controls for the actuators are computed A = W * H.
226  #
227  # SynergyController is a concrete implementation of InputController, which
228  # means that it uses Input control signals as the synergy excitations.
229  # Moco automatically detects InputController%s in a model provided to a
230  # MocoProblem and adds continuous variables to the optimization problem
231  # for each Input control signal. The variable names are based on the path
232  # to the controller appended with the Input control labels (e.g.,
233  # "/path/to/my_synergy_controller/synergy_excitation_0").
234 
235  # Use non-negative matrix factorization (NNMF) to extract a set of muscle
236  # synergies for each leg.
237  from sklearn.decomposition import NMF
238  import numpy as np
239  nmf = NMF(n_components=numSynergies, init='random', random_state=0)
240 
241  Al = leftControls.getMatrix().to_numpy()
242  Wl = nmf.fit_transform(Al)
243  Hl = nmf.components_
244 
245  Ar = rightControls.getMatrix().to_numpy()
246  Wr = nmf.fit_transform(Ar)
247  Hr = nmf.components_
248 
249  # Scale W and H assuming that the elements of H are all 0.5. This prevents the
250  # synergy vector weights and synergy excitations from being very large or very
251  # small.
252  scaleVec = 0.5*np.ones(Hl.shape[1])
253  for i in range(numSynergies):
254  scale_l = np.linalg.norm(scaleVec) / np.linalg.norm(Hl[i, :])
255  Hl[i, :] *= scale_l
256  Wl[:, i] /= scale_l
257 
258  scale_r = np.linalg.norm(scaleVec) / np.linalg.norm(Hr[i, :])
259  Hr[i, :] *= scale_r
260  Wr[:, i] /= scale_r
261 
262  # Add a SynergyController for the left leg to the model.
263  leftController = osim.SynergyController()
264  leftController.setName("synergy_controller_left_leg")
265  # The number of actuators connected to the controller defines the number of
266  # weights in each synergy vector expected by the controller.
267  for name in leftControlNames:
268  leftController.addActuator(
269  osim.Muscle.safeDownCast(model.getComponent(name)))
270  # Adding a synergy vector increases the number of synergies in the
271  # controller by one. This means that the number of Input control
272  # signals expected by the controller is also increased by one.
273  for i in range(numSynergies):
274  synergyVector = osim.Vector(Hl.shape[1], 0.0)
275  for j in range(Hl.shape[1]):
276  synergyVector.set(j, Hl[i, j])
277  leftController.addSynergyVector(synergyVector)
278  model.addController(leftController)
279 
280  # Add a SynergyController for the right leg to the model.
281  rightController = osim.SynergyController()
282  rightController.setName("synergy_controller_right_leg")
283  for name in rightControlNames:
284  rightController.addActuator(
285  osim.Muscle.safeDownCast(model.getComponent(name)))
286  for i in range(numSynergies):
287  synergyVector = osim.Vector(Hr.shape[1], 0.0)
288  for j in range(Hr.shape[1]):
289  synergyVector.set(j, Hr[i, j])
290  rightController.addSynergyVector(synergyVector)
291  model.addController(rightController)
292  model.finalizeConnections()
293  model.initSystem()
294 
295  # Construct the MocoInverse tool.
296  inverse = osim.MocoInverse()
297  inverse.setName("example3DWalking_MocoInverseWithSynergies")
298  inverse.setModel(osim.ModelProcessor(model))
299  inverse.setKinematics(osim.TableProcessor('coordinates.sto'))
300  inverse.set_initial_time(0.48)
301  inverse.set_final_time(1.61)
302  inverse.set_mesh_interval(0.02)
303  inverse.set_kinematics_allow_extra_columns(True)
304 
305  # Initialize the MocoInverse study and set the control bounds for the
306  # muscle synergies excitations. 'setInputControlInfo()' is equivalent to
307  # 'setControlInfo()', but reserved for Input control variables. Note that
308  # while we make a distinction between "control" variables and
309  # "Input control" variables in the API, the optimal control problem
310  # constructed by Moco treats them both as algebraic variables.
311  study = inverse.initialize()
312  problem = study.updProblem()
313 
314  # We will also increase the weight on the synergy excitations in the
315  # control effort cost term. MocoControlGoal, and other MocoGoals, that
316  # depend on control variables have options configuring cost terms with
317  # Input control values.
318  effort = osim.MocoControlGoal.safeDownCast(
319  problem.updGoal("excitation_effort"))
320  for i in range(numSynergies):
321  nameLeft = (f'/controllerset/synergy_controller_left_leg'
322  f'/synergy_excitation_{i}')
323  problem.setInputControlInfo(nameLeft, [0, 1.0])
324  effort.setWeightForControl(nameLeft, 10)
325 
326  nameRight = (f'/controllerset/synergy_controller_right_leg'
327  f'/synergy_excitation_{i}')
328  problem.setInputControlInfo(nameRight, [0, 1.0])
329  effort.setWeightForControl(nameRight, 10)
330 
331  # Solve!
332  solution = study.solve()
333 
334  # This function computes the model control values from the
335  # SynergyControllers in the model and inserts them into the solution.
336  solution.generateControlsFromModelControllers(model)
337 
338  # Add the multibody states into the solutions so we can visualize the
339  # trajectory or utilize the plotting utilities.
340  coordinateValues = prevSolution.exportToValuesTable()
341  coordinateSpeeds = prevSolution.exportToSpeedsTable()
342  solution.insertStatesTrajectory(coordinateValues)
343  solution.insertStatesTrajectory(coordinateSpeeds)
344 
345  # Write the solution to a Storage file.
346  solutionFile = (f'example3DWalking_MocoInverseWith'
347  f'{numSynergies}Synergies_solution.sto')
348  solution.write(solutionFile)
349 
350  # Generate a report comparing MocoInverse solutions with and without
351  # muscle synergies.
352  output = (f'example3DWalking_MocoInverseWith'
353  f'{numSynergies}Synergies_report.pdf')
354  ref_files = ['example3DWalking_MocoInverse_solution.sto']
355  report = osim.report.Report(model, solutionFile,
356  output=output, bilateral=True,
357  ref_files=ref_files,
358  colors=['black', 'red'])
359  # The PDF is saved to the working directory.
360  report.generate()
361 
362 
363 # Solve the basic muscle redundancy problem with MocoInverse.
364 solveMocoInverse()
365 
366 # This problem penalizes the deviation from electromyography data for a
367 # subset of muscles.
368 solveMocoInverseWithEMG()
369 
370 # This problem extracts muscle synergies from the muscle excitations from
371 # the first example and uses them to solve the inverse problem using
372 # SynergyControllers.
373 numSynergies = 5
374 solveMocoInverseWithSynergies(numSynergies)