├── .gitignore
├── ActivityAnalyses
└── gait_analysis.py
├── CHANGELOG.md
├── Examples
├── ForcePlateIntegration
│ ├── ID_setup
│ │ ├── Setup_ExternalLoads.xml
│ │ └── Setup_ID.xml
│ ├── example_integrate_forceplates.py
│ └── readme.md
├── example_COM_analysis.ipynb
├── example_COM_analysis.py
├── example_gait_analysis.py
├── example_get_sessions_subject.py
└── example_walking_opensimAD.py
├── LICENSE.md
├── Moco
├── README.md
├── exampleSquat
│ ├── exampleSquat.m
│ └── squats1_videoAndMocap.mot
├── exampleWalking
│ ├── exampleWalking.m
│ └── walking1_videoAndMocap.mot
└── models
│ └── LaiArnoldModified2017_contacts.osim
├── OpenSimPipeline
├── InverseDynamics
│ ├── DefaultPosition_rajagopal.mot
│ └── Setup_InverseDynamics.xml
├── JointReaction
│ ├── Setup_JointReaction.xml
│ └── computeJointLoading.py
├── Models
│ └── LaiUhlrich2022.osim
└── MuscleAnalysis
│ └── DummyMotion.mot
├── README.md
├── Resources
└── README.txt
├── UtilsDynamicSimulations
└── OpenSimAD
│ ├── README.md
│ ├── boundsOpenSimAD.py
│ ├── buildExpressionGraph
│ └── CMakeLists.txt
│ ├── buildExternalFunction
│ └── CMakeLists.txt
│ ├── functionCasADiOpenSimAD.py
│ ├── initialGuessOpenSimAD.py
│ ├── mainOpenSimAD.py
│ ├── muscleDataOpenSimAD.py
│ ├── muscleModelOpenSimAD.py
│ ├── plotsOpenSimAD.py
│ ├── polynomialsOpenSimAD.py
│ ├── settingsOpenSimAD.py
│ ├── utilsKineticsOpenSimAD.py
│ └── utilsOpenSimAD.py
├── batchDownload.ipynb
├── batchDownload.py
├── createAuthenticationEnvFile.py
├── example.ipynb
├── example.py
├── example_kinetics.py
├── requirements.txt
├── utils.py
├── utilsAPI.py
├── utilsAuthentication.py
├── utilsKinematics.py
├── utilsPlotting.py
├── utilsProcessing.py
└── utilsTRC.py
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | .env
3 | *.log
4 | *.ipynb_checkpoints
5 | Data/*
6 | **/Data/
7 |
8 | *DS_Store
9 |
10 | .vscode/*
11 |
12 | UtilsDynamicSimulations/OpenSimAD/build-ExpressionGraphF*
13 | UtilsDynamicSimulations/OpenSimAD/build-ExternalFunctionF*
14 | UtilsDynamicSimulations/OpenSimAD/install-ExternalFunctionF*
15 | UtilsDynamicSimulations/OpenSimAD/opensimAD-install/*
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | This document lists the changes to `opencap-processing`. When possible, we provide the date, GitHub issues, or pull requests that are related to the items below. If there is no issue or pull request related to the change, then we may provide the commit.
2 |
3 | This is not a comprehensive list of changes but rather a hand-curated collection of the more notable ones. For a comprehensive history, see the [OpenCap Processing GitHub repo](https://github.com/stanfordnmbl/opencap-processing).
4 |
5 | Changes
6 | =======
7 | - 07/03/2023: Add support for torque-driven models ([pull request](https://github.com/stanfordnmbl/opencap-processing/pull/66))
8 | - 06/28/2023: Add support for contacts on one side only ([pull request](https://github.com/stanfordnmbl/opencap-processing/pull/57))
9 | - 12/16/2022: Install CMake using conda, no need to install manually anymore ([pull request](https://github.com/stanfordnmbl/opencap-processing/pull/45)).
10 |
--------------------------------------------------------------------------------
/Examples/ForcePlateIntegration/ID_setup/Setup_ExternalLoads.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | calcn_r
8 |
9 | ground
10 |
11 | ground
12 |
13 | R_ground_force_v
14 |
15 | R_ground_force_p
16 |
17 | R_ground_torque_
18 |
19 |
20 |
21 | calcn_l
22 |
23 | ground
24 |
25 | ground
26 |
27 | L_ground_force_v
28 |
29 | L_ground_force_p
30 |
31 | L_ground_torque_
32 |
33 |
34 |
35 |
36 | ../dummyFolder
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Examples/ForcePlateIntegration/ID_setup/Setup_ID.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ../subject1_scaled.osim
10 |
11 | 0.01 0.1
12 |
13 | Muscles
14 |
15 |
16 |
17 | dummy_motion.mot
18 |
19 | -1
20 |
21 | ID_OSIM.sto
22 |
23 |
24 |
25 | body_forces_at_joints.sto
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Examples/ForcePlateIntegration/example_integrate_forceplates.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_gait_analysis.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2023 Stanford University and the Authors
6 |
7 | Author(s): Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | Please contact us for any questions: https://www.opencap.ai/#contact
19 |
20 | This example shows how to integrate force data from forceplates for a jump.
21 |
22 | All data should be expressed in meters and Newtons.
23 |
24 | Input data:
25 | 1) OpenCap session identifier and trial name
26 | 2) Force data in a .mot file with 9 columns per leg: (see example data)
27 | (Fx, Fy, Fz, Tx, Ty, Tz, COPx, COPy, COPz). Column names should be
28 | (R_ground_force_x,...R_ground_torque_x,...,R_ground_force_px,...
29 | L_ground_force_x,........)
30 | All data should be expressed in meters and Newtons. The
31 |
32 | '''
33 |
34 | import os
35 | import sys
36 | sys.path.append("./../../")
37 | script_folder,_ = os.path.split(os.path.abspath(__file__))
38 |
39 | import numpy as np
40 | import pandas as pd
41 | from scipy.spatial.transform import Rotation as R
42 | import matplotlib.pyplot as plt
43 | import opensim
44 | import requests
45 |
46 | import utils as ut
47 | from utilsProcessing import lowPassFilter
48 | from utilsPlotting import plot_dataframe
49 | import utilsKinematics
50 |
51 | # %% User-defined variables.
52 |
53 | # OpenCap session information from url.
54 | # View example at https://app.opencap.ai/session/9eea5bf0-a550-4fa5-bc69-f5f072765848
55 | session_id = '9eea5bf0-a550-4fa5-bc69-f5f072765848'
56 | trial_name = 'jump2'
57 | # Specify where to download the kinematic data. Make sure there are no spaces
58 | # in this path.
59 | data_folder = os.path.abspath(os.path.join(script_folder,'Data', session_id))
60 |
61 | # Path and filename for force data. Should be a *.mot file of forces applied
62 | # to right and left foot.
63 | force_dir = os.path.join(data_folder,'MeasuredForces',trial_name)
64 | force_path = os.path.join(force_dir,'jump2_forces_filt30Hz.mot')
65 |
66 | # Download force data from gdrive: this is only for this example. This section
67 | # can be deleted if your force_path points to a local file.
68 | os.makedirs(force_dir,exist_ok=True)
69 | force_gdrive_url = 'https://drive.google.com/uc?id=1Uppzkskn7OiJ5GncNT2sl35deX_U3FTN&export=download'
70 | response = requests.get(force_gdrive_url)
71 | with open(force_path, 'wb') as f:
72 | f.write(response.content)
73 |
74 | # Lowpass filter ground forces and kinematics
75 | lowpass_filter_frequency = 30
76 | filter_force_data = True
77 | filter_kinematics_data = True
78 |
79 | ## Transform from force reference frame to OpenCap reference frame.
80 | # We will use 4 reference frames G, C, R, and L:
81 | # C (checkerboard) and G (ground) are aligned and axes are defined by checkerboard.
82 | # You should always have a black square in the top left corner, and the board
83 | # should be on its side.
84 | # x out of board, y up, z away from checkers (left if looking at board).
85 | # Origin (C0) is the top left black-to-black corner;
86 | # Origin (G0) defined as opencap as the lowest point in the trial. There is a
87 | # vertical offset value in the opencap data that defines r_C0_to_G0
88 | # Both kinematic and force data will be output in G.
89 | # R and L are the frames for the force plate data with origins R0 and L0.
90 |
91 | ## Position from checkerboard origin to force plate origin expressed in C.
92 | # You will need to measure this and position the checkerboard consistently for every
93 | # collection.
94 |
95 | r_C0_to_forceOrigin_exp_C = {'R': [0,-.191,.083],
96 | 'L': [0,-.191,.083]}
97 |
98 |
99 | ## Rotation from force plates to checkerboard
100 | # You can represent rotations however you like, just make it a
101 | # scipy.spatial.transform.Rotation object.
102 | # docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.transform.Rotation.html
103 |
104 | R_forcePlates_to_C = {'R':R.from_euler('y',-90,degrees=True),
105 | 'L':R.from_euler('y',-90,degrees=True)}
106 |
107 | # Flags
108 | visualize_synchronization = False # visualize kinematic/force synchronization
109 | run_ID = True
110 |
111 | # %% Functions
112 |
113 | def get_columns(list1,list2):
114 | inds = [i for i, item in enumerate(list2) if item in list1]
115 | return inds
116 |
117 | # %% Load and transform force data
118 | # We will transform the force data into the OpenCap reference frame. We will
119 | # then add a vertical offset to the COP, because the OpenCap data has a vertical
120 | # offset from the checkerboard origin.
121 |
122 | # Download kinematics data if folder doesn't exist. If incomplete data in folder,
123 | # delete the folder and re-run.
124 | if not os.path.exists(os.path.join(data_folder,'MarkerData')):
125 | _,model_name = ut.download_kinematics(session_id, folder=data_folder)
126 | else:
127 | model_name,_ = os.path.splitext(ut.get_model_name_from_metadata(data_folder))
128 |
129 | # Initiate kinematic analysis
130 | kinematics = utilsKinematics.kinematics(data_folder, trial_name,
131 | modelName=model_name,
132 | lowpass_cutoff_frequency_for_coordinate_values=10)
133 |
134 | # Get mass and vertical offset from opencap outputs
135 | opencap_settings = ut.get_main_settings(data_folder,trial_name)
136 | opencap_metadata = ut.import_metadata(os.path.join(data_folder,'sessionMetadata.yaml'))
137 | mass = opencap_metadata['mass_kg']
138 | if 'verticalOffset' in opencap_settings.keys():
139 | vertical_offset = opencap_settings['verticalOffset']
140 | else:
141 | vertical_offset = 0
142 |
143 | forces_structure = ut.storage_to_numpy(force_path)
144 | force_data = forces_structure.view(np.float64).reshape(forces_structure.shape + (-1,))
145 | force_headers = forces_structure.dtype.names
146 |
147 | # %%
148 |
149 | # Filter force data
150 | # Note - it is not great to filter COP data directly. In the example GRF data
151 | # we filtered raw forces and moments before computing COP.
152 | if filter_force_data:
153 | force_data[:,1:] = lowPassFilter(force_data[:,0], force_data[:,1:],
154 | lowpass_filter_frequency, order=4)
155 |
156 | # Rotate the forces into C
157 | quantity = ['ground_force_v','ground_torque_','ground_force_p']
158 | directions = ['x','y','z']
159 | for q in quantity:
160 | for leg in ['R','L']:
161 | force_columns= get_columns([leg + '_' + q + d for d in directions],force_headers)
162 | rot = R_forcePlates_to_C[leg]
163 | # we want r_expFP * R_FP_to_C, but rot.apply does R*r, so need to invert
164 | force_data[:,force_columns] = rot.inv().apply(force_data[:,force_columns])
165 |
166 | ## Transform COP from force plates to G
167 | r_G0_to_C0_expC = np.array((0,-vertical_offset,0))
168 |
169 | for leg in ['R','L']:
170 | force_columns = get_columns([leg + '_ground_force_p' + d for d in directions],force_headers)
171 | r_forceOrigin_to_COP_exp_C = force_data[:,force_columns]
172 | r_G0_to_COP_exp_G = ( r_G0_to_C0_expC +
173 | r_C0_to_forceOrigin_exp_C[leg] +
174 | r_forceOrigin_to_COP_exp_C )
175 | force_data[:,force_columns] = r_G0_to_COP_exp_G
176 |
177 | ## Time synchronize
178 | # Here we will use cross correlation of the summed vertical GRFs vs. COM acceleration
179 | center_of_mass_acc = kinematics.get_center_of_mass_accelerations(lowpass_cutoff_frequency=4)
180 | force_columns = get_columns([leg + '_ground_force_vy' for leg in ['R','L']],force_headers)
181 | forces_for_cross_corr = np.sum(force_data[:,force_columns],axis=1,keepdims=True)
182 |
183 | framerate_forces = 1/np.diff(force_data[:2,0])[0]
184 | framerate_kinematics = 1/np.diff(kinematics.time[:2])[0]
185 | time_forces_downsamp, forces_for_cross_corr_downsamp = ut.downsample(forces_for_cross_corr,
186 | force_data[:,0],
187 | framerate_forces,
188 | framerate_kinematics)
189 | forces_for_cross_corr_downsamp = lowPassFilter(time_forces_downsamp,
190 | forces_for_cross_corr_downsamp,
191 | 4, order=4)
192 |
193 | # get body mass from metadata
194 | mass = opencap_metadata['mass_kg']
195 |
196 | # zero pad the shorter signal
197 | dif_lengths = len(forces_for_cross_corr_downsamp) - len(center_of_mass_acc['y'])
198 | if dif_lengths > 0:
199 | com_signal = np.pad(center_of_mass_acc['y']*mass + mass*9.8,
200 | (int(np.floor(dif_lengths / 2)),
201 | int(np.ceil(dif_lengths / 2))),
202 | 'constant',constant_values=0)[:,np.newaxis]
203 | kinematics_pad_length = int(np.floor(dif_lengths / 2))
204 | force_signal = forces_for_cross_corr_downsamp
205 | else:
206 | force_signal = np.pad(forces_for_cross_corr_downsamp,
207 | (int(np.floor(np.abs(dif_lengths) / 2)),
208 | int(np.ceil(np.abs(dif_lengths) / 2))),
209 | 'constant', constant_values=0)
210 | kinematics_pad_length = 0
211 | com_signal = center_of_mass_acc['y'][:,np.newaxis]*mass + mass*9.8
212 |
213 | # compute the lag between GRFs and forces
214 | _,lag = ut.cross_corr(np.squeeze(com_signal),np.squeeze(force_signal),
215 | visualize=visualize_synchronization)
216 |
217 | force_data_new = np.copy(force_data)
218 | force_data_new[:,0] = force_data[:,0] - (-lag+kinematics_pad_length)/framerate_kinematics
219 |
220 | # Plot vertical force and (COM acceleration*m +mg)
221 | if visualize_synchronization:
222 | plt.figure()
223 | plt.plot(kinematics.time,center_of_mass_acc['y']*mass + mass*9.8,label='COM acceleration')
224 | plt.plot(force_data_new[:,0],forces_for_cross_corr, label = 'vGRF')
225 | plt.legend()
226 |
227 | # Force data directories
228 | force_folder = os.path.join(data_folder,'MeasuredForces',trial_name)
229 | os.makedirs(force_folder,exist_ok=True)
230 |
231 | # Save force data
232 | _,force_file_name = os.path.split(force_path)
233 | root,ext = os.path.splitext(force_file_name)
234 | force_output_path = os.path.join(force_folder,trial_name + '_syncd_forces.mot')
235 | ut.numpy_to_storage(force_headers, force_data_new, force_output_path, datatype=None)
236 |
237 | # Identify time range for ID
238 | time_range = {}
239 | time_range['start'] = np.max([force_data_new[0,0], kinematics.time[0]])
240 | time_range['end'] = np.min([force_data_new[-1,0], kinematics.time[-1]])
241 |
242 | # %% Run Inverse Dynamics
243 |
244 | # Make folders
245 | opensim_folder = os.path.join(data_folder,'OpenSimData')
246 |
247 | id_folder = os.path.join(opensim_folder,'InverseDynamics',trial_name)
248 | os.makedirs(id_folder,exist_ok=True)
249 |
250 | model_path = os.path.join(opensim_folder,'Model',model_name + '.osim')
251 | ik_path = os.path.join(opensim_folder,'Kinematics', trial_name + '.mot')
252 | el_path = os.path.join(id_folder,'Setup_ExternalLoads.xml')
253 | id_path = os.path.join(id_folder,'Setup_ID.xml')
254 |
255 | # Generic setups
256 | id_path_generic = os.path.join(script_folder,'ID_setup','Setup_ID.xml')
257 | el_path_generic = os.path.join(script_folder,'ID_setup','Setup_ExternalLoads.xml')
258 |
259 | if run_ID:
260 | # External loads
261 | opensim.Logger.setLevelString('error')
262 | ELTool = opensim.ExternalLoads(el_path_generic, True)
263 | ELTool.setDataFileName(force_output_path)
264 | ELTool.setName(trial_name)
265 | ELTool.printToXML(el_path)
266 |
267 | # ID
268 | IDTool = opensim.InverseDynamicsTool(id_path_generic)
269 | IDTool.setModelFileName(model_path)
270 | IDTool.setName(trial_name)
271 | IDTool.setStartTime(time_range['start'])
272 | IDTool.setEndTime(time_range['end'])
273 | IDTool.setExternalLoadsFileName(el_path)
274 | IDTool.setCoordinatesFileName(ik_path)
275 | if not filter_kinematics_data:
276 | freq = -1
277 | else:
278 | freq = lowpass_filter_frequency
279 | IDTool.setLowpassCutoffFrequency(freq)
280 | IDTool.setResultsDir(id_folder)
281 | IDTool.setOutputGenForceFileName(trial_name + '.sto')
282 | IDTool.printToXML(id_path)
283 | print('Running inverse dynamics.')
284 | IDTool.run()
285 |
286 | # %% Load and plot kinematics, forces, inverse dynamics results
287 |
288 | # Create force and ID dataframesID results
289 | id_output_path = os.path.join(id_folder,trial_name + '.sto')
290 | id_dataframe = ut.load_storage(id_output_path,outputFormat='dataframe')
291 |
292 | force_dataframe = pd.DataFrame(force_data_new, columns=force_headers)
293 |
294 | # Select columns to plot
295 | sagittal_dofs = ['ankle_angle','knee_angle','hip_flexion']
296 | kinematics_columns_plot = [s + '_' + leg for s in sagittal_dofs for leg in ['r','l']]
297 | moment_columns_plot = [s + '_moment' for s in kinematics_columns_plot]
298 | force_columns_plot = [leg + '_ground_force_v' + dir for leg in ['R','L']
299 | for dir in ['x','y','z']]
300 |
301 | # Make plots
302 | plt.close('all')
303 | plot_dataframe([kinematics.get_coordinate_values()], xlabel='time', ylabel='angle [deg]',
304 | y = kinematics_columns_plot ,title='Kinematics')
305 |
306 | plot_dataframe([id_dataframe], xlabel='time', ylabel='moment [Nm]',
307 | y = moment_columns_plot ,title='Moments')
308 |
309 | plot_dataframe([force_dataframe], xlabel='time', ylabel='force [N]',
310 | y = force_columns_plot ,title='Ground Forces',xrange=list(time_range.values()))
311 |
312 |
--------------------------------------------------------------------------------
/Examples/ForcePlateIntegration/readme.md:
--------------------------------------------------------------------------------
1 | 1) Identify the origin and frame of your force plates.
2 | 2) Identify a way to consistently place the checkerboard relative to the force plates. Black square at top left.
3 | 3) Measure from the checkerboard origin to force plate origin, expressed in checkerboard frame.
4 |
5 | 
6 |
7 |
8 | 4) After running the example_integrate_forceplates.py, sanity check that forces align with kinematics using OpenSim.
9 | a) load model file (.osim) that was downloaded from OpenCap into OpenSim (ensure there is not model offset by right clicking on it in the navigator and making Model Offset = 0 in all directions)
10 | b) load inverse kinematics file downloaded from OpenCap
11 | c) right click on the Motion in the Navigator, click 'Associate Motion Data,' and select the ground reaction forces (OpenCapData/MeasuredForces//_syncd_forces.mot)
12 |
13 | 
14 |
15 |
--------------------------------------------------------------------------------
/Examples/example_COM_analysis.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "metadata": {},
6 | "source": [
7 | "
"
8 | ]
9 | },
10 | {
11 | "cell_type": "markdown",
12 | "metadata": {},
13 | "source": [
14 | "# Effect of countermovement on jump height\n",
15 | "\n",
16 | "In this example, we will study the effect of countermovement on jump height when performing vertical jumps. We will use data measured with OpenCap of a participant performing vertical jumps with and without countermovement.\n",
17 | "\n",
18 | "We will go over several steps that include setting up the Colab environment, downloading and processing OpenCap data, and analyzing some results."
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "metadata": {},
24 | "source": [
25 | "# Install OpenSim using condacolab\n",
26 | "\n",
27 | "The first step is to install [condacolab](https://github.com/conda-incubator/condacolab) in your notebook. Once this is complete, you can use conda to install OpenSim into the environment. Note, this might take a few minutes to complete."
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "metadata": {},
34 | "outputs": [],
35 | "source": [
36 | "!pip install -q condacolab\n",
37 | "import condacolab\n",
38 | "condacolab.install()\n",
39 | "!conda install -c opensim-org opensim"
40 | ]
41 | },
42 | {
43 | "cell_type": "markdown",
44 | "metadata": {},
45 | "source": [
46 | "# Import the opencap-processing source code\n",
47 | "\n",
48 | "This example is part of the opencap-processing Github repo. Let's clone this repo and install some Python packages."
49 | ]
50 | },
51 | {
52 | "cell_type": "code",
53 | "execution_count": null,
54 | "metadata": {},
55 | "outputs": [],
56 | "source": [
57 | "!git clone https://github.com/stanfordnmbl/opencap-processing.git\n",
58 | "%cd /content/opencap-processing\n",
59 | "!python3 -m pip install -r requirements.txt"
60 | ]
61 | },
62 | {
63 | "cell_type": "markdown",
64 | "metadata": {},
65 | "source": [
66 | "If the block above runs successfully, you should see that it installed several Python packages. You should get a message starting with:\n",
67 | "\n",
68 | "```\n",
69 | "Successfully installed ...\n",
70 | "```"
71 | ]
72 | },
73 | {
74 | "cell_type": "markdown",
75 | "metadata": {},
76 | "source": [
77 | "# Import packages\n",
78 | "\n",
79 | "Now that the correct Python packages have been installed and the source code has been cloned to this notebook, you can import packages specific to this script.\n",
80 | "\n",
81 | "Note, you will be prompted to login to OpenCap with your account. If you have not created an account yet, visit [app.opencap.ai](https://app.opencap.ai/register-nmbl). This will enable you to authenticate and analyze your own data in the future."
82 | ]
83 | },
84 | {
85 | "cell_type": "code",
86 | "execution_count": null,
87 | "metadata": {},
88 | "outputs": [],
89 | "source": [
90 | "import os\n",
91 | "import sys\n",
92 | "sys.path.append(\"..\")\n",
93 | "import numpy as np\n",
94 | "import matplotlib.pyplot as plt\n",
95 | "import utilsKinematics\n",
96 | "from utils import download_kinematics"
97 | ]
98 | },
99 | {
100 | "cell_type": "markdown",
101 | "metadata": {},
102 | "source": [
103 | "You should see a `Login successful` message."
104 | ]
105 | },
106 | {
107 | "cell_type": "markdown",
108 | "metadata": {},
109 | "source": [
110 | "# Download data\n",
111 | "\n",
112 | "First, enter the session ID of the OpenCap data collection containing the jumping data. This will be used to download the data directly from OpenCap. We will share the session ID on the [confluence page of the workshop](https://simtk-confluence.stanford.edu:8443/display/OpenSim/ISB+Technical+Group+on+Computer+Simulation+%28TGCS%29+2023+OpenSim+Workshop) right after we collect data during the workshop. Note, you can also proceed with the example session ID below.\n",
113 | "\n",
114 | "Then, run the following block of code to download the data."
115 | ]
116 | },
117 | {
118 | "cell_type": "code",
119 | "execution_count": null,
120 | "metadata": {},
121 | "outputs": [],
122 | "source": [
123 | "\n",
124 | "# Specify session id; see end of url in app.opencap.ai/session/.\n",
125 | "session_id = \"3ef5cfad-cf8a-420b-af15-2d833a33cfb8\"\n",
126 | "\n",
127 | "# Specify trial names in a list; use None to process all trials in a session.\n",
128 | "specific_trial_names = None \n",
129 | "\n",
130 | "# Specify where to download the data.\n",
131 | "data_folder = os.path.join(\"./../Data\", session_id)\n",
132 | "\n",
133 | "# Download data.\n",
134 | "trial_names, modelName = download_kinematics(session_id, folder=data_folder, trialNames=specific_trial_names)"
135 | ]
136 | },
137 | {
138 | "cell_type": "markdown",
139 | "metadata": {},
140 | "source": [
141 | "# Process data\n",
142 | "\n",
143 | "Let's extract kinematic quantities using the OpenSim API. To do so, we use a class named `utilsKinematics.py`. Take a look at the `utilsKinematics.py` code in the github repo to see all the available functions.\n",
144 | "\n",
145 | "For this example, we will extract the center of mass values, speeds, and accelerations of the different trials."
146 | ]
147 | },
148 | {
149 | "cell_type": "code",
150 | "execution_count": null,
151 | "metadata": {},
152 | "outputs": [],
153 | "source": [
154 | "# Get center of mass kinematics.\n",
155 | "kinematics, center_of_mass = {}, {}\n",
156 | "center_of_mass['values'], center_of_mass['speeds'], center_of_mass['accelerations'] = {}, {}, {}\n",
157 | "for trial_name in trial_names:\n",
158 | " # Create object from class kinematics.\n",
159 | " kinematics[trial_name] = utilsKinematics.kinematics(data_folder, trial_name, modelName=modelName, lowpass_cutoff_frequency_for_coordinate_values=10)\n",
160 | " # Get center of mass values, speeds, and accelerations.\n",
161 | " center_of_mass['values'][trial_name] = kinematics[trial_name].get_center_of_mass_values(lowpass_cutoff_frequency=10)\n",
162 | " center_of_mass['speeds'][trial_name] = kinematics[trial_name].get_center_of_mass_speeds(lowpass_cutoff_frequency=10)\n",
163 | " center_of_mass['accelerations'][trial_name] = kinematics[trial_name].get_center_of_mass_accelerations(lowpass_cutoff_frequency=10)"
164 | ]
165 | },
166 | {
167 | "cell_type": "markdown",
168 | "metadata": {},
169 | "source": [
170 | "# Analyze data (part 1)\n",
171 | "\n",
172 | "Let's now compare the center of mass values and speeds between our trials. "
173 | ]
174 | },
175 | {
176 | "cell_type": "code",
177 | "execution_count": null,
178 | "metadata": {},
179 | "outputs": [],
180 | "source": [
181 | "# Plot center of mass vertical values and speeds.\n",
182 | "fig, axs = plt.subplots(2, 1, figsize=(6, 6), sharex=True)\n",
183 | "for trial_name in trial_names:\n",
184 | " # Align signals based on peak velocity.\n",
185 | " idx_peak_velocity = np.argmax(center_of_mass['speeds'][trial_name]['y'])\n",
186 | " time = center_of_mass['speeds'][trial_name]['time']\n",
187 | " x = time - time[idx_peak_velocity]\n",
188 | " # Plot center of mass values.\n",
189 | " y_values = center_of_mass['values'][trial_name]['y']\n",
190 | " y = y_values-y_values[0]\n",
191 | " axs[0].plot(x, y, label=trial_name, linewidth=3)\n",
192 | " # Plot center of mass speeds.\n",
193 | " y_speeds = center_of_mass['speeds'][trial_name]['y']\n",
194 | " y = y_speeds-y_speeds[0]\n",
195 | " axs[1].plot(x, y, label=trial_name, linewidth=3)\n",
196 | " \n",
197 | "# Figure setttings.\n",
198 | "for ax in axs:\n",
199 | " # Add labels. \n",
200 | " ax.legend(fontsize=14)\n",
201 | " # Remove top and right borders.\n",
202 | " ax.spines['top'].set_visible(False)\n",
203 | " ax.spines['right'].set_visible(False)\n",
204 | " # Change font size.\n",
205 | " ax.tick_params(axis='both', which='major', labelsize=16)\n",
206 | " # Change size labels.\n",
207 | " ax.xaxis.label.set_size(16)\n",
208 | " ax.yaxis.label.set_size(16)\n",
209 | "# Add labels.\n",
210 | "axs[0].set_ylabel('CoM position (m)')\n",
211 | "axs[1].set_ylabel('CoM velocity (m/s)')\n",
212 | "axs[1].set_xlabel('Time (s)')\n",
213 | "fig.align_ylabels(axs)"
214 | ]
215 | },
216 | {
217 | "cell_type": "markdown",
218 | "metadata": {},
219 | "source": [
220 | "# Analyze data (part 2)\n",
221 | "\n",
222 | "Finally, let's compare the vertical forces that results from the center of mass accelerations. To approximate these forces we will do a simple F = ma calculation."
223 | ]
224 | },
225 | {
226 | "cell_type": "code",
227 | "execution_count": null,
228 | "metadata": {},
229 | "outputs": [],
230 | "source": [
231 | "# Plot vertical forces from accelerations (F=ma).\n",
232 | "fig, ax = plt.subplots(1, 1, figsize=(6, 3))\n",
233 | "gravity = 9.81\n",
234 | "for trial_name in trial_names:\n",
235 | " # Align signals based on peak velocity.\n",
236 | " idx_peak_velocity = np.argmax(center_of_mass['speeds'][trial_name]['y'])\n",
237 | " time = center_of_mass['speeds'][trial_name]['time']\n",
238 | " x = time - time[idx_peak_velocity]\n",
239 | " # Plot vertical ground reaction force.\n",
240 | " y_accelerations = center_of_mass['accelerations'][trial_name]['y']\n",
241 | " y = (y_accelerations + gravity) / gravity # vGRF expressed in bodyweights: vGRF = m(a+g)/mg\n",
242 | " ax.plot(x, y, label=trial_name, linewidth=3)\n",
243 | "\n",
244 | "# Figure setttings.\n",
245 | "# Add labels. \n",
246 | "ax.set_ylabel('vGRF (bodyweight)')\n",
247 | "ax.set_xlabel('Time (s)') \n",
248 | "ax.legend(fontsize=14)\n",
249 | "# Remove top and right borders.\n",
250 | "ax.spines['top'].set_visible(False)\n",
251 | "ax.spines['right'].set_visible(False)\n",
252 | "# Change font size.\n",
253 | "ax.tick_params(axis='both', which='major', labelsize=16)\n",
254 | "# Change size labels.\n",
255 | "ax.xaxis.label.set_size(16)\n",
256 | "ax.yaxis.label.set_size(16)"
257 | ]
258 | },
259 | {
260 | "cell_type": "markdown",
261 | "metadata": {},
262 | "source": [
263 | "Version 1.0\n",
264 | "\n",
265 | "Creators: Antoine Falisse, Scott Uhlrich.\n",
266 | "Last Updated on July 21, 2023\n",
267 | "\n",
268 | "You can also find a non-Colab version of this script [here](https://github.com/stanfordnmbl/opencap-processing/blob/main/Examples/example_COM_analysis.py)\n",
269 | "\n",
270 | "This notebook is made available under the [Apache 2.0 license](https://www.apache.org/licenses/LICENSE-2.0)."
271 | ]
272 | },
273 | {
274 | "cell_type": "markdown",
275 | "metadata": {},
276 | "source": []
277 | }
278 | ],
279 | "metadata": {
280 | "kernelspec": {
281 | "display_name": "py37",
282 | "language": "python",
283 | "name": "python3"
284 | },
285 | "language_info": {
286 | "codemirror_mode": {
287 | "name": "ipython",
288 | "version": 3
289 | },
290 | "file_extension": ".py",
291 | "mimetype": "text/x-python",
292 | "name": "python",
293 | "nbconvert_exporter": "python",
294 | "pygments_lexer": "ipython3",
295 | "version": "3.7.10"
296 | },
297 | "orig_nbformat": 4
298 | },
299 | "nbformat": 4,
300 | "nbformat_minor": 2
301 | }
302 |
--------------------------------------------------------------------------------
/Examples/example_COM_analysis.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_COM_analysis.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2023 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | import os
22 | import sys
23 | sys.path.append("..")
24 | import numpy as np
25 | import matplotlib.pyplot as plt
26 | import utilsKinematics
27 | from utils import download_kinematics
28 |
29 | # %% User inputs.
30 | # Specify session id; see end of url in app.opencap.ai/session/.
31 | session_id = "3ef5cfad-cf8a-420b-af15-2d833a33cfb8"
32 |
33 | # Specify trial names in a list; use None to process all trials in a session.
34 | specific_trial_names = None
35 |
36 | # Specify where to download the data.
37 | data_folder = os.path.join("./../Data", session_id)
38 |
39 | # %% Download data.
40 | trial_names, modelName = download_kinematics(session_id, folder=data_folder, trialNames=specific_trial_names)
41 |
42 | # %% Get center of mass kinematics.
43 | kinematics, center_of_mass = {}, {}
44 | center_of_mass['values'], center_of_mass['speeds'], center_of_mass['accelerations'] = {}, {}, {}
45 | for trial_name in trial_names:
46 | # Create object from class kinematics.
47 | kinematics[trial_name] = utilsKinematics.kinematics(data_folder, trial_name, modelName=modelName, lowpass_cutoff_frequency_for_coordinate_values=10)
48 | # Get center of mass values, speeds, and accelerations.
49 | center_of_mass['values'][trial_name] = kinematics[trial_name].get_center_of_mass_values(lowpass_cutoff_frequency=10)
50 | center_of_mass['speeds'][trial_name] = kinematics[trial_name].get_center_of_mass_speeds(lowpass_cutoff_frequency=10)
51 | center_of_mass['accelerations'][trial_name] = kinematics[trial_name].get_center_of_mass_accelerations(lowpass_cutoff_frequency=10)
52 |
53 | # %% Plot center of mass vertical values and speeds.
54 | fig, axs = plt.subplots(2, 1, figsize=(6, 6), sharex=True)
55 | for trial_name in trial_names:
56 | # Align signals based on peak velocity.
57 | idx_peak_velocity = np.argmax(center_of_mass['speeds'][trial_name]['y'])
58 | time = center_of_mass['speeds'][trial_name]['time']
59 | x = time - time[idx_peak_velocity]
60 | # Plot center of mass values.
61 | y_values = center_of_mass['values'][trial_name]['y']
62 | y = y_values-y_values[0]
63 | axs[0].plot(x, y, label=trial_name, linewidth=3)
64 | # Plot center of mass speeds.
65 | y_speeds = center_of_mass['speeds'][trial_name]['y']
66 | y = y_speeds-y_speeds[0]
67 | axs[1].plot(x, y, label=trial_name, linewidth=3)
68 |
69 | # Figure setttings.
70 | for ax in axs:
71 | # Add labels.
72 | ax.legend(fontsize=14)
73 | # Remove top and right borders.
74 | ax.spines['top'].set_visible(False)
75 | ax.spines['right'].set_visible(False)
76 | # Change font size.
77 | ax.tick_params(axis='both', which='major', labelsize=16)
78 | # Change size labels.
79 | ax.xaxis.label.set_size(16)
80 | ax.yaxis.label.set_size(16)
81 | # Add labels.
82 | axs[0].set_ylabel('CoM position (m)')
83 | axs[1].set_ylabel('CoM velocity (m/s)')
84 | axs[1].set_xlabel('Time (s)')
85 | fig.align_ylabels(axs)
86 |
87 | # %% Plot vertical forces from accelerations (F=ma).
88 | fig, ax = plt.subplots(1, 1, figsize=(6, 3))
89 | gravity = 9.81
90 | for trial_name in trial_names:
91 | # Align signals based on peak velocity.
92 | idx_peak_velocity = np.argmax(center_of_mass['speeds'][trial_name]['y'])
93 | time = center_of_mass['speeds'][trial_name]['time']
94 | x = time - time[idx_peak_velocity]
95 | # Plot vertical ground reaction force.
96 | y_accelerations = center_of_mass['accelerations'][trial_name]['y']
97 | y = (y_accelerations + gravity) / gravity # vGRF expressed in bodyweights: vGRF = m(a+g)/mg
98 | ax.plot(x, y, label=trial_name, linewidth=3)
99 |
100 | # Figure setttings.
101 | # Add labels.
102 | ax.set_ylabel('vGRF (bodyweight)')
103 | ax.set_xlabel('Time (s)')
104 | ax.legend(fontsize=14)
105 | # Remove top and right borders.
106 | ax.spines['top'].set_visible(False)
107 | ax.spines['right'].set_visible(False)
108 | # Change font size.
109 | ax.tick_params(axis='both', which='major', labelsize=16)
110 | # Change size labels.
111 | ax.xaxis.label.set_size(16)
112 | ax.yaxis.label.set_size(16)
113 |
114 | # %% Show figures.
115 | plt.show()
--------------------------------------------------------------------------------
/Examples/example_gait_analysis.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_gait_analysis.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2023 Stanford University and the Authors
6 |
7 | Author(s): Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | Please contact us for any questions: https://www.opencap.ai/#contact
19 |
20 | This example shows how to run a kinematic analysis of gait data. It works
21 | with either treadmill or overground gait. You can compute scalar metrics
22 | as well as gait cycle-averaged kinematic curves.
23 |
24 | '''
25 |
26 | import os
27 | import sys
28 | sys.path.append("../")
29 | sys.path.append("../ActivityAnalyses")
30 |
31 | from gait_analysis import gait_analysis
32 | from utils import get_trial_id, download_trial
33 | from utilsPlotting import plot_dataframe_with_shading
34 |
35 | # %% Paths.
36 | baseDir = os.path.join(os.getcwd(), '..')
37 | dataFolder = os.path.join(baseDir, 'Data')
38 |
39 | # %% User-defined variables.
40 | # Select example: options are treadmill and overground.
41 | example = 'treadmill'
42 |
43 | if example == 'treadmill':
44 | session_id = '4d5c3eb1-1a59-4ea1-9178-d3634610561c' # 1.25m/s
45 | trial_name = 'walk_1_25ms'
46 |
47 | elif example == 'overground':
48 | session_id = 'b39b10d1-17c7-4976-b06c-a6aaf33fead2'
49 | trial_name = 'gait_3'
50 |
51 | scalar_names = {'gait_speed','stride_length','step_width','cadence',
52 | 'single_support_time','double_support_time','step_length_symmetry'}
53 |
54 | # Select how many gait cycles you'd like to analyze. Select -1 for all gait
55 | # cycles detected in the trial.
56 | n_gait_cycles = -1
57 |
58 | # Select lowpass filter frequency for kinematics data.
59 | filter_frequency = 6
60 |
61 | # %% Gait analysis.
62 | # Get trial id from name.
63 | trial_id = get_trial_id(session_id,trial_name)
64 |
65 | # Set session path.
66 | sessionDir = os.path.join(dataFolder, session_id)
67 |
68 | # Download data.
69 | trialName = download_trial(trial_id,sessionDir,session_id=session_id)
70 |
71 | # Init gait analysis.
72 | gait_r = gait_analysis(
73 | sessionDir, trialName, leg='r',
74 | lowpass_cutoff_frequency_for_coordinate_values=filter_frequency,
75 | n_gait_cycles=n_gait_cycles)
76 | gait_l = gait_analysis(
77 | sessionDir, trialName, leg='l',
78 | lowpass_cutoff_frequency_for_coordinate_values=filter_frequency,
79 | n_gait_cycles=n_gait_cycles)
80 |
81 | # Compute scalars and get time-normalized kinematic curves.
82 | gaitResults = {}
83 | gaitResults['scalars_r'] = gait_r.compute_scalars(scalar_names)
84 | gaitResults['curves_r'] = gait_r.get_coordinates_normalized_time()
85 | gaitResults['scalars_l'] = gait_l.compute_scalars(scalar_names)
86 | gaitResults['curves_l'] = gait_l.get_coordinates_normalized_time()
87 |
88 | # %% Print scalar results.
89 | print('\nRight foot gait metrics:')
90 | print('-----------------')
91 | for key, value in gaitResults['scalars_r'].items():
92 | rounded_value = round(value['value'], 2)
93 | print(f"{key}: {rounded_value} {value['units']}")
94 |
95 | print('\nLeft foot gait metrics:')
96 | print('-----------------')
97 | for key, value in gaitResults['scalars_l'].items():
98 | rounded_value = round(value['value'], 2)
99 | print(f"{key}: {rounded_value} {value['units']}")
100 |
101 |
102 | # %% You can plot multiple curves, in this case we compare right and left legs.
103 | plot_dataframe_with_shading(
104 | [gaitResults['curves_r']['mean'], gaitResults['curves_l']['mean']],
105 | [gaitResults['curves_r']['sd'], gaitResults['curves_l']['sd']],
106 | leg = ['r','l'],
107 | xlabel = '% gait cycle',
108 | title = 'kinematics (m or deg)',
109 | legend_entries = ['right','left'])
--------------------------------------------------------------------------------
/Examples/example_get_sessions_subject.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_get_sessions_subject.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2024 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | Please contact us for any questions: https://www.opencap.ai/#contact
19 |
20 | This example shows how to retrieve all session IDs from a subject, given the
21 | subject's name. The example assumes that you have already authenticated
22 | with OpenCap. If you haven't, please run createAuthenticationEnvFile.py
23 | first.
24 |
25 | '''
26 | import sys
27 | sys.path.append("../")
28 |
29 | from utils import get_user_subjects, get_subject_sessions
30 |
31 | # Insert the name of the subject you are interested in.
32 | subject_name = 'my_subject_name'
33 |
34 | # Get list with all your subjects.
35 | subjects = get_user_subjects()
36 |
37 | # Get subject IDs. There could be multiple subjects with the same name.
38 | subject_ids = [subject['id'] for subject in subjects if subject['name'] == subject_name]
39 | print ("We found {} subjects with the name {}".format(len(subject_ids), subject_name))
40 |
41 | # Get session IDs from subject(s).
42 | session_ids = []
43 | for subject_id in subject_ids:
44 | session_ids.append(get_subject_sessions(subject_id))
45 |
--------------------------------------------------------------------------------
/Examples/example_walking_opensimAD.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_walking_opensimAD.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2023 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | This code makes use of CasADi, which is licensed under LGPL, Version 3.0;
19 | https://github.com/casadi/casadi/blob/master/LICENSE.txt.
20 |
21 | Install requirements:
22 | - Visit https://github.com/stanfordnmbl/opencap-processing for details.
23 | - Third-party software packages:
24 | - Windows
25 | - Visual studio: https://visualstudio.microsoft.com/downloads/
26 | - Make sure you install C++ support
27 | - Code tested with community editions 2017-2019-2022
28 | - Linux
29 | - OpenBLAS libraries
30 | - sudo apt-get install libopenblas-base
31 |
32 | Please contact us for any questions: https://www.opencap.ai/#contact
33 |
34 | This example shows how to run dynamic simulations of walking using
35 | data collected with OpenCap. The code uses OpenSimAD, a custom version
36 | of OpenSim that supports automatic differentiation (AD).
37 |
38 | This example is made of two sub-examples. The first one uses a torque-driven
39 | musculoskeletal model and the second one uses a muscle-driven
40 | musculoskeletal model. Please note that both examples are meant to
41 | demonstrate how to run dynamic simualtions and are not meant to be
42 | biomechanically valid. We only made sure the simulations converged to
43 | solutions that were visually reasonable. You can find more examples of
44 | dynamic simulations using OpenSimAD in example_kinetics.py.
45 | '''
46 |
47 | # %% Select the example you want to run.
48 | runTorqueDrivenProblem = True
49 | runMuscleDrivenProblem = False
50 | runComparison = False
51 |
52 | # %% Directories, paths, and imports. You should not need to change anything.
53 | import os
54 | import sys
55 | baseDir = os.path.join(os.getcwd(), '..')
56 | sys.path.append(baseDir)
57 | opensimADDir = os.path.join(baseDir, 'UtilsDynamicSimulations', 'OpenSimAD')
58 | sys.path.append(opensimADDir)
59 |
60 | from utilsOpenSimAD import processInputsOpenSimAD, plotResultsOpenSimAD
61 | from mainOpenSimAD import run_tracking
62 | from utilsAuthentication import get_token
63 | from utilsProcessing import segment_gait
64 | from utils import get_trial_id, download_trial
65 | from utilsKineticsOpenSimAD import kineticsOpenSimAD
66 | from utilsPlotting import plot_dataframe
67 |
68 | # %% OpenCap authentication. Visit https://app.opencap.ai/login to create an
69 | # account if you don't have one yet.
70 | get_token(saveEnvPath=os.getcwd())
71 |
72 | # %% Inputs common to both examples.
73 |
74 | # Insert your session ID here. You can find the ID of all your sessions at
75 | # https://app.opencap.ai/sessions.
76 | # Visit https://app.opencap.ai/session/ to visualize the data of
77 | # your session.
78 | session_id = "4d5c3eb1-1a59-4ea1-9178-d3634610561c"
79 |
80 | # Insert the name of the trial you want to simulate.
81 | trial_name = 'walk_1_25ms'
82 |
83 | # Insert the path to where you want the data to be downloaded.
84 | dataFolder = os.path.join(baseDir, 'Data')
85 |
86 | # Insert the type of activity you want to simulate. We have pre-defined settings
87 | # for different activities (more details above). Visit
88 | # ./UtilsDynamicSimulations/OpenSimAD/settingsOpenSimAD.py to see all available
89 | # activities and their settings. If your activity is not in the list, select
90 | # 'other' to use generic settings or set your own settings.
91 | motion_type = 'walking'
92 |
93 | # Insert the time interval you want to simulate. It is recommended to simulate
94 | # trials shorter than 2s (more details above). Set to [] to simulate full trial.
95 | # We here selected a time window that corresponds to a full gait stride in order
96 | # to use periodic constraints. You can use the gait segmentation function to
97 | # automatically segment the gait cycle. Also insert the speed of the treadmill
98 | # in m/s. A positive value indicates that the subject is moving forward.
99 | # You should ignore this parameter or set it to 0 if the trial was not measured
100 | # on a treadmill. You can also use the gait segmenter to automatically extract
101 | # the treadmill speed.
102 | segmentation_method = 'manual'
103 | if segmentation_method == 'automatic':
104 | # Download the trial.
105 | download_trial(get_trial_id(session_id,trial_name),
106 | os.path.join(dataFolder,session_id),
107 | session_id=session_id)
108 | time_window, gaitObject = segment_gait(
109 | session_id, trial_name, dataFolder, gait_cycles_from_end=3)
110 | treadmill_speed = gaitObject.treadmillSpeed
111 | else:
112 | time_window = [5.7333333, 6.9333333]
113 | treadmill_speed = 1.25
114 |
115 | # %% Sub-example 1: walking simulation with torque-driven model.
116 | # Insert a string to "name" you case.
117 | case = 'torque_driven'
118 |
119 | # Prepare inputs for dynamic simulation (this will be skipped if already done):
120 | # - Download data from OpenCap database
121 | # - Adjust wrapping surfaces
122 | # - Add foot-ground contacts
123 | # - Generate external function (OpenSimAD)
124 | settings = processInputsOpenSimAD(
125 | baseDir, dataFolder, session_id, trial_name, motion_type,
126 | time_window=time_window, treadmill_speed=treadmill_speed)
127 |
128 | # Adjust settings for this example.
129 | # Set the model to be torque-driven.
130 | settings['torque_driven_model'] = True
131 |
132 | # Adjust the weights of the objective function and remove the default
133 | # muscle-related weigths. The objective function contains terms for tracking
134 | # coordinate values (positionTrackingTerm), speeds (velocityTrackingTerm), and
135 | # accelerations (accelerationTrackingTerm), as well as terms for minimizing
136 | # excitations of the ideal torque actuators at the arms (armExcitationTerm),
137 | # lumbar (lumbarExcitationTerm), and lower-extremity (coordinateExcitationTerm)
138 | # joints. The objective function also contains a regularization term that
139 | # minimizes the coordinate accelerations (jointAccelerationTerm).
140 | settings['weights'] = {
141 | 'positionTrackingTerm': 10,
142 | 'velocityTrackingTerm': 10,
143 | 'accelerationTrackingTerm': 50,
144 | 'armExcitationTerm': 0.001,
145 | 'lumbarExcitationTerm': 0.001,
146 | 'coordinateExcitationTerm': 1,
147 | 'jointAccelerationTerm': 0.001,}
148 |
149 | # Add periodic constraints to the problem. This will constrain initial and
150 | # final states of the problem to be the same. This is useful for obtaining
151 | # faster convergence. Please note that the walking trial we selected might not
152 | # be perfectly periodic. We here add periodic constraints to show how to do it.
153 | # We here add periodic constraints for the coordinate values (coordinateValues)
154 | # and coordinate speeds (coordinateSpeeds) of the lower-extremity joints
155 | # (lowerLimbJoints). We also add periodic constraints for the activations of the
156 | # ideal torque actuators at the lower-extremity (lowerLimbJointActivations) and
157 | # lumbar (lumbarJointActivations) joints.
158 | settings['periodicConstraints'] = {
159 | 'coordinateValues': ['lowerLimbJoints'],
160 | 'coordinateSpeeds': ['lowerLimbJoints'],
161 | 'lowerLimbJointActivations': ['all'],
162 | 'lumbarJointActivations': ['all']}
163 |
164 | # Filter the data to be tracked. We here filter the coordinate values (Qs) with
165 | # a 6 Hz (cutoff_freq_Qs) low-pass filter, the coordinate speeds (Qds) with a 6
166 | # Hz (cutoff_freq_Qds) low-pass filter, and the coordinate accelerations (Qdds)
167 | # with a 6 Hz (cutoff_freq_Qdds) low-pass filter. We also compute the coordinate
168 | # accelerations by first splining the coordinate speeds (splineQds=True) and
169 | # then taking the first time derivative (default is to spline the coordinate
170 | # values and then take the second time derivative).
171 | settings['filter_Qs_toTrack'] = True
172 | settings['cutoff_freq_Qs'] = 6
173 | settings['filter_Qds_toTrack'] = True
174 | settings['cutoff_freq_Qds'] = 6
175 | settings['filter_Qdds_toTrack'] = True
176 | settings['cutoff_freq_Qdds'] = 6
177 | settings['splineQds'] = True
178 |
179 | # We set the mesh density to 50. We recommend using a mesh density of 100 by
180 | # default, but we here use a lower value to reduce the computation time.
181 | settings['meshDensity'] = 50
182 |
183 | # Run the dynamic simulation.
184 | if runTorqueDrivenProblem:
185 | run_tracking(baseDir, dataFolder, session_id, settings, case=case)
186 | # Plot some results.
187 | plotResultsOpenSimAD(dataFolder, session_id, trial_name, settings, [case])
188 |
189 | # %% Sub-example 2: walking simulation with muscle-driven model.
190 | # Insert a string to "name" you case.
191 | case = 'muscle_driven'
192 |
193 | # Prepare inputs for dynamic simulation (this will be skipped if already done):
194 | # - Download data from OpenCap database
195 | # - Adjust wrapping surfaces
196 | # - Add foot-ground contacts
197 | # - Generate external function (OpenSimAD)
198 | settings = processInputsOpenSimAD(
199 | baseDir, dataFolder, session_id, trial_name, motion_type,
200 | time_window=time_window, treadmill_speed=treadmill_speed)
201 |
202 | # Add periodic constraints to the problem. This will constrain initial and
203 | # final states of the problem to be the same. This is useful for obtaining
204 | # faster convergence. Please note that the walking trial we selected might not
205 | # be perfectly periodic. We here add periodic constraints to show how to do it.
206 | # We here add periodic constraints for the coordinate values (coordinateValues)
207 | # and coordinate speeds (coordinateSpeeds) of the lower-extremity joints
208 | # (lowerLimbJoints). We also add periodic constraints for the activations and
209 | # forces of all muscles acuating the lower-extremity (muscleActivationsForces)
210 | # and for activations of the ideal torque actuators at the lumbar
211 | # (lumbarJointActivations) joints.
212 | settings['periodicConstraints'] = {
213 | 'coordinateValues': ['lowerLimbJoints'],
214 | 'coordinateSpeeds': ['lowerLimbJoints'],
215 | 'muscleActivationsForces': ['all'],
216 | 'lumbarJointActivations': ['all']}
217 |
218 | # Filter the data to be tracked. We here filter the coordinate values (Qs) with
219 | # a 6 Hz (cutoff_freq_Qs) low-pass filter, the coordinate speeds (Qds) with a 6
220 | # Hz (cutoff_freq_Qds) low-pass filter, and the coordinate accelerations (Qdds)
221 | # with a 6 Hz (cutoff_freq_Qdds) low-pass filter. We also compute the coordinate
222 | # accelerations by first splining the coordinate speeds (splineQds=True) and
223 | # then taking the first time derivative (default is to spline the coordinate
224 | # values and then take the second time derivative).
225 | settings['filter_Qs_toTrack'] = True
226 | settings['cutoff_freq_Qs'] = 6
227 | settings['filter_Qds_toTrack'] = True
228 | settings['cutoff_freq_Qds'] = 6
229 | settings['filter_Qdds_toTrack'] = True
230 | settings['cutoff_freq_Qdds'] = 6
231 | settings['splineQds'] = True
232 |
233 | # We set the mesh density to 50. We recommend using a mesh density of 100 by
234 | # default, but we here use a lower value to reduce the computation time.
235 | settings['meshDensity'] = 50
236 |
237 | # Run the dynamic simulation.
238 | if runMuscleDrivenProblem:
239 | run_tracking(baseDir, dataFolder, session_id, settings, case=case)
240 | # Plot some results.
241 | plotResultsOpenSimAD(dataFolder, session_id, trial_name, settings, [case])
242 |
243 | # Retrieve results from the optimal solution using utilsKineticsOpenSimAD.
244 | opt_sol_obj = kineticsOpenSimAD(dataFolder, session_id, trial_name, case)
245 | # Extract and plot muscle forces.
246 | muscle_forces = opt_sol_obj.get_muscle_forces()
247 | plot_dataframe(
248 | dataframes = [muscle_forces],
249 | xlabel = 'Time (s)',
250 | ylabel = 'Force (N)',
251 | title = 'Muscle forces',
252 | labels = [trial_name])
253 |
254 | # %% Comparison torque-driven vs. muscle-driven model.
255 | if runComparison:
256 | plotResultsOpenSimAD(dataFolder, session_id, trial_name, settings,
257 | ['torque_driven', 'muscle_driven'])
258 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Moco/README.md:
--------------------------------------------------------------------------------
1 | # OpenSim Moco examples
2 |
3 | This folder contains examples that use [OpenSim Moco](https://opensim-org.github.io/opensim-moco-site/) to generate simulations using kinematic data processed by OpenCap. For installation instructions, please refer to the OpenSim Moco documentation. Currently, example scripts are provided in Matlab.
4 |
5 | ## Examples
6 | - `exampleSquat` contains an example Matlab script (`exampleSquat.m`) for tracking the provided squatting data (`squats1_videoAndMocap.mot`).
7 | - `exampleWalking` contains an example Matlab script (`exampleWalking.m`) for tracking the provided walking data (`walking1_videoAndMocap.mot`).
8 |
9 | ## Models
10 | - The `models` folder contains models that the example scripts use for tracking the data.
11 |
--------------------------------------------------------------------------------
/Moco/exampleSquat/exampleSquat.m:
--------------------------------------------------------------------------------
1 | % -------------------------------------------------------------------------- %
2 | % OpenSim Moco: exampleSquat.m %
3 | % -------------------------------------------------------------------------- %
4 | % Copyright (c) 2022 Stanford University and the Authors %
5 | % %
6 | % Author(s): Carmichael Ong, Antoine Falisse, Scott Uhlrich %
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 | % Example using OpenSim Moco to track OpenCap data of squatting. This example
20 | % solves in about 6 hours on a computer with 8 cores. The solution and solve
21 | % time can vary on different computers, so the solve time provided here is
22 | % provided as a rough guideline.
23 | %
24 | % Model
25 | % -----
26 | % This script uses the same base model from OpenCap and then adjusts the
27 | % model to use a muscle class better suited for use with Moco
28 | % (DeGrooteFregly2016), to increase the optimal force of
29 | % CoordinateActuators that actuate the lumbar and upper limb joints, and to
30 | % weld the toe (mtp) joints.
31 | %
32 | % Data
33 | % ----
34 | % Data are from a processed OpenCap trial of walking and contain coordinate
35 | % values after inverse kinematics.
36 |
37 | clear;
38 |
39 | % Load the Moco libraries
40 | import org.opensim.modeling.*;
41 |
42 | % ---------------------------------------------------------------------------
43 | % Set up a coordinate tracking problem where the goal is to minimize the
44 | % difference between provided and simulated coordinate values and speeds,
45 | % as well as to minimize an effort cost (squared controls). The provided
46 | % data represents multiple squats, and we choose initial and final
47 | % simulation times that correspond to a single squat. Endpoint
48 | % constraints enforce periodicity of the coordinate values (except for
49 | % pelvis tx) and speeds, coordinate actuator controls, and muscle activations.
50 |
51 |
52 | % Define the optimal control problem
53 | % ==================================
54 | mocoTrackName = 'squats1';
55 | track = MocoTrack();
56 | track.setName(mocoTrackName);
57 |
58 | % Set the weights and parameters for the terms in the objective function.
59 | % The values below were obtained by trial and error.
60 | controlEffortWeight = 0.1;
61 | stateTrackingWeight = 1;
62 | calcnPosTrackingWeight = 1000;
63 | calcnVelTrackingWeight = 100;
64 | calcnTargetTrackingHeight = 0.03;
65 |
66 | % Set other parameters, including the initial and final times of the
67 | % simulation, the mesh interval, the relative size of the state bounds, and
68 | % the optimal force for the lumbar and upper limb coordinate actuators.
69 | % Since we enforce periodicity, the initial and final time are chosen to be
70 | % time points at the start and end of the squat that are in the standing
71 | % position. These exact values were chosen by visualizing the input motion
72 | % in the OpenSim GUI and finding time points in the standing position at
73 | % the start and end of one of the squats.
74 | initialTime = 1.70;
75 | finalTime = 3.20;
76 | meshInterval = 0.08;
77 | fractionExtraBoundSize = 0.1;
78 | coordinateActuatorsOptimalForce = 250;
79 |
80 | % Reference data for tracking problem
81 | tableProcessor = TableProcessor('squats1_videoAndMocap.mot');
82 | tableProcessor.append(TabOpLowPassFilter(6));
83 | tableProcessor.append(TabOpUseAbsoluteStateNames());
84 |
85 | % Load the model and increase the optimal force for the coordinate
86 | % actuators that control the lumbar and upper limbs.
87 | inModel = Model('../models/LaiArnoldModified2017_contacts.osim');
88 | setCoordinateActuatorsOptimalForce(inModel, coordinateActuatorsOptimalForce);
89 |
90 | % Add additional hip rotation reserve actuators needed for a squat
91 | % simulation.
92 | hipRotActL = CoordinateActuator('hip_rotation_l');
93 | hipRotActL.setName('hip_rot_l');
94 | hipRotActL.setOptimalForce(30);
95 | hipRotActR = CoordinateActuator('hip_rotation_r');
96 | hipRotActR.setName('hip_rot_r');
97 | hipRotActR.setOptimalForce(30);
98 | inModel.addForce(hipRotActL);
99 | inModel.addForce(hipRotActR);
100 |
101 | % Set the ModelProcessor to weld mtp joints and adjust muscle model
102 | modelProcessor = ModelProcessor(inModel);
103 | jointsToWeld = StdVectorString();
104 | jointsToWeld.add('mtp_r');
105 | jointsToWeld.add('mtp_l');
106 | modelProcessor.append(ModOpReplaceJointsWithWelds(jointsToWeld));
107 | modelProcessor.append(ModOpReplaceMusclesWithDeGrooteFregly2016());
108 | modelProcessor.append(ModOpIgnorePassiveFiberForcesDGF());
109 | modelProcessor.append(ModOpTendonComplianceDynamicsModeDGF('implicit'));
110 |
111 | % Set the settings for the MocoTrack problem.
112 | track.setModel(modelProcessor);
113 | track.setStatesReference(tableProcessor);
114 | track.set_states_global_tracking_weight(stateTrackingWeight);
115 | track.set_allow_unused_references(true);
116 | track.set_track_reference_position_derivatives(true);
117 | track.set_apply_tracked_states_to_guess(true);
118 | track.set_initial_time(initialTime);
119 | track.set_final_time(finalTime);
120 | track.set_mesh_interval(meshInterval);
121 | study = track.initialize();
122 | problem = study.updProblem();
123 |
124 |
125 | % Goals
126 | % =====
127 |
128 | % Tracking
129 | % --------
130 | % Set different tracking weights for states (weights for states not
131 | % explicitly set here have a default value of 1.0). The values below
132 | % were obtained by trial and error.
133 | stateTrackingGoal = MocoStateTrackingGoal.safeDownCast(problem.updGoal('state_tracking'));
134 | stateTrackingGoal.setWeightForState('/jointset/ground_pelvis/pelvis_tilt/value', 10.0);
135 | stateTrackingGoal.setWeightForState('/jointset/ground_pelvis/pelvis_tilt/speed', 10.0);
136 | stateTrackingGoal.setWeightForState('/jointset/back/lumbar_extension/value', 10.0);
137 | stateTrackingGoal.setWeightForState('/jointset/back/lumbar_extension/speed', 10.0);
138 | stateTrackingGoal.setWeightForState('/jointset/hip_r/hip_flexion_r/value', 10.0);
139 | stateTrackingGoal.setWeightForState('/jointset/hip_r/hip_flexion_r/speed', 10.0);
140 | stateTrackingGoal.setWeightForState('/jointset/hip_l/hip_flexion_l/value', 10.0);
141 | stateTrackingGoal.setWeightForState('/jointset/hip_l/hip_flexion_l/speed', 10.0);
142 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_r/knee_angle_r/value', 10.0);
143 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_r/knee_angle_r/speed', 10.0);
144 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_l/knee_angle_l/value', 10.0);
145 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_l/knee_angle_l/speed', 10.0);
146 | stateTrackingGoal.setWeightForState('/jointset/ankle_r/ankle_angle_r/speed', 10.0);
147 | stateTrackingGoal.setWeightForState('/jointset/ankle_r/ankle_angle_r/value', 10.0);
148 | stateTrackingGoal.setWeightForState('/jointset/ankle_l/ankle_angle_l/speed', 10.0);
149 | stateTrackingGoal.setWeightForState('/jointset/ankle_l/ankle_angle_l/value', 10.0);
150 |
151 | % Periodicity
152 | % -----------
153 | % This goal enforces periodicity (i.e., enforce that certain values are
154 | % the same at the beginning of the simulation as the end of the
155 | % simulation).
156 | periodicityGoal = MocoPeriodicityGoal('periodicityGoal');
157 | problem.addGoal(periodicityGoal);
158 | model = modelProcessor.process();
159 | model.initSystem();
160 |
161 | % Periodic coordinate values (except for pelvis_tx) and speeds. Note that for
162 | % the pelvis translation coordinates, a periodic goal is added for the "y" and
163 | % "z" directions (vertical and side-to-side directions) because the coordinate
164 | % values at the initial and final times were small (< 1 cm). The "x"
165 | % direction (forward-backward direction) is not added as its difference was
166 | % larger between the initial and final time points, indicating that a person
167 | % performing a squat may end up leaning forward or backward more than in
168 | % the initial position.
169 | for i = 1:model.getNumStateVariables()
170 | currentStateName = string(model.getStateVariableNames().getitem(i-1));
171 | if startsWith(currentStateName , '/jointset')
172 | if (~contains(currentStateName, 'pelvis_tx/value'))
173 | periodicityGoal.addStatePair(MocoPeriodicityGoalPair(currentStateName));
174 | end
175 | end
176 | end
177 |
178 | % Periodic muscle activations.
179 | for i = 1:model.getNumStateVariables()
180 | currentStateName = string(model.getStateVariableNames().getitem(i-1));
181 | if endsWith(currentStateName,'/activation')
182 | periodicityGoal.addStatePair(MocoPeriodicityGoalPair(currentStateName));
183 | end
184 | end
185 |
186 | % Periodic coordinate actuator (lumbar and upper limb) controls.
187 | modelActuators = model.getActuators();
188 | for i = 1:modelActuators.getSize()
189 | thisActuator = modelActuators.get(i-1);
190 | if strcmp(thisActuator.getConcreteClassName, 'CoordinateActuator')
191 | thisPair = MocoPeriodicityGoalPair(thisActuator.getAbsolutePathString());
192 | periodicityGoal.addControlPair(thisPair);
193 | end
194 | end
195 |
196 | % Effort
197 | % ------
198 | % Get a reference to the MocoControlGoal that is added to every MocoTrack
199 | % problem by default and change the weight
200 | effort = MocoControlGoal.safeDownCast(problem.updGoal('control_effort'));
201 | effort.setWeight(controlEffortWeight);
202 |
203 | % Foot tracking
204 | % -------------
205 | % During a squat motion, often a person is asked to keep their heels on the
206 | % ground throughout the motion. In this simulation, we add tracking goals
207 | % for each foot to capture this goal. Specifically, for the two calcanei
208 | % bodies (calcn_r and calcn_l), we add a goal to track the vertical position
209 | % and velocity of the origin of the bodies. The vertical position tracks
210 | % a constant value provided in the settings above (calcnTargetTrackingHeight),
211 | % and the vertical velocity tracks a constant value of 0.0.
212 | calcnRPosYTracking = MocoOutputTrackingGoal();
213 | calcnRPosYTracking.setName('calcn_r_pos_y_tracking');
214 | calcnRPosYTracking.setOutputPath('/bodyset/calcn_r|position');
215 | calcnRPosYTracking.setExponent(2);
216 | calcnRPosYTracking.setOutputIndex(1);
217 | calcnRPosYTracking.setTrackingFunction(Constant(calcnTargetTrackingHeight));
218 | calcnRPosYTracking.setWeight(calcnPosTrackingWeight);
219 |
220 | calcnRVelYTracking = MocoOutputTrackingGoal();
221 | calcnRVelYTracking.setName('calcn_r_vel_y_tracking');
222 | calcnRVelYTracking.setOutputPath('/bodyset/calcn_r|linear_velocity');
223 | calcnRVelYTracking.setExponent(2);
224 | calcnRVelYTracking.setOutputIndex(1);
225 | calcnRVelYTracking.setTrackingFunction(Constant(0.0));
226 | calcnRVelYTracking.setWeight(calcnVelTrackingWeight);
227 |
228 | calcnLPosYTracking = MocoOutputTrackingGoal();
229 | calcnLPosYTracking.setName('calcn_l_pos_y_tracking');
230 | calcnLPosYTracking.setOutputPath('/bodyset/calcn_l|position');
231 | calcnLPosYTracking.setExponent(2);
232 | calcnLPosYTracking.setOutputIndex(1);
233 | calcnLPosYTracking.setTrackingFunction(Constant(calcnTargetTrackingHeight));
234 | calcnLPosYTracking.setWeight(calcnPosTrackingWeight);
235 |
236 | calcnLVelYTracking = MocoOutputTrackingGoal();
237 | calcnLVelYTracking.setName('calcn_l_vel_y_tracking');
238 | calcnLVelYTracking.setOutputPath('/bodyset/calcn_l|linear_velocity');
239 | calcnLVelYTracking.setExponent(2);
240 | calcnLVelYTracking.setOutputIndex(1);
241 | calcnLVelYTracking.setTrackingFunction(Constant(0.0));
242 | calcnLVelYTracking.setWeight(calcnVelTrackingWeight);
243 |
244 | problem.addGoal(calcnRPosYTracking);
245 | problem.addGoal(calcnRVelYTracking);
246 | problem.addGoal(calcnLPosYTracking);
247 | problem.addGoal(calcnLVelYTracking);
248 |
249 | % Bounds
250 | % ======
251 | % Set the bounds for all tracked states based on the range of the data.
252 |
253 | % Load the tracked_states file into a table to find the range of the
254 | % coordinate values in the data.
255 | trackedStatesFile = [mocoTrackName, '_tracked_states.sto'];
256 | trackedStatesTable = TimeSeriesTable(trackedStatesFile);
257 | trackedStatesTable.trimFrom(initialTime);
258 | trackedStatesTable.trimTo(finalTime);
259 |
260 | constrainBoundsAllTrackedStates(problem, trackedStatesTable, ...
261 | fractionExtraBoundSize);
262 |
263 | % Solve the problem
264 | % =================
265 | % Add a term to the solver that minimizes the implicit auxiliary
266 | % derivatives that are used with the implicit mode of muscles.
267 | solver = MocoCasADiSolver.safeDownCast(study.updSolver());
268 | solver.set_minimize_implicit_auxiliary_derivatives(true);
269 | solver.set_implicit_auxiliary_derivatives_weight(1e-6);
270 |
271 | solution = study.solve();
272 | solution.write(strcat(mocoTrackName, '.sto'));
273 |
274 | % Extract ground reaction forces
275 | % ==============================
276 | % Extract ground reaction forces for downstream analysis. Add the contact
277 | % force elements to vectors, then use Moco's
278 | % createExternalLoadsTableForGait() function.
279 | contact_r = StdVectorString();
280 | contact_l = StdVectorString();
281 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s1_r');
282 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s2_r');
283 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s3_r');
284 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s4_r');
285 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s5_r');
286 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s6_r');
287 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s1_l');
288 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s2_l');
289 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s3_l');
290 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s4_l');
291 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s5_l');
292 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s6_l');
293 |
294 | model = modelProcessor.process();
295 | externalForcesTableFlat = ...
296 | opensimMoco.createExternalLoadsTableForGait(model, ...
297 | solution.exportToStatesTrajectory(model), ...
298 | contact_r, contact_l);
299 | STOFileAdapter.write(externalForcesTableFlat, strcat(mocoTrackName, '_grfs.sto'));
300 |
301 | %% Helper functions
302 |
303 | % Set each state's bounds based on a fraction (fractionExtraBoundSize) of
304 | % the range of the state's value from the tracked data. The bounds are
305 | % set as the following:
306 | % - Lower bound: (minimum value) - fractionExtraBoundSize * (range of value)
307 | % - Upper bound: (maximum value) + fractionExtraBoundSize * (range of value)
308 | function constrainBoundsAllTrackedStates(problem, trackedStatesTable, ...
309 | fractionExtraBoundSize)
310 |
311 | % Get all of the state names from the table column labels but omit
312 | % the toe joint since it was locked in the model above. Store the
313 | % state names in a vector to pass into the function that constrains the
314 | % bounds.
315 | colLabelsStdVec = trackedStatesTable.getColumnLabels();
316 | colLabelsCell = cell(1, colLabelsStdVec.size());
317 | for i = 1:colLabelsStdVec.size()
318 | thisColLabel = char(colLabelsStdVec.get(i-1));
319 | if ~contains(thisColLabel, 'mtp_angle')
320 | colLabelsCell{i} = thisColLabel;
321 | end
322 | end
323 | colLabelsCell = colLabelsCell(~cellfun('isempty', colLabelsCell));
324 |
325 | for i = 1:numel(colLabelsCell)
326 | thisStatePath = colLabelsCell{i};
327 | stateColumn = ...
328 | trackedStatesTable.getDependentColumn(thisStatePath).getAsMat();
329 | thisColMin = min(stateColumn);
330 | thisColMax = max(stateColumn);
331 | thisColRange = thisColMax - thisColMin;
332 | extraBoundSize = thisColRange * fractionExtraBoundSize;
333 |
334 | thisBounds = [thisColMin - extraBoundSize, thisColMax + extraBoundSize];
335 | problem.setStateInfo(thisStatePath, thisBounds, [], []);
336 | end
337 |
338 | end
339 |
340 | % Set the optimal force of all CoordinateActuator's in the model to a given
341 | % value (optimalForce).
342 | function setCoordinateActuatorsOptimalForce(model, optimalForce)
343 | % import inside of the function needed for downcast step
344 | import org.opensim.modeling.*;
345 |
346 | modelActuators = model.getActuators();
347 | for i = 1:modelActuators.getSize()
348 | thisActuator = modelActuators.get(i-1);
349 | if strcmp(thisActuator.getConcreteClassName(), 'CoordinateActuator')
350 | thisCoordinateActuator = CoordinateActuator.safeDownCast(thisActuator);
351 | thisCoordinateActuator.setOptimalForce(optimalForce);
352 | end
353 | end
354 | end
--------------------------------------------------------------------------------
/Moco/exampleWalking/exampleWalking.m:
--------------------------------------------------------------------------------
1 | % -------------------------------------------------------------------------- %
2 | % OpenSim Moco: exampleWalking.m %
3 | % -------------------------------------------------------------------------- %
4 | % Copyright (c) 2022 Stanford University and the Authors %
5 | % %
6 | % Author(s): Carmichael Ong, Antoine Falisse, Scott Uhlrich %
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 | % Example using OpenSim Moco to track OpenCap data of walking. This example
20 | % solves in about 5 hours on a computer with 8 cores. The solution and solve
21 | % time can vary on different computers, so the solve time provided here is
22 | % provided as a rough guideline.
23 | %
24 | % Model
25 | % -----
26 | % This script uses the same base model from OpenCap and then adjusts the
27 | % model to use a muscle class better suited for use with Moco
28 | % (DeGrooteFregly2016), to increase the optimal force of
29 | % CoordinateActuators that actuate the lumbar and upper limb joints, and to
30 | % weld the toe (mtp) joints.
31 | %
32 | % Data
33 | % ----
34 | % Data are from a processed OpenCap trial of walking and contain coordinate
35 | % values after inverse kinematics.
36 |
37 | clear;
38 |
39 | % Load the Moco libraries
40 | import org.opensim.modeling.*;
41 |
42 | % ---------------------------------------------------------------------------
43 | % Set up a coordinate tracking problem where the goal is to minimize the
44 | % difference between provided and simulated coordinate values and speeds,
45 | % as well as to minimize an effort cost (squared controls). Endpoint
46 | % constraints enforce periodicity of the coordinate values (except for
47 | % pelvis tx) and speeds, coordinate actuator controls, and muscle
48 | % activations.
49 |
50 |
51 | % Define the optimal control problem
52 | % ==================================
53 | mocoTrackName = 'walking1';
54 | track = MocoTrack();
55 | track.setName(mocoTrackName);
56 |
57 | % Set the weights for the terms in the objective function. The values below were
58 | % obtained by trial and error.
59 | controlEffortWeight = 0.1;
60 | stateTrackingWeight = 1;
61 |
62 | % Set other parameters, including the initial and final times of the
63 | % simulation, the mesh interval, the relative size of the state bounds, and
64 | % the optimal force for the lumbar and upper limb coordinate actuators.
65 | % Since we enforce periodicity, the initial and final time are chosen to be
66 | % time points during adjacent strides at the same point in the gait cycle.
67 | % These exact values were chosen by visualizing the input motion in the
68 | % Opensim GUI and finding times with similar joint coordinate values.
69 | initialTime = 0.07;
70 | finalTime = 1.45;
71 | meshInterval = 0.05;
72 | fractionExtraBoundSize = 0.1;
73 | coordinateActuatorsOptimalForce = 250;
74 |
75 | % Reference data for tracking problem
76 | tableProcessor = TableProcessor('walking1_videoAndMocap.mot');
77 | tableProcessor.append(TabOpLowPassFilter(6));
78 | tableProcessor.append(TabOpUseAbsoluteStateNames());
79 |
80 | % Load the model and increase the optimal force for the coordinate
81 | % actuators that control the lumbar and upper limbs.
82 | inModel = Model('../models/LaiArnoldModified2017_contacts.osim');
83 | setCoordinateActuatorsOptimalForce(inModel, coordinateActuatorsOptimalForce);
84 |
85 | % Process the model to weld the toe (mtp) joints, replace the muscles in
86 | % the model to use the DeGrooteFregly2016 muscles, and to use the implicit
87 | % mode for the muscles.
88 | modelProcessor = ModelProcessor(inModel);
89 | jointsToWeld = StdVectorString();
90 | jointsToWeld.add('mtp_r');
91 | jointsToWeld.add('mtp_l');
92 | modelProcessor.append(ModOpReplaceJointsWithWelds(jointsToWeld));
93 | modelProcessor.append(ModOpReplaceMusclesWithDeGrooteFregly2016());
94 | modelProcessor.append(ModOpTendonComplianceDynamicsModeDGF('implicit'));
95 |
96 | % Set the settings for the MocoTrack problem.
97 | track.setModel(modelProcessor);
98 | track.setStatesReference(tableProcessor);
99 | track.set_states_global_tracking_weight(stateTrackingWeight);
100 | track.set_allow_unused_references(true);
101 | track.set_track_reference_position_derivatives(true);
102 | track.set_apply_tracked_states_to_guess(true);
103 | track.set_initial_time(initialTime);
104 | track.set_final_time(finalTime);
105 | track.set_mesh_interval(meshInterval);
106 | study = track.initialize();
107 | problem = study.updProblem();
108 |
109 |
110 | % Goals
111 | % =====
112 |
113 | % Tracking
114 | % --------
115 | % Set different tracking weights for states (weights for states not
116 | % explicitly set here have a default value of 1.0). The values below
117 | % were obtained by trial and error.
118 | stateTrackingGoal = MocoStateTrackingGoal.safeDownCast(problem.updGoal('state_tracking'));
119 | stateTrackingGoal.setWeightForState('/jointset/ground_pelvis/pelvis_tilt/value', 10.0);
120 | stateTrackingGoal.setWeightForState('/jointset/ground_pelvis/pelvis_tilt/speed', 10.0);
121 | stateTrackingGoal.setWeightForState('/jointset/back/lumbar_extension/value', 10.0);
122 | stateTrackingGoal.setWeightForState('/jointset/back/lumbar_extension/speed', 10.0);
123 | stateTrackingGoal.setWeightForState('/jointset/hip_r/hip_flexion_r/value', 10.0);
124 | stateTrackingGoal.setWeightForState('/jointset/hip_r/hip_flexion_r/speed', 10.0);
125 | stateTrackingGoal.setWeightForState('/jointset/hip_l/hip_flexion_l/value', 10.0);
126 | stateTrackingGoal.setWeightForState('/jointset/hip_l/hip_flexion_l/speed', 10.0);
127 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_r/knee_angle_r/value', 10.0);
128 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_r/knee_angle_r/speed', 10.0);
129 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_l/knee_angle_l/value', 10.0);
130 | stateTrackingGoal.setWeightForState('/jointset/walker_knee_l/knee_angle_l/speed', 10.0);
131 | stateTrackingGoal.setWeightForState('/jointset/ankle_r/ankle_angle_r/speed', 10.0);
132 | stateTrackingGoal.setWeightForState('/jointset/ankle_r/ankle_angle_r/value', 10.0);
133 | stateTrackingGoal.setWeightForState('/jointset/ankle_l/ankle_angle_l/speed', 10.0);
134 | stateTrackingGoal.setWeightForState('/jointset/ankle_l/ankle_angle_l/value', 10.0);
135 |
136 | % Periodicity
137 | % -----------
138 | % This goal enforces periodicity (i.e., enforce that certain values are
139 | % the same at the beginning of the simulation as the end of the
140 | % simulation).
141 | periodicityGoal = MocoPeriodicityGoal('periodicityGoal');
142 | problem.addGoal(periodicityGoal);
143 | model = modelProcessor.process();
144 | model.initSystem();
145 |
146 | % Periodic coordinate values (except for pelvis_tx) and speeds.
147 | for i = 1:model.getNumStateVariables()
148 | currentStateName = string(model.getStateVariableNames().getitem(i-1));
149 | if startsWith(currentStateName , '/jointset')
150 | if (~contains(currentStateName, 'pelvis_tx/value'))
151 | periodicityGoal.addStatePair(MocoPeriodicityGoalPair(currentStateName));
152 | end
153 | end
154 | end
155 |
156 | % Periodic muscle activations.
157 | for i = 1:model.getNumStateVariables()
158 | currentStateName = string(model.getStateVariableNames().getitem(i-1));
159 | if endsWith(currentStateName,'/activation')
160 | periodicityGoal.addStatePair(MocoPeriodicityGoalPair(currentStateName));
161 | end
162 | end
163 |
164 | % Periodiic coordinate actuator (lumbar and upper limb) controls.
165 | modelActuators = model.getActuators();
166 | for i = 1:modelActuators.getSize()
167 | thisActuator = modelActuators.get(i-1);
168 | if strcmp(thisActuator.getConcreteClassName, 'CoordinateActuator')
169 | thisPair = MocoPeriodicityGoalPair(thisActuator.getAbsolutePathString());
170 | periodicityGoal.addControlPair(thisPair);
171 | end
172 | end
173 |
174 | % Effort
175 | % ------
176 | % Get a reference to the MocoControlGoal that is added to every MocoTrack
177 | % problem by default and change the weight
178 | effort = MocoControlGoal.safeDownCast(problem.updGoal('control_effort'));
179 | effort.setWeight(controlEffortWeight);
180 |
181 |
182 | % Bounds
183 | % ======
184 | % Set the bounds for all tracked states based on the range of the data.
185 |
186 | % Load the tracked_states file into a table to find the range of the
187 | % coordinate values in the data.
188 | trackedStatesFile = [mocoTrackName, '_tracked_states.sto'];
189 | trackedStatesTable = TimeSeriesTable(trackedStatesFile);
190 | trackedStatesTable.trimFrom(initialTime);
191 | trackedStatesTable.trimTo(finalTime);
192 |
193 | % Use the helper function to constrain the states in the vector based on
194 | % the range of the data in the table.
195 | constrainBoundsAllTrackedStates(problem, trackedStatesTable, ...
196 | fractionExtraBoundSize);
197 |
198 | % Solve the problem
199 | % =================
200 | % Add a term to the solver that minimizes the implicit auxiliary
201 | % derivatives that are used with the implicit mode of muscles.
202 | solver = MocoCasADiSolver.safeDownCast(study.updSolver());
203 | solver.set_minimize_implicit_auxiliary_derivatives(true);
204 | solver.set_implicit_auxiliary_derivatives_weight(1e-6);
205 |
206 | % Solve the problem and save the results.
207 | solution = study.solve();
208 | solution.write(strcat(mocoTrackName, '.sto'));
209 |
210 | % Extract ground reaction forces
211 | % ==============================
212 | % Extract ground reaction forces for downstream analysis. Add the contact
213 | % force elements to vectors, then use Moco's
214 | % createExternalLoadsTableForGait() function.
215 | contact_r = StdVectorString();
216 | contact_l = StdVectorString();
217 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s1_r');
218 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s2_r');
219 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s3_r');
220 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s4_r');
221 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s5_r');
222 | contact_r.add('/forceset/SmoothSphereHalfSpaceForce_s6_r');
223 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s1_l');
224 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s2_l');
225 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s3_l');
226 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s4_l');
227 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s5_l');
228 | contact_l.add('/forceset/SmoothSphereHalfSpaceForce_s6_l');
229 |
230 | model = modelProcessor.process();
231 | externalForcesTableFlat = ...
232 | opensimMoco.createExternalLoadsTableForGait(model, ...
233 | solution.exportToStatesTrajectory(model), ...
234 | contact_r, contact_l);
235 | STOFileAdapter.write(externalForcesTableFlat, strcat(mocoTrackName, '_grfs.sto'));
236 |
237 | %% Helper functions
238 |
239 | % Set each state's bounds based on a fraction (fractionExtraBoundSize) of
240 | % the range of the state's value from the tracked data. The bounds are
241 | % set as the following:
242 | % - Lower bound: (minimum value) - fractionExtraBoundSize * (range of value)
243 | % - Upper bound: (maximum value) + fractionExtraBoundSize * (range of value)
244 | function constrainBoundsAllTrackedStates(problem, trackedStatesTable, ...
245 | fractionExtraBoundSize)
246 |
247 | % Get all of the state names from the table column labels but omit
248 | % the toe joint since it was locked in the model above. Store the
249 | % state names in a vector to pass into the function that constrains the
250 | % bounds.
251 | colLabelsStdVec = trackedStatesTable.getColumnLabels();
252 | colLabelsCell = cell(1, colLabelsStdVec.size());
253 | for i = 1:colLabelsStdVec.size()
254 | thisColLabel = char(colLabelsStdVec.get(i-1));
255 | if ~contains(thisColLabel, 'mtp_angle')
256 | colLabelsCell{i} = thisColLabel;
257 | end
258 | end
259 | colLabelsCell = colLabelsCell(~cellfun('isempty', colLabelsCell));
260 |
261 | for i = 1:numel(colLabelsCell)
262 | thisStatePath = colLabelsCell{i};
263 | stateColumn = ...
264 | trackedStatesTable.getDependentColumn(thisStatePath).getAsMat();
265 | thisColMin = min(stateColumn);
266 | thisColMax = max(stateColumn);
267 | thisColRange = thisColMax - thisColMin;
268 | extraBoundSize = thisColRange * fractionExtraBoundSize;
269 |
270 | thisBounds = [thisColMin - extraBoundSize, thisColMax + extraBoundSize];
271 | problem.setStateInfo(thisStatePath, thisBounds, [], []);
272 | end
273 |
274 | end
275 |
276 | % Set the optimal force of all CoordinateActuator's in the model to a given
277 | % value (optimalForce).
278 | function setCoordinateActuatorsOptimalForce(model, optimalForce)
279 | % import inside of the function needed for downcast step
280 | import org.opensim.modeling.*;
281 |
282 | modelActuators = model.getActuators();
283 | for i = 1:modelActuators.getSize()
284 | thisActuator = modelActuators.get(i-1);
285 | if strcmp(thisActuator.getConcreteClassName(), 'CoordinateActuator')
286 | thisCoordinateActuator = CoordinateActuator.safeDownCast(thisActuator);
287 | thisCoordinateActuator.setOptimalForce(optimalForce);
288 | end
289 | end
290 |
291 | end
--------------------------------------------------------------------------------
/OpenSimPipeline/InverseDynamics/DefaultPosition_rajagopal.mot:
--------------------------------------------------------------------------------
1 | Coordinates
2 | version=1
3 | nRows=10
4 | nColumns=36
5 | inDegrees=no
6 |
7 | "Units are S.I. units (second, meters, Newtons, ...)"
8 | Angles are in degrees.
9 |
10 | endheader
11 | time pelvis_tilt pelvis_list pelvis_rotation pelvis_tx pelvis_ty pelvis_tz hip_flexion_r hip_adduction_r hip_rotation_r knee_angle_r knee_adduction_r ankle_angle_r subtalar_angle_r mtp_angle_r hip_flexion_l hip_adduction_l hip_rotation_l knee_angle_l knee_adduction_l ankle_angle_l subtalar_angle_l mtp_angle_l lumbar_extension lumbar_bending lumbar_rotation arm_flex_l arm_add_l arm_rot_l arm_flex_r arm_add_r arm_rot_r elbow_flex_l elbow_flex_r pro_sup_l pro_sup_r
12 | 0.01 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
13 | 0.02 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
14 | 0.03 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
15 | 0.04 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
16 | 0.05 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
17 | 0.06 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
18 | 0.07 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
19 | 0.08 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
20 | 0.09 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
21 | 0.1 0.05 0.05 0.05 0.05 -0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05 0.05
22 |
--------------------------------------------------------------------------------
/OpenSimPipeline/InverseDynamics/Setup_InverseDynamics.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | ../subject1_scaled.osim
10 |
11 | 0.01 0.1
12 |
13 | Muscles
14 |
15 |
16 |
17 | dummy_motion.mot
18 |
19 | -1
20 |
21 | ID_OSIM.sto
22 |
23 |
24 |
25 | body_forces_at_joints.sto
26 |
27 |
28 |
--------------------------------------------------------------------------------
/OpenSimPipeline/JointReaction/Setup_JointReaction.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | true
6 |
7 | 1
8 |
9 | Inf
10 |
11 | 1
12 |
13 | true
14 |
15 |
16 |
17 | all
18 |
19 | child
20 |
21 | child
22 |
23 |
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenCap Processing
2 |
3 | This repository enables the post-processing of human movement kinematics collected using [OpenCap](opencap.ai). You can run kinematic analyses, download multiple sessions using scripting, and run muscle-driven simulations to estimate kinetics.
4 |
5 | ## Publication
6 | More information is available in our [paper](https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1011462):
7 | Uhlrich SD*, Falisse A*, Kidzinski L*, Ko M, Chaudhari AS, Hicks JL, Delp SL, 2022. OpenCap: Human movement dynamics from smartphone videos. PLoS Comput Biol 19(10): e1011462. https://doi.org/10.1371/journal.pcbi.1011462. *contributed equally
8 | Archived code base corresponding to publication: https://zenodo.org/record/7419973
9 |
10 | ## Install requirements
11 | ### General
12 | 1. Install [Anaconda](https://www.anaconda.com/)
13 | 1. Open Anaconda prompt
14 | 2. Create environment (python 3.11 recommended): `conda create -n opencap-processing python=3.11`
15 | 3. Activate environment: `conda activate opencap-processing`
16 | 4. Install OpenSim: `conda install -c opensim-org opensim=4.5=py311np123`
17 | - Test that OpenSim was successfully installed:
18 | - Start python: `python`
19 | - Import OpenSim: `import opensim`
20 | - If you don't get any error message at this point, you should be good to go.
21 | - You can also double check which version you installed : `opensim.GetVersion()`
22 | - Exit python: `quit()`
23 | - Visit this [webpage](https://opensimconfluence.atlassian.net/wiki/spaces/OpenSim/pages/53116061/Conda+Package) for more details about the OpenSim conda package.
24 | 5. (Optional): Install an IDE such as Spyder: `conda install spyder`
25 | 6. Clone the repository to your machine:
26 | - Navigate to the directory where you want to download the code: eg. `cd Documents`. Make sure there are no spaces in this path.
27 | - Clone the repository: `git clone https://github.com/stanfordnmbl/opencap-processing.git`
28 | - Navigate to the directory: `cd opencap-processing`
29 | 7. Install required packages: `python -m pip install -r requirements.txt`
30 | 8. Run `python createAuthenticationEnvFile.py`
31 | - An environment variable (`.env` file) will be saved after authenticating.
32 |
33 | ### Muscle-driven simulations
34 | 1. **Windows only**: Install [Visual Studio](https://visualstudio.microsoft.com/downloads/)
35 | - The Community variant is sufficient and is free for everyone.
36 | - During the installation, select the *workload Desktop Development with C++*.
37 | - The code was tested with the 2017, 2019, and 2022 Community editions.
38 | 2. **Linux only**: Install OpenBLAS libraries
39 | - `sudo apt-get install libopenblas-base`
40 | 3. **MacOs**: As of now this workflow is supported only on x86_64 architecture. If you intend to use this workflow use x86_64 conda environment even on apple silicon (arm64 is not supported yet).
41 |
42 | ## Examples
43 | - Run `example.py` for examples of how to run kinematic analyses
44 | - Run `example_kinetics.py` for examples of how to generate muscle-driven simulations
45 | - Moco
46 | - The [Moco folder](https://github.com/stanfordnmbl/opencap-processing/tree/main/Moco) contains examples for generating muscle-driven simulations using [OpenSim Moco](https://opensim-org.github.io/opensim-moco-site/).
47 |
48 | ## Download OpenCap data
49 |
50 | ### Using Colab
51 | - Open `batchDownload.ipynb` in Colab and follow the instructions
52 | - You do not need to follow the install requirements above.
53 |
54 | ### Locally
55 | - Follow the install requirements above
56 | - Open `batchDownload.py` and follow the instructions
57 |
--------------------------------------------------------------------------------
/Resources/README.txt:
--------------------------------------------------------------------------------
1 | Thank you for using OpenCap!
2 |
3 | ###################################################################################################
4 | OpenCap is a web-based software package to estimate the kinematics and dynamics of human movement from smartphone videos.
5 | For more information about OpenCap, please visit https://www.opencap.ai/.
6 | You will also find details about the software in our pre-print: https://www.biorxiv.org/content/10.1101/2022.07.07.499061v1.
7 | Please cite this preprint if you use OpenCap in your research.
8 |
9 | ###################################################################################################
10 | For technical questions, please use our forum: https://simtk.org/plugins/phpBB/indexPhpbb.php?group_id=2385&pluginname=phpBB.
11 | For other questions, please use our web form: https://www.opencap.ai/#contact.
12 |
13 | ###################################################################################################
14 | This folder contains data processed with OpenCap.
15 | Here is a brief overview of the folder structure:
16 | - CalibrationImages
17 | -> This folder contains images of the checkerboard used for camera calibration.
18 | - MarkerData
19 | -> This folder contains marker data; each marker data file is named .trc.
20 | -> Markers with names ending with _study are anatomical markers; other markers are video keypoints. Anatomical markers are predicted from video keypoints (see pre-print for details: https://www.biorxiv.org/content/10.1101/2022.07.07.499061v1).
21 | - OpenSimData:
22 | -> This folder contains data processed with OpenSim.
23 | -> For more information about OpenSim, please visit https://simtk-confluence.stanford.edu:8443/display/OpenSim.
24 | - Model
25 | -> The .osim file is the scaled musculoskeletal model.
26 | -> The model can be opened in the OpenSim GUI for visualization.
27 | -> The model can be opened as as text file with any text editor (e.g., Notepad++).
28 | - Kinematics
29 | -> This folder contains motion files with the kinematic data; each motion file is named .mot.
30 | -> The motion files can be loaded, together with the scaled musculoskeletal model, in the OpenSim GUI for visualization.
31 | -> The motion files can be opened as spreadsheets with any spreadsheet software (e.g., Excel).
32 | - Videos
33 | -> This folder contains raw and processed video data.
34 | -> There are as many subfolders as there are cameras used for data collection; the subfolders are named Cam where i is the index of the camera.
35 | - Cam:
36 | - InputMedia
37 | -> This folder contains one subfolder per trial; the subfolders are named as per trial name.
38 | -
39 | -> .mov is the raw recorded video.
40 | -> _sync.mp4 is the processed video synced across cameras.
41 | - OutputPkl
42 | -> This folder contains Python pickle files with outputs from the pose detection algorithm; each file is named _keypoints.pkl.
43 | - cameraIntrinsicsExtrinsics.pickle
44 | -> This Python pickle file contains the camera intrinsic and extrinsic parameters.
45 | - mappingCamDevice.pickle
46 | -> This Python pickle file is used internally to map the recorded videos to the recording cameras.
47 | - sessionMetadata.yaml
48 | -> This file contains metadata about the session.
49 | ###################################################################################################
50 |
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/README.md:
--------------------------------------------------------------------------------
1 | # Muscle-driven simulations with OpenSimAD
2 |
3 | The examples we provide to generate muscle-driven simulations use OpenSimAD, which is a custom version of OpenSim that supports automatic differentiation (AD). AD is an alternative to finite differences to compute derivatives that is faster in most cases. You can find more details about OpenSimAD and some benchmarking against finite differences in [this publication](https://journals.plos.org/plosone/article/comments?id=10.1371/journal.pone.0217730). Please keep in mind that OpenSimAD does not support all features of OpenSim, you should therefore carefully verify what you are doing should you diverge from the provided examples (eg, if you use a different musculoskeletal model). We will contribute examples to generate muscle-driven simulations using Moco in the near future, which will make it easier to use different OpenSim models.
4 |
5 | OpenSimAD requires compiling C++ and C code. Everything is automated, but please follow the [specific install requirements](https://github.com/stanfordnmbl/opencap-processing#install-requirements-to-run-muscle-driven-simulations) to make sure you have everything you need (CMake and compiler).
6 |
7 | ### Getting started running muscle-driven simulations
8 | We recommend starting with `example_kinetics.py` to see how to run dynamic simulations using this repository. The below documentation provides further details about the simulations.
9 |
10 | ### Overview of the pipeline for muscle-driven simulations
11 | 1. **Process inputs**
12 | - [Download the model and the motion file](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/utilsOpenSimAD.py#L1912) with the coordinate values estimated from videos.
13 | - [Adjust the wrapping surfaces](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/utilsOpenSimAD.py#L1917) of the model to enforce meaningful moment arms (ie, address known bug of wrapping surfaces).
14 | - [Add contact spheres](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/utilsOpenSimAD.py#L1919) to the musculoskeletal model to model foot-ground interactions.
15 | - [Generate differentiable external function](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/utilsOpenSimAD.py#L1921) to leverage AD when solving the optimal control problem.
16 | - More details about this process in [this publication](https://journals.plos.org/plosone/article/comments?id=10.1371/journal.pone.0217730) and [this repository](https://github.com/antoinefalisse/opensimAD).
17 | 2. **Fit polynomials to approximate muscle-tendon lenghts and velocities, and moment arms.**
18 | - We use polynomial approximations of coordinates values to estimate muscle-tendon lenghts and velocities, and moment arms. We [fit the polynomial coefficients](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/mainOpenSimAD.py#L541) before solving the optimal control problem. Using polynomial approximations speeds up evaluations of muscle-tendon lenghts and velocities, and moment arms.
19 | 3. **Solve optimal control / trajectory optimization problem**
20 | - We generate [muscle-driven simulations](https://github.com/stanfordnmbl/opencap-processing/blob/main/UtilsDynamicSimulations/OpenSimAD/mainOpenSimAD.py#L999) that track joint kinematics. The general idea is to solve for the model controls that will drive the musculoskeletal model to closely track the measured kinematics while satisfying the dynamic equations describing muscle and skeletal dynamics and minimizing muscle effort. We use direct collocation methods to solve this problem, and leverage AD through [CasADi](https://web.casadi.org/).
21 | 4. **Process results**
22 | - From the simulations, we can extract dynamic variables like muscle forces, joint moments, ground reaction forces, or joint contact forces.
23 |
24 | ### Overview outputs
25 | - If your problem converges, you should get a few files under OpenSimData/Dynamics:
26 | - forces_.mot
27 | - Muscle forces and non muscle-driven joint torques (eg, reserve actuators).
28 | - GRF_resultant_.mot
29 | - Resultant ground reaction forces and moments.
30 | - GRF_.mot
31 | - Ground reaction forces and moments (per contact sphere).
32 | - kinematics_activations_.mot
33 | - Joint kinematics and muscle activations.
34 | - kinetics_.mot
35 | - Net joint moments.
36 | - optimaltrajectories.npy
37 | - Dictionary with compiled results:
38 | - time: discretized time vector.
39 | - coordinate_values_toTrack: reference coordinate values estimated from videos.
40 | - coordinate_values: coordinate values resulting from the dynamic simulation.
41 | - coordinate_speeds_toTrack: reference coordinate speeds estimated from videos.
42 | - coordinate_speeds: coordinate speeds resulting from the dynamic simulation.
43 | - coordinate_accelerations_toTrack: reference coordinate accelerations estimated from videos.
44 | - coordinate_accelerations: coordinate accelerations resulting from the dynamic simulation.
45 | - torques: joint torques/moments from the dynamic simulation.
46 | - torques_BWht: joint torques/moments normalized by body weight times height from the dynamic simulation.
47 | - GRF: resultant ground reaction forces from the dynamic simulation.
48 | - GRF_BW: resultant ground reaction forces normalized by body weight from the dynamic simulation.
49 | - GRM: resultant ground reaction moments from the dynamic simulation, expressed with respect to global reference frame.
50 | - GRM_BWht: resultant ground reaction moments normalized by body weight times height from the dynamic simulation.
51 | - muscle_activations: muscle activations from the dynamic simulation.
52 | - passive_muscle_torques: passive torque contribution of the muscles from the dynamic simulation.
53 | - active_muscle_torques: active torque contribution of the muscles from the dynamic simulation.
54 | - passive_limit_torques: torque contributions from limit torques from the dynamic simulation.
55 | - KAM: knee adduction moments from the dynamic simulation.
56 | - KAM_BWht: knee adduction moments normalized by body weight times height from the dynamic simulation.
57 | - MCF: medial knee contact forces from the dynamic simulation.
58 | - MCF_BW: medial knee contact forces normalized by body weight from the dynamic simulation.
59 | - coordinates: coordinate names.
60 | - rotationalCoordinates: rotational coordinate names.
61 | - GRF_labels: ground reaction force names.
62 | - muscles: muscle names.
63 | - muscle_driven_joints: muscle-driven coordinate names.
64 | - limit_torques_joints : names of coordinates with limit torques.
65 | - KAM_labels: labels of knee adduction moments.
66 | - MCF_labels: labels of medial knee contact fprces.
67 | - iter_count: number of iterations the problem took to converge.
68 |
69 | ### Overview files
70 | - `boundsOpenSimAD.py`: script describing the bounds of the problem variables.
71 | - `functionCasADiOpenSimAD.py`: various helper CasADi functions.
72 | - `initialGuessOpenSimAD.py`: script describing the initial guess of the problem variables.
73 | - `mainOpenSimAD.py`: main script formulating and solving the problem.
74 | - `muscleDataOpenSimAD.py`: various helper functions related to muscle models.
75 | - `muscleModelOpenSimAD.py`: implementation of the [DeGrooteFregly](https://pubmed.ncbi.nlm.nih.gov/27001399/) muscle model.
76 | - `plotsOpenSimAD.py`: helper plots for intermediate visualization.
77 | - `polynomialsOpenSimAD.py`: script to fit polynomial coefficients.
78 | - `settingsOpenSimAD.py`: settings for the problem with pre-defined settings for simulating different activities.
79 | - `utilsOpenSimAD.py`: various utilities for OpenSimAD.
80 |
81 | ### Food for thought / Tips & Tricks
82 |
83 | Dynamic simulations of human movement require solving complex optimal control problems. **It is a tedious task with no guarantee of success.** Even if the problem converges (*optimal solution found*), you should always verify that the results are biomechanically meaningful. It is possible that the problem satisfied all constraints but did not converge to the expected solution. You might want to play with the settings (eg, weights of the different terms in the cost function), constraints, and cost function terms to generate simulations that make sense for the particular activity you are interested in. We have gathered some [tips and tricks](https://docs.google.com/document/d/1zgF9PqOaSZHma3vdQnccHz6mc7Fv6AG3fHLNLh4TQuA/edit) in this document.
84 |
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/buildExpressionGraph/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required (VERSION 3.10)
2 | set (CMAKE_CXX_STANDARD 11)
3 |
4 | set(TARGET_NAME
5 | ""
6 | CACHE
7 | STRING
8 | "Target name")
9 |
10 | set(TEST_TARGET ${TARGET_NAME})
11 | project(${TEST_TARGET})
12 |
13 | set(CPP_DIR "Path to external function directory" CACHE PATH "Location of directory")
14 | add_executable(${TARGET_NAME} ${CPP_DIR}/${TARGET_NAME}.cpp)
15 |
16 | set(SDK_DIR "Path to OpenSim include directory" CACHE PATH "Location of directory")
17 |
18 | target_compile_definitions(${TARGET_NAME} PUBLIC SimTK_REAL_IS_ADOUBLE)
19 |
20 | if(WIN32)
21 | target_include_directories(${TARGET_NAME} PUBLIC ${SDK_DIR}/include ${SDK_DIR}/Simbody/include)
22 | target_link_libraries(${TARGET_NAME} PUBLIC ${SDK_DIR}/lib/osimCommon_recorder.lib ${SDK_DIR}/lib/osimSimulation_recorder.lib ${SDK_DIR}/Simbody/lib/SimTKcommon_recorder.lib ${SDK_DIR}/Simbody/lib/SimTKmath_recorder.lib ${SDK_DIR}/Simbody/lib/SimTKsimbody_recorder.lib)
23 | else()
24 | target_include_directories(${TARGET_NAME} PUBLIC ${SDK_DIR}/include ${SDK_DIR}/include/simbody)
25 | if(APPLE)
26 | target_link_libraries(${TARGET_NAME} PUBLIC ${SDK_DIR}/lib/libosimCommon_recorder.dylib ${SDK_DIR}/lib/libosimSimulation_recorder.dylib ${SDK_DIR}/lib/libSimTKcommon_recorder.dylib ${SDK_DIR}/lib/libSimTKmath_recorder.dylib ${SDK_DIR}/lib/libSimTKsimbody_recorder.dylib)
27 | elseif(UNIX)
28 | target_link_libraries(${TARGET_NAME} PUBLIC ${SDK_DIR}/lib/libosimCommon_recorder.so ${SDK_DIR}/lib/libosimSimulation_recorder.so ${SDK_DIR}/lib/libSimTKcommon_recorder.so ${SDK_DIR}/lib/libSimTKmath_recorder.so ${SDK_DIR}/lib/libSimTKsimbody_recorder.so)
29 | endif()
30 |
31 | add_custom_command(TARGET ${TARGET_NAME}
32 | POST_BUILD
33 | COMMAND ${TARGET_NAME}
34 | )
35 | endif()
36 |
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/buildExternalFunction/CMakeLists.txt:
--------------------------------------------------------------------------------
1 | cmake_minimum_required (VERSION 3.10)
2 | set (CMAKE_CXX_STANDARD 11)
3 |
4 | set(TARGET_NAME
5 | ""
6 | CACHE
7 | STRING
8 | "Target name")
9 |
10 | set(TEST_TARGET ${TARGET_NAME})
11 | project(${TEST_TARGET})
12 |
13 | add_library(${TEST_TARGET} SHARED foo_jac.c)
14 | install(TARGETS ${TEST_TARGET}
15 | PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE
16 | GROUP_READ GROUP_WRITE GROUP_EXECUTE
17 | WORLD_READ WORLD_EXECUTE
18 | LIBRARY DESTINATION lib
19 | ARCHIVE DESTINATION lib
20 | RUNTIME DESTINATION bin
21 | )
22 |
23 | set(INSTALL_DIR "Path to install directory" CACHE PATH "Location of directory")
24 | # get_filename_component(BASE_DIR ${CMAKE_CURRENT_BINARY_DIR} DIRECTORY)
25 | # Default install prefix libraries.
26 | set(CMAKE_INSTALL_PREFIX
27 | ${INSTALL_DIR}
28 | CACHE
29 | PATH
30 | "Default CMAKE_INSTALL_PREFIX." FORCE)
31 |
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/functionCasADiOpenSimAD.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: functionCasADiOpenSimAD.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2022 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse, Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | This script defines several CasADi functions for use when setting up
19 | the optimal control problem.
20 | '''
21 |
22 | import casadi as ca
23 | import numpy as np
24 |
25 | # %% CasADi function to approximate muscle-tendon lenghts, velocities,
26 | # and moment arms based on polynomial approximations of joint positions and
27 | # velocities.
28 | def polynomialApproximation(musclesPolynomials, polynomialData, NPolynomial):
29 |
30 | from polynomialsOpenSimAD import polynomials
31 |
32 | # Function variables.
33 | qin = ca.SX.sym('qin', 1, NPolynomial)
34 | qdotin = ca.SX.sym('qdotin', 1, NPolynomial)
35 |
36 | lMT = ca.SX(len(musclesPolynomials), 1)
37 | vMT = ca.SX(len(musclesPolynomials), 1)
38 | dM = ca.SX(len(musclesPolynomials), NPolynomial)
39 | for count, musclePolynomials in enumerate(musclesPolynomials):
40 | coefficients = polynomialData[musclePolynomials]['coefficients']
41 | dimension = polynomialData[musclePolynomials]['dimension']
42 | order = polynomialData[musclePolynomials]['order']
43 | spanning = polynomialData[musclePolynomials]['spanning']
44 | polynomial = polynomials(coefficients, dimension, order)
45 | idxSpanning = [i for i, e in enumerate(spanning) if e == 1]
46 | lMT[count] = polynomial.calcValue(qin[0, idxSpanning])
47 | dM[count, :] = 0
48 | vMT[count] = 0
49 | for i in range(len(idxSpanning)):
50 | dM[count, idxSpanning[i]] = - polynomial.calcDerivative(
51 | qin[0, idxSpanning], i)
52 | vMT[count] += (-dM[count, idxSpanning[i]] *
53 | qdotin[0, idxSpanning[i]])
54 | f_polynomial = ca.Function('f_polynomial',[qin, qdotin],[lMT, vMT, dM])
55 |
56 | return f_polynomial
57 |
58 | # %% CasADi function to describe the Hill equilibrium based on the
59 | # DeGrooteFregly2016MuscleModel muscle model.
60 | def hillEquilibrium(mtParameters, tendonCompliance, tendonShift,
61 | specificTension, ignorePassiveFiberForce=False):
62 |
63 | NMuscles = mtParameters.shape[1]
64 |
65 | # Function variables
66 | activation = ca.SX.sym('activation', NMuscles)
67 | mtLength = ca.SX.sym('mtLength', NMuscles)
68 | mtVelocity = ca.SX.sym('mtVelocity', NMuscles)
69 | normTendonForce = ca.SX.sym('normTendonForce', NMuscles)
70 | normTendonForceDT = ca.SX.sym('normTendonForceDT', NMuscles)
71 |
72 | hillEquilibrium = ca.SX(NMuscles, 1)
73 | tendonForce = ca.SX(NMuscles, 1)
74 | activeFiberForce = ca.SX(NMuscles, 1)
75 | normActiveFiberLengthForce = ca.SX(NMuscles, 1)
76 | passiveFiberForce = ca.SX(NMuscles, 1)
77 | normFiberLength = ca.SX(NMuscles, 1)
78 | fiberVelocity = ca.SX(NMuscles, 1)
79 | activeFiberForcePen = ca.SX(NMuscles, 1)
80 | passiveFiberForcePen = ca.SX(NMuscles, 1)
81 |
82 | from muscleModelOpenSimAD import DeGrooteFregly2016MuscleModel
83 | for m in range(NMuscles):
84 | muscle = DeGrooteFregly2016MuscleModel(
85 | mtParameters[:, m], activation[m], mtLength[m],
86 | mtVelocity[m], normTendonForce[m],
87 | normTendonForceDT[m], tendonCompliance[:, m],
88 | tendonShift[:, m], specificTension[:, m],
89 | ignorePassiveFiberForce=ignorePassiveFiberForce)
90 | hillEquilibrium[m] = muscle.deriveHillEquilibrium()
91 | tendonForce[m] = muscle.getTendonForce()
92 | activeFiberForce[m] = muscle.getActiveFiberForce()[0]
93 | passiveFiberForce[m] = muscle.getPassiveFiberForce()[0]
94 | normActiveFiberLengthForce[m] = muscle.getActiveFiberLengthForce()
95 | normFiberLength[m] = muscle.getFiberLength()[1]
96 | fiberVelocity[m] = muscle.getFiberVelocity()[0]
97 | activeFiberForcePen[m] = muscle.getActiveFiberForce()[2]
98 | passiveFiberForcePen[m] = muscle.getPassiveFiberForce()[2]
99 | f_hillEquilibrium = ca.Function(
100 | 'f_hillEquilibrium', [activation, mtLength, mtVelocity,
101 | normTendonForce, normTendonForceDT],
102 | [hillEquilibrium, tendonForce, activeFiberForce, passiveFiberForce,
103 | normActiveFiberLengthForce, normFiberLength, fiberVelocity,
104 | activeFiberForcePen, passiveFiberForcePen])
105 |
106 | return f_hillEquilibrium
107 |
108 | # %% CasADi function to describe the dynamics of the coordinate actuators.
109 | def coordinateActuatorDynamics(nJoints):
110 |
111 | # Function variables
112 | eArm = ca.SX.sym('eArm',nJoints)
113 | aArm = ca.SX.sym('aArm',nJoints)
114 |
115 | t = 0.035 # time constant
116 | aArmDt = (eArm - aArm) / t
117 | f_armActivationDynamics = ca.Function('f_armActivationDynamics',
118 | [eArm, aArm], [aArmDt])
119 |
120 | return f_armActivationDynamics
121 |
122 | # %% CasADi function to compute passive limit joint torques.
123 | def limitPassiveTorque(k, theta, d):
124 |
125 | # Function variables
126 | Q = ca.SX.sym('Q', 1)
127 | Qdot = ca.SX.sym('Qdot', 1)
128 |
129 | passiveJointTorque = (k[0] * np.exp(k[1] * (Q - theta[1])) + k[2] *
130 | np.exp(k[3] * (Q - theta[0])) - d * Qdot)
131 | f_limitPassiveTorque = ca.Function('f_limitPassiveTorque', [Q, Qdot],
132 | [passiveJointTorque])
133 |
134 | return f_limitPassiveTorque
135 |
136 | # %% CasADi function to compute linear passive joint torques given stiffness
137 | # and damping.
138 | def linarPassiveTorque(k, d):
139 |
140 | # Function variables
141 | Q = ca.SX.sym('Q', 1)
142 | Qdot = ca.SX.sym('Qdot', 1)
143 |
144 | passiveJointTorque = -k * Q - d * Qdot
145 | f_linarPassiveTorque = ca.Function('f_linarPassiveTorque', [Q, Qdot],
146 | [passiveJointTorque])
147 |
148 | return f_linarPassiveTorque
149 |
150 | # %% CasADi function to compute the normalized sum of the weighted elements in
151 | # a vector elevated to a given power.
152 | def normSumWeightedPow(N, exp):
153 |
154 | # Function variables
155 | x = ca.SX.sym('x', N, 1)
156 | w = ca.SX.sym('w', N, 1)
157 |
158 | nsp = ca.sum1(w * (x**exp))
159 | nsp = nsp / N
160 | f_normSumPow = ca.Function('f_normSumWeightedPow', [x, w], [nsp])
161 |
162 | return f_normSumPow
163 |
164 | # %% CasADi function to compute the normalized sum of the squared elements in a
165 | # vector.
166 | def normSumSqr(N):
167 |
168 | # Function variables
169 | x = ca.SX.sym('x', N, 1)
170 |
171 | ss = ca.sumsqr(x) / N
172 | f_normSumSqr = ca.Function('f_normSumSqr', [x], [ss])
173 |
174 | return f_normSumSqr
175 |
176 | # %% CasADi function to compute difference in torques (inverse dynamics vs
177 | # muscle and passive contributions).
178 | def diffTorques():
179 |
180 | # Function variables
181 | jointTorque = ca.SX.sym('x', 1)
182 | muscleTorque = ca.SX.sym('x', 1)
183 | passiveTorque = ca.SX.sym('x', 1)
184 |
185 | diffTorque = jointTorque - (muscleTorque + passiveTorque)
186 | f_diffTorques = ca.Function(
187 | 'f_diffTorques', [jointTorque, muscleTorque, passiveTorque],
188 | [diffTorque])
189 |
190 | return f_diffTorques
191 |
192 | # %% CasADi function to compute the normalized sum of the weighted squared
193 | # difference between two vectors.
194 | def normSumWeightedSqrDiff(dim):
195 |
196 | # Function variables
197 | x = ca.SX.sym('x', dim, 1)
198 | x_ref = ca.SX.sym('x_ref', dim, 1)
199 | w = ca.SX.sym('w', dim, 1)
200 |
201 | nSD = ca.sum1(w * (x-x_ref)**2)
202 | nSD = nSD / dim
203 | f_normSumSqrDiff = ca.Function('f_normSumSqrDiff', [x, x_ref, w], [nSD])
204 |
205 | return f_normSumSqrDiff
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/initialGuessOpenSimAD.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: initialGuessOpenSimAD.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2022 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse, Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | import pandas as pd
20 | import scipy.interpolate as interpolate
21 |
22 | # %% Data-driven initial guess.
23 | class dataDrivenGuess_tracking:
24 | def __init__(self, Qs, N, d, joints, muscles):
25 |
26 | self.Qs = Qs
27 | self.N = N
28 | self.d = d
29 | self.joints = joints
30 | self.muscles = muscles
31 |
32 | def splineQs(self):
33 |
34 | self.Qs_spline = self.Qs.copy()
35 | self.Qdots_spline = self.Qs.copy()
36 | self.Qdotdots_spline = self.Qs.copy()
37 |
38 | for joint in self.joints:
39 | spline = interpolate.InterpolatedUnivariateSpline(
40 | self.Qs['time'], self.Qs[joint], k=3)
41 | self.Qs_spline[joint] = spline(self.Qs['time'])
42 | splineD1 = spline.derivative(n=1)
43 | self.Qdots_spline[joint] = splineD1(self.Qs['time'])
44 | splineD2 = spline.derivative(n=2)
45 | self.Qdotdots_spline[joint] = splineD2(self.Qs['time'])
46 |
47 | # Mesh points
48 | def getGuessPosition(self, scaling):
49 | self.splineQs()
50 | self.guessPosition = pd.DataFrame()
51 | g = [0] * (self.N)
52 | for count, joint in enumerate(self.joints):
53 | if joint == 'mtp_angle_l' or joint == 'mtp_angle_r':
54 | self.guessPosition.insert(count, joint, g)
55 |
56 | else:
57 | self.guessPosition.insert(count, joint,
58 | self.Qs_spline[joint] /
59 | scaling.iloc[0][joint])
60 |
61 | return self.guessPosition
62 |
63 | def getGuessVelocity(self, scaling):
64 | self.splineQs()
65 | self.guessVelocity = pd.DataFrame()
66 | g = [0] * (self.N)
67 | for count, joint in enumerate(self.joints):
68 | if joint == 'mtp_angle_l' or joint == 'mtp_angle_r':
69 | self.guessVelocity.insert(count, joint, g)
70 | else:
71 | self.guessVelocity.insert(count, joint,
72 | self.Qdots_spline[joint] /
73 | scaling.iloc[0][joint])
74 | return self.guessVelocity
75 |
76 | def getGuessAcceleration(self, scaling, zeroAcceleration=False):
77 | self.splineQs()
78 | self.guessAcceleration = pd.DataFrame()
79 | g = [0] * self.N
80 | g1 = [0] * (self.N)
81 | for count, joint in enumerate(self.joints):
82 | if zeroAcceleration:
83 | self.guessAcceleration.insert(
84 | count, joint, g / scaling.iloc[0][joint])
85 | else:
86 | if joint == 'mtp_angle_l' or joint == 'mtp_angle_r':
87 | self.guessAcceleration.insert(count, joint, g1)
88 | else:
89 | self.guessAcceleration.insert(
90 | count, joint, self.Qdotdots_spline[joint] /
91 | scaling.iloc[0][joint])
92 |
93 | return self.guessAcceleration
94 |
95 | def getGuessActivation(self, scaling):
96 | g = [0.1] * (self.N + 1)
97 | self.guessActivation = pd.DataFrame()
98 | for count, muscle in enumerate(self.muscles):
99 | self.guessActivation.insert(count, muscle,
100 | g / scaling.iloc[0][muscle])
101 |
102 | return self.guessActivation
103 |
104 | def getGuessActivationDerivative(self, scaling):
105 | g = [0.01] * self.N
106 | guessActivationDerivative = pd.DataFrame()
107 | for count, muscle in enumerate(self.muscles):
108 | guessActivationDerivative.insert(count, muscle,
109 | g / scaling.iloc[0][muscle])
110 |
111 | return guessActivationDerivative
112 |
113 | def getGuessForce(self, scaling):
114 | g = [0.1] * (self.N + 1)
115 | self.guessForce = pd.DataFrame()
116 | for count, muscle in enumerate(self.muscles):
117 | self.guessForce.insert(count, muscle, g / scaling.iloc[0][muscle])
118 |
119 | return self.guessForce
120 |
121 | def getGuessForceDerivative(self, scaling):
122 | g = [0.01] * self.N
123 | self.guessForceDerivative = pd.DataFrame()
124 | for count, muscle in enumerate(self.muscles):
125 | self.guessForceDerivative.insert(count, muscle,
126 | g / scaling.iloc[0][muscle])
127 |
128 | return self.guessForceDerivative
129 |
130 | def getGuessTorqueActuatorActivation(self, torqueActuatorJoints):
131 | g = [0.1] * (self.N + 1)
132 | self.guessTorqueActuatorActivation = pd.DataFrame()
133 | for count, torqueActuatorJoint in enumerate(torqueActuatorJoints):
134 | self.guessTorqueActuatorActivation.insert(
135 | count, torqueActuatorJoint, g)
136 |
137 | return self.guessTorqueActuatorActivation
138 |
139 | def getGuessTorqueActuatorExcitation(self, torqueActuatorJoints):
140 | g = [0.1] * self.N
141 | guessTorqueActuatorExcitation = pd.DataFrame()
142 | for count, torqueActuatorJoint in enumerate(torqueActuatorJoints):
143 | guessTorqueActuatorExcitation.insert(count, torqueActuatorJoint, g)
144 |
145 | return guessTorqueActuatorExcitation
146 |
147 | def getGuessReserveActuators(self, joint):
148 | g = [0] * self.N
149 | guessReserveActuators = pd.DataFrame(g, columns=[joint])
150 |
151 | return guessReserveActuators
152 |
153 | # Collocation points
154 | def getGuessActivationCol(self):
155 | temp = []
156 | for k in range(self.N):
157 | for c in range(self.d):
158 | temp.append(self.guessActivation.iloc[k])
159 | guessActivationCol = pd.DataFrame.from_records(temp)
160 |
161 | return guessActivationCol
162 |
163 | def getGuessForceCol(self):
164 | temp = []
165 | for k in range(self.N):
166 | for c in range(self.d):
167 | temp.append(self.guessForce.iloc[k])
168 | guessForceCol = pd.DataFrame.from_records(temp)
169 |
170 | return guessForceCol
171 |
172 | def getGuessForceDerivativeCol(self):
173 | temp = []
174 | for k in range(self.N):
175 | for c in range(self.d):
176 | temp.append(self.guessForceDerivative.iloc[k])
177 | guessForceDerivativeCol = pd.DataFrame.from_records(temp)
178 |
179 | return guessForceDerivativeCol
180 |
181 | def getGuessTorqueActuatorActivationCol(self, torqueActuatorJoints):
182 | temp = []
183 | for k in range(self.N):
184 | for c in range(self.d):
185 | temp.append(self.guessTorqueActuatorActivation.iloc[k])
186 | guessTorqueActuatorActivationCol = pd.DataFrame.from_records(temp)
187 |
188 | return guessTorqueActuatorActivationCol
189 |
190 | def getGuessPositionCol(self):
191 | temp = []
192 | for k in range(self.N):
193 | for c in range(self.d):
194 | temp.append(self.guessPosition.iloc[k])
195 | guessPositionCol = pd.DataFrame.from_records(temp)
196 |
197 | return guessPositionCol
198 |
199 | def getGuessVelocityCol(self):
200 | temp = []
201 | for k in range(self.N):
202 | for c in range(self.d):
203 | temp.append(self.guessVelocity.iloc[k])
204 | guessVelocityCol = pd.DataFrame.from_records(temp)
205 |
206 | return guessVelocityCol
207 |
208 | def getGuessAccelerationCol(self):
209 | temp = []
210 | guessAccelerationCol = pd.DataFrame(columns=self.joints)
211 | for k in range(self.N):
212 | for c in range(self.d):
213 | temp.append(self.guessAcceleration.iloc[k])
214 | guessAccelerationCol = pd.DataFrame.from_records(temp)
215 |
216 | return guessAccelerationCol
217 |
218 | def getGuessOffset(self, scaling):
219 |
220 | guessOffset = 0.2 / scaling
221 |
222 | return guessOffset
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/muscleModelOpenSimAD.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: muscleModelOpenSimAD.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2022 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse, Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | import numpy as np
20 |
21 | # %% DeGrooteFregly2016MuscleModel
22 | # This class implements the muscle model described in De Groote et al. (2016).
23 | # https://link.springer.com/article/10.1007%2Fs10439-016-1591-9
24 | class DeGrooteFregly2016MuscleModel:
25 |
26 | def __init__(self, mtParameters, activation, mtLength, mtVelocity,
27 | normTendonForce, normTendonForceDT, tendonCompliance,
28 | tendonShift, specificTension, ignorePassiveFiberForce=False):
29 | self.mtParameters = mtParameters
30 |
31 | self.maximalIsometricForce = mtParameters[0]
32 | self.optimalFiberLength = mtParameters[1]
33 | self.tendonSlackLength = mtParameters[2]
34 | self.optimalPennationAngle = mtParameters[3]
35 | self.maximalFiberVelocity = mtParameters[4]
36 |
37 | self.activation = activation
38 | self.mtLength = mtLength
39 | self.mtVelocity = mtVelocity
40 | self.normTendonForce = normTendonForce
41 | self.normTendonForceDT = normTendonForceDT
42 | self.tendonCompliance = tendonCompliance
43 | self.tendonShift = tendonShift
44 | self.specificTension = specificTension
45 | self.paramFLa = np.array([0.814483478343008, 1.05503342897057,
46 | 0.162384573599574, 0.0633034484654646,
47 | 0.433004984392647, 0.716775413397760,
48 | -0.0299471169706956, 0.200356847296188])
49 | self.paramFLp = np.array([-0.995172050006169, 53.5981500331442])
50 | self.paramFV = np.array([-0.318323436899127, -8.14915604347525,
51 | -0.374121508647863, 0.885644059915004])
52 |
53 | self.ignorePassiveFiberForce = ignorePassiveFiberForce
54 |
55 | def getMuscleVolume(self):
56 | self.muscleVolume = np.multiply(self.maximalIsometricForce,
57 | self.optimalFiberLength)
58 | return self.muscleVolume
59 |
60 |
61 | def getMuscleMass(self):
62 | muscleMass = np.divide(np.multiply(self.muscleVolume, 1059.7),
63 | np.multiply(self.specificTension, 1e6))
64 |
65 | return muscleMass
66 |
67 | def getTendonForce(self):
68 | tendonForce = np.multiply(self.normTendonForce,
69 | self.maximalIsometricForce)
70 |
71 | return tendonForce
72 |
73 | def getTendonLength(self):
74 | # Tendon force-length relationship.
75 | self.normTendonLength = np.divide(
76 | np.log(5*(self.normTendonForce + 0.25 - self.tendonShift)),
77 | self.tendonCompliance) + 0.995
78 | self.tendonLength = np.multiply(self.tendonSlackLength,
79 | self.normTendonLength)
80 |
81 | return self.tendonLength, self.normTendonLength
82 |
83 | def getFiberLength(self):
84 | # Hill-type muscle model: geometric relationships.
85 | self.getTendonLength()
86 | w = np.multiply(self.optimalFiberLength,
87 | np.sin(self.optimalPennationAngle))
88 | self.fiberLength = np.sqrt(
89 | (self.mtLength - self.tendonLength)**2 + w**2)
90 | self.normFiberLength = np.divide(self.fiberLength,
91 | self.optimalFiberLength)
92 |
93 | return self.fiberLength, self.normFiberLength
94 |
95 | def getFiberVelocity(self):
96 | # Hill-type muscle model: geometric relationships.
97 | self.getFiberLength()
98 | tendonVelocity = np.divide(np.multiply(self.tendonSlackLength,
99 | self.normTendonForceDT),
100 | 0.2 * self.tendonCompliance * np.exp(self.tendonCompliance *
101 | (self.normTendonLength -
102 | 0.995)))
103 | self.cosPennationAngle = np.divide((self.mtLength - self.tendonLength),
104 | self.fiberLength)
105 | self.fiberVelocity = np.multiply((self.mtVelocity - tendonVelocity),
106 | self.cosPennationAngle)
107 | self.normFiberVelocity = np.divide(self.fiberVelocity,
108 | self.maximalFiberVelocity)
109 |
110 | return self.fiberVelocity, self.normFiberVelocity
111 |
112 | def getActiveFiberLengthForce(self):
113 | self.getFiberLength()
114 | # Active muscle force-length relationship.
115 | b11 = self.paramFLa[0]
116 | b21 = self.paramFLa[1]
117 | b31 = self.paramFLa[2]
118 | b41 = self.paramFLa[3]
119 | b12 = self.paramFLa[4]
120 | b22 = self.paramFLa[5]
121 | b32 = self.paramFLa[6]
122 | b42 = self.paramFLa[7]
123 | b13 = 0.1
124 | b23 = 1
125 | b33 = 0.5 * np.sqrt(0.5)
126 | b43 = 0
127 | num3 = self.normFiberLength - b23
128 | den3 = b33 + b43 * self.normFiberLength
129 | FMtilde3 = b13 * np.exp(-0.5 * (np.divide(num3**2, den3**2)))
130 | num1 = self.normFiberLength - b21
131 | den1 = b31 + b41 * self.normFiberLength
132 | FMtilde1 = b11 * np.exp(-0.5 * (np.divide(num1**2, den1**2)))
133 | num2 = self.normFiberLength - b22
134 | den2 = b32 + b42 * self.normFiberLength
135 | FMtilde2 = b12 * np.exp(-0.5 * (np.divide(num2**2, den2**2)))
136 | self.normActiveFiberLengthForce = FMtilde1 + FMtilde2 + FMtilde3
137 |
138 | return self.normActiveFiberLengthForce
139 |
140 | def getActiveFiberVelocityForce(self):
141 | self.getFiberVelocity()
142 | # Active muscle force-velocity relationship.
143 | e1 = self.paramFV[0]
144 | e2 = self.paramFV[1]
145 | e3 = self.paramFV[2]
146 | e4 = self.paramFV[3]
147 |
148 | self.normActiveFiberVelocityForce = e1 * np.log(
149 | (e2 * self.normFiberVelocity + e3)
150 | + np.sqrt((e2 * self.normFiberVelocity + e3)**2 + 1)) + e4
151 |
152 | def getActiveFiberForce(self):
153 | d = 0.01
154 | self.getActiveFiberLengthForce()
155 | self.getActiveFiberVelocityForce()
156 |
157 | self.normActiveFiberForce = ((self.activation *
158 | self.normActiveFiberLengthForce *
159 | self.normActiveFiberVelocityForce) +
160 | d * self.normFiberVelocity)
161 |
162 | activeFiberForce = (self.normActiveFiberForce *
163 | self.maximalIsometricForce)
164 |
165 | activeFiberForcePen = np.multiply(activeFiberForce,
166 | self.cosPennationAngle)
167 |
168 | return (activeFiberForce, self.normActiveFiberForce,
169 | activeFiberForcePen)
170 |
171 | def getPassiveFiberForce(self):
172 |
173 | if self.ignorePassiveFiberForce:
174 | passiveFiberForce = 0
175 | self.normPassiveFiberForce = 0
176 | passiveFiberForcePen = 0
177 |
178 | else:
179 | paramFLp = self.paramFLp
180 | self.getFiberLength()
181 |
182 | # Passive muscle force-length relationship.
183 | e0 = 0.6
184 | kpe = 4
185 | t5 = np.exp(kpe * (self.normFiberLength - 1) / e0)
186 | self.normPassiveFiberForce = np.divide(((t5 - 1) - paramFLp[0]),
187 | paramFLp[1])
188 |
189 | passiveFiberForce = (self.normPassiveFiberForce *
190 | self.maximalIsometricForce)
191 |
192 | passiveFiberForcePen = np.multiply(passiveFiberForce,
193 | self.cosPennationAngle)
194 |
195 | return (passiveFiberForce, self.normPassiveFiberForce,
196 | passiveFiberForcePen)
197 |
198 | def deriveHillEquilibrium(self):
199 | self.getActiveFiberForce()
200 | self.getPassiveFiberForce()
201 |
202 | hillEquilibrium = ((np.multiply(self.normActiveFiberForce +
203 | self.normPassiveFiberForce,
204 | self.cosPennationAngle)) -
205 | self.normTendonForce)
206 |
207 | return hillEquilibrium
--------------------------------------------------------------------------------
/UtilsDynamicSimulations/OpenSimAD/plotsOpenSimAD.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: plotsOpenSimAD.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2022 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse, Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | # TODO: This script is poorly documented and generates rough plots for
20 | # debugging.
21 |
22 | import numpy as np
23 |
24 | from utilsOpenSimAD import plotVSBounds
25 | from utilsOpenSimAD import plotVSvaryingBounds
26 |
27 | def plotGuessVSBounds(lw, uw, w0, nJoints, N, d, guessQsEnd,
28 | guessQdsEnd, withArms=True,
29 | withLumbarCoordinateActuators=True,
30 | torque_driven_model=False):
31 |
32 | # States.
33 | if torque_driven_model:
34 | # Coordinate activation at mesh points.
35 | lwp = lw['CoordA'].to_numpy().T
36 | uwp = uw['CoordA'].to_numpy().T
37 | y = w0['CoordA'].to_numpy().T
38 | title='Coordinate activation at mesh points'
39 | plotVSBounds(y,lwp,uwp,title)
40 | # Coordinate activation at collocation points.
41 | lwp = lw['CoordA'].to_numpy().T
42 | uwp = uw['CoordA'].to_numpy().T
43 | y = w0['CoordAj'].to_numpy().T
44 | title='Coordinate activation at collocation points'
45 | plotVSBounds(y,lwp,uwp,title)
46 | else:
47 | # Muscle activation at mesh points.
48 | lwp = lw['A'].to_numpy().T
49 | uwp = uw['A'].to_numpy().T
50 | y = w0['A'].to_numpy().T
51 | title='Muscle activation at mesh points'
52 | plotVSBounds(y,lwp,uwp,title)
53 | # Muscle activation at collocation points.
54 | lwp = lw['A'].to_numpy().T
55 | uwp = uw['A'].to_numpy().T
56 | y = w0['Aj'].to_numpy().T
57 | title='Muscle activation at collocation points'
58 | plotVSBounds(y,lwp,uwp,title)
59 | # Muscle force at mesh points.
60 | lwp = lw['F'].to_numpy().T
61 | uwp = uw['F'].to_numpy().T
62 | y = w0['F'].to_numpy().T
63 | title='Muscle force at mesh points'
64 | plotVSBounds(y,lwp,uwp,title)
65 | # Muscle force at collocation points.
66 | lwp = lw['F'].to_numpy().T
67 | uwp = uw['F'].to_numpy().T
68 | y = w0['Fj'].to_numpy().T
69 | title='Muscle force at collocation points'
70 | plotVSBounds(y,lwp,uwp,title)
71 | # Joint position at mesh points.
72 | lwp = np.reshape(
73 | lw['Qsk'], (nJoints, N+1), order='F')
74 | uwp = np.reshape(
75 | uw['Qsk'], (nJoints, N+1), order='F')
76 | y = guessQsEnd
77 | title='Joint position at mesh points'
78 | plotVSvaryingBounds(y,lwp,uwp,title)
79 | # Joint position at collocation points.
80 | lwp = np.reshape(
81 | lw['Qsj'], (nJoints, d*N), order='F')
82 | uwp = np.reshape(
83 | uw['Qsj'], (nJoints, d*N), order='F')
84 | y = w0['Qsj'].to_numpy().T
85 | title='Joint position at collocation points'
86 | plotVSvaryingBounds(y,lwp,uwp,title)
87 | # Joint velocity at mesh points.
88 | lwp = lw['Qds'].to_numpy().T
89 | uwp = uw['Qds'].to_numpy().T
90 | y = guessQdsEnd
91 | title='Joint velocity at mesh points'
92 | plotVSBounds(y,lwp,uwp,title)
93 | # Joint velocity at collocation points.
94 | lwp = lw['Qds'].to_numpy().T
95 | uwp = uw['Qds'].to_numpy().T
96 | y = w0['Qdsj'].to_numpy().T
97 | title='Joint velocity at collocation points'
98 | plotVSBounds(y,lwp,uwp,title)
99 | if withArms:
100 | # Arm activation at mesh points.
101 | lwp = lw['ArmA'].to_numpy().T
102 | uwp = uw['ArmA'].to_numpy().T
103 | y = w0['ArmA'].to_numpy().T
104 | title='Arm activation at mesh points'
105 | plotVSBounds(y,lwp,uwp,title)
106 | # Arm activation at collocation points.
107 | lwp = lw['ArmA'].to_numpy().T
108 | uwp = uw['ArmA'].to_numpy().T
109 | y = w0['ArmAj'].to_numpy().T
110 | title='Arm activation at collocation points'
111 | plotVSBounds(y,lwp,uwp,title)
112 | if withLumbarCoordinateActuators:
113 | # Lumbar activation at mesh points.
114 | lwp = lw['LumbarA'].to_numpy().T
115 | uwp = uw['LumbarA'].to_numpy().T
116 | y = w0['LumbarA'].to_numpy().T
117 | title='Lumbar activation at mesh points'
118 | plotVSBounds(y,lwp,uwp,title)
119 | # Lumbar activation at collocation points.
120 | lwp = lw['LumbarA'].to_numpy().T
121 | uwp = uw['LumbarA'].to_numpy().T
122 | y = w0['LumbarAj'].to_numpy().T
123 | title='Lumbar activation at collocation points'
124 | plotVSBounds(y,lwp,uwp,title)
125 | # Controls.
126 | if torque_driven_model:
127 | # Coordinate excitation at mesh points.
128 | lwp = lw['CoordE'].to_numpy().T
129 | uwp = uw['CoordE'].to_numpy().T
130 | y = w0['CoordE'].to_numpy().T
131 | title='Coordinate excitation at mesh points'
132 | plotVSBounds(y,lwp,uwp,title)
133 | else:
134 | # Muscle activation derivative at mesh points.
135 | lwp = lw['ADt'].to_numpy().T
136 | uwp = uw['ADt'].to_numpy().T
137 | y = w0['ADt'].to_numpy().T
138 | title='Muscle activation derivative at mesh points'
139 | plotVSBounds(y,lwp,uwp,title)
140 | if withArms:
141 | # Arm excitation at mesh points.
142 | lwp = lw['ArmE'].to_numpy().T
143 | uwp = uw['ArmE'].to_numpy().T
144 | y = w0['ArmE'].to_numpy().T
145 | title='Arm excitation at mesh points'
146 | plotVSBounds(y,lwp,uwp,title)
147 | if withLumbarCoordinateActuators:
148 | # Lumbar excitation at mesh points.
149 | lwp = lw['LumbarE'].to_numpy().T
150 | uwp = uw['LumbarE'].to_numpy().T
151 | y = w0['LumbarE'].to_numpy().T
152 | title='Lumbar excitation at mesh points'
153 | plotVSBounds(y,lwp,uwp,title)
154 | if not torque_driven_model:
155 | # Muscle force derivative at mesh points.
156 | lwp = lw['FDt'].to_numpy().T
157 | uwp = uw['FDt'].to_numpy().T
158 | y = w0['FDt'].to_numpy().T
159 | title='Muscle force derivative at mesh points'
160 | plotVSBounds(y,lwp,uwp,title)
161 | # Joint velocity derivative (acceleration) at mesh points.
162 | lwp = lw['Qdds'].to_numpy().T
163 | uwp = uw['Qdds'].to_numpy().T
164 | y = w0['Qdds'].to_numpy().T
165 | title='Joint velocity derivative (acceleration) at mesh points'
166 | plotVSBounds(y,lwp,uwp,title)
167 |
168 | def plotOptimalSolutionVSBounds(lw, uw, c_wopt, torque_driven_model=False):
169 | # States
170 | if torque_driven_model:
171 | # Coordinate activation at mesh points
172 | lwp = lw['CoordA'].to_numpy().T
173 | uwp = uw['CoordA'].to_numpy().T
174 | y = c_wopt['aCoord_opt']
175 | title='Coordinate activation at mesh points'
176 | plotVSBounds(y,lwp,uwp,title)
177 | # Coordinate activation at collocation points
178 | lwp = lw['CoordA'].to_numpy().T
179 | uwp = uw['CoordA'].to_numpy().T
180 | y = c_wopt['aCoord_col_opt']
181 | title='Coordinate activation at collocation points'
182 | plotVSBounds(y,lwp,uwp,title)
183 | else:
184 | # Muscle activation at mesh points
185 | lwp = lw['A'].to_numpy().T
186 | uwp = uw['A'].to_numpy().T
187 | y = c_wopt['a_opt']
188 | title='Muscle activation at mesh points'
189 | plotVSBounds(y,lwp,uwp,title)
190 | # Muscle activation at collocation points
191 | lwp = lw['A'].to_numpy().T
192 | uwp = uw['A'].to_numpy().T
193 | y = c_wopt['a_col_opt']
194 | title='Muscle activation at collocation points'
195 | plotVSBounds(y,lwp,uwp,title)
196 | # Muscle force at mesh points
197 | lwp = lw['F'].to_numpy().T
198 | uwp = uw['F'].to_numpy().T
199 | y = c_wopt['nF_opt']
200 | title='Muscle force at mesh points'
201 | plotVSBounds(y,lwp,uwp,title)
202 | # Muscle force at collocation points
203 | lwp = lw['F'].to_numpy().T
204 | uwp = uw['F'].to_numpy().T
205 | y = c_wopt['nF_col_opt']
206 | title='Muscle force at collocation points'
207 | plotVSBounds(y,lwp,uwp,title)
208 | # Joint position at mesh points
209 | lwp = lw['Qs'].to_numpy().T
210 | uwp = uw['Qs'].to_numpy().T
211 | y = c_wopt['Qs_opt']
212 | title='Joint position at mesh points'
213 | plotVSBounds(y,lwp,uwp,title)
214 | # Joint position at collocation points
215 | lwp = lw['Qs'].to_numpy().T
216 | uwp = uw['Qs'].to_numpy().T
217 | y = c_wopt['Qs_col_opt']
218 | title='Joint position at collocation points'
219 | plotVSBounds(y,lwp,uwp,title)
220 | # Joint velocity at mesh points
221 | lwp = lw['Qds'].to_numpy().T
222 | uwp = uw['Qds'].to_numpy().T
223 | y = c_wopt['Qds_opt']
224 | title='Joint velocity at mesh points'
225 | plotVSBounds(y,lwp,uwp,title)
226 | # Joint velocity at collocation points
227 | lwp = lw['Qds'].to_numpy().T
228 | uwp = uw['Qds'].to_numpy().T
229 | y = c_wopt['Qds_col_opt']
230 | title='Joint velocity at collocation points'
231 | plotVSBounds(y,lwp,uwp,title)
232 | # Controls
233 | if torque_driven_model:
234 | # Muscle activation derivative at mesh points
235 | lwp = lw['CoordE'].to_numpy().T
236 | uwp = uw['CoordE'].to_numpy().T
237 | y = c_wopt['eCoord_opt']
238 | title='Coordinate excitation at mesh points'
239 | plotVSBounds(y,lwp,uwp,title)
240 | else:
241 | # Muscle activation derivative at mesh points
242 | lwp = lw['ADt'].to_numpy().T
243 | uwp = uw['ADt'].to_numpy().T
244 | y = c_wopt['aDt_opt']
245 | title='Muscle activation derivative at mesh points'
246 | plotVSBounds(y,lwp,uwp,title)
247 | # Slack controls
248 | if not torque_driven_model:
249 | # Muscle force derivative at collocation points
250 | lwp = lw['FDt'].to_numpy().T
251 | uwp = uw['FDt'].to_numpy().T
252 | y = c_wopt['nFDt_opt']
253 | title='Muscle force derivative at collocation points'
254 | plotVSBounds(y,lwp,uwp,title)
255 | # Joint velocity derivative (acceleration) at collocation points
256 | lwp = lw['Qdds'].to_numpy().T
257 | uwp = uw['Qdds'].to_numpy().T
258 | y = c_wopt['Qdds_opt']
259 | title='Joint velocity derivative (acceleration) at collocation points'
260 | plotVSBounds(y,lwp,uwp,title)
--------------------------------------------------------------------------------
/batchDownload.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "nbformat": 4,
3 | "nbformat_minor": 0,
4 | "metadata": {
5 | "colab": {
6 | "provenance": [],
7 | "collapsed_sections": [],
8 | "authorship_tag": "ABX9TyNxMtdWTBzmUVRZJLMBfmmo",
9 | "include_colab_link": true
10 | },
11 | "kernelspec": {
12 | "name": "python3",
13 | "display_name": "Python 3"
14 | },
15 | "language_info": {
16 | "name": "python"
17 | }
18 | },
19 | "cells": [
20 | {
21 | "cell_type": "markdown",
22 | "metadata": {
23 | "id": "view-in-github",
24 | "colab_type": "text"
25 | },
26 | "source": [
27 | "
"
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "source": [
33 | "# Batch download\n",
34 | "\n",
35 | "**Aim**\n",
36 | "\n",
37 | "With this jupyter notebook, you can download data collected using [OpenCap](https://www.opencap.ai/). Note that you can also use the python script [batchDownload.py](https://github.com/stanfordnmbl/opencap-processing/blob/main/batchDownload.py) for this purpose.\n",
38 | "\n",
39 | "**Users inputs**\n",
40 | "\n",
41 | "You will need to authenticate using the credentials you used when collecting data with the [web application](https://app.opencap.ai/). You will also need to provide the IDs of the sessions you would like to download (see Download data below)."
42 | ],
43 | "metadata": {
44 | "id": "ozEyD2dWIsuu"
45 | }
46 | },
47 | {
48 | "cell_type": "markdown",
49 | "source": [
50 | "# Setup conda and install packages\n",
51 | "\n",
52 | "In the following cell, you will use [condacolab](https://github.com/conda-incubator/condacolab) to install [Miniconda](https://docs.conda.io/en/latest/miniconda.html). You will also install several packages needed for downloading your data."
53 | ],
54 | "metadata": {
55 | "id": "u855yu106dry"
56 | }
57 | },
58 | {
59 | "cell_type": "code",
60 | "execution_count": 45,
61 | "metadata": {
62 | "id": "1hd2tkuEEWUf",
63 | "outputId": "464f1053-aa44-46ab-dd5e-bebc51af0440",
64 | "colab": {
65 | "base_uri": "https://localhost:8080/"
66 | }
67 | },
68 | "outputs": [
69 | {
70 | "output_type": "stream",
71 | "name": "stdout",
72 | "text": [
73 | "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
74 | "\u001b[0m✨🍰✨ Everything looks OK!\n",
75 | "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
76 | "Requirement already satisfied: python-decouple in /usr/local/lib/python3.7/site-packages (3.6)\n",
77 | "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
78 | "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
79 | "Requirement already satisfied: maskpass==0.3.6 in /usr/local/lib/python3.7/site-packages (0.3.6)\n",
80 | "Requirement already satisfied: pynput in /usr/local/lib/python3.7/site-packages (from maskpass==0.3.6) (1.7.6)\n",
81 | "Requirement already satisfied: evdev>=1.3 in /usr/local/lib/python3.7/site-packages (from pynput->maskpass==0.3.6) (1.6.0)\n",
82 | "Requirement already satisfied: six in /usr/local/lib/python3.7/site-packages (from pynput->maskpass==0.3.6) (1.16.0)\n",
83 | "Requirement already satisfied: python-xlib>=0.17 in /usr/local/lib/python3.7/site-packages (from pynput->maskpass==0.3.6) (0.31)\n",
84 | "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
85 | "\u001b[0mLooking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n",
86 | "Requirement already satisfied: requests in /usr/local/lib/python3.7/site-packages (2.28.1)\n",
87 | "Requirement already satisfied: urllib3<1.27,>=1.21.1 in /usr/local/lib/python3.7/site-packages (from requests) (1.26.11)\n",
88 | "Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/site-packages (from requests) (2022.6.15)\n",
89 | "Requirement already satisfied: charset-normalizer<3,>=2 in /usr/local/lib/python3.7/site-packages (from requests) (2.1.1)\n",
90 | "Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.7/site-packages (from requests) (3.3)\n",
91 | "\u001b[33mWARNING: Running pip as the 'root' user can result in broken permissions and conflicting behaviour with the system package manager. It is recommended to use a virtual environment instead: https://pip.pypa.io/warnings/venv\u001b[0m\u001b[33m\n",
92 | "\u001b[0m"
93 | ]
94 | }
95 | ],
96 | "source": [
97 | "!pip install -q condacolab\n",
98 | "import condacolab\n",
99 | "condacolab.install()\n",
100 | "!pip install python-decouple\n",
101 | "!pip install maskpass==0.3.6\n",
102 | "!pip install requests\n",
103 | "!conda install -c opensim-org opensim=4.4.1=py310np121"
104 | ]
105 | },
106 | {
107 | "cell_type": "markdown",
108 | "source": [
109 | "# Clone repository\n",
110 | "\n",
111 | "The code required to download your data is part of [opencap-processing](https://github.com/stanfordnmbl/opencap-processing). You will here clone the repository (or update it) and add it to the path."
112 | ],
113 | "metadata": {
114 | "id": "CO40IXqc642z"
115 | }
116 | },
117 | {
118 | "cell_type": "code",
119 | "source": [
120 | "import os\n",
121 | "if os.path.exists(os.path.join(os.getcwd(), 'opencap-processing')):\n",
122 | " %cd opencap-processing\n",
123 | " !git pull\n",
124 | " %cd ..\n",
125 | "else:\n",
126 | " !git clone https://github.com/stanfordnmbl/opencap-processing.git\n",
127 | "import sys\n",
128 | "sys.path.append('./opencap-processing')"
129 | ],
130 | "metadata": {
131 | "colab": {
132 | "base_uri": "https://localhost:8080/"
133 | },
134 | "id": "CtNBj5OWFe0T",
135 | "outputId": "a6131cda-f165-4508-988d-e570bb6776ed"
136 | },
137 | "execution_count": 46,
138 | "outputs": [
139 | {
140 | "output_type": "stream",
141 | "name": "stdout",
142 | "text": [
143 | "/content/opencap-processing\n",
144 | "Already up to date.\n",
145 | "/content\n"
146 | ]
147 | }
148 | ]
149 | },
150 | {
151 | "cell_type": "markdown",
152 | "source": [
153 | "# Download data\n",
154 | "\n",
155 | "Add the IDs of the sessions you would like to download to the variable `sessionList` in the code cell below, you can list as many session IDs as you like. The session ID is the 36-character string at the end of the session url. For example, the session ID for https://app.opencap.ai/session/7272a71a-e70a-4794-a253-39e11cb7542c is '7272a71a-e70a-4794-a253-39e11cb7542c'. You can find your sessions at https://app.opencap.ai/sessions. You will be prompted to enter your username and password. The data will be downloaded in your Downloads folder."
156 | ],
157 | "metadata": {
158 | "id": "UaO5mayq-NaE"
159 | }
160 | },
161 | {
162 | "cell_type": "code",
163 | "source": [
164 | "from utils import download_session\n",
165 | "import shutil\n",
166 | "from google.colab import files \n",
167 | "\n",
168 | "# Enter the session IDs you would like to download.\n",
169 | "sessionList = ['7272a71a-e70a-4794-a253-39e11cb7542c',\n",
170 | " 'abe79267-646f-436b-a19e-a9e1d8f32807']\n",
171 | "\n",
172 | "downloadPath = os.path.join(os.getcwd(), 'Data')\n",
173 | "for session_id in sessionList:\n",
174 | " # If you only want markers and kinematics, downloadVideos=False will be faster.\n",
175 | " download_session(session_id,sessionBasePath=downloadPath,downloadVideos=True)\n",
176 | " pathData = os.path.join(downloadPath, 'OpenCapData_' + session_id)\n",
177 | " shutil.make_archive(pathData, 'zip', pathData)\n",
178 | " shutil.rmtree(pathData)\n",
179 | " pathZip = pathData + '.zip'\n",
180 | " files.download(pathZip)"
181 | ],
182 | "metadata": {
183 | "id": "euExyeyQJK_O"
184 | },
185 | "execution_count": null,
186 | "outputs": []
187 | }
188 | ]
189 | }
190 |
--------------------------------------------------------------------------------
/batchDownload.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: batchDownload.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | from utils import download_session
22 | import os
23 |
24 | # List of sessions you'd like to download. They go to the Data folder in the
25 | # current directory.
26 | sessionList = ['7272a71a-e70a-4794-a253-39e11cb7542c',
27 | 'abe79267-646f-436b-a19e-a9e1d8f32807']
28 |
29 |
30 | # # alternatively, read list of session IDs from CSV column
31 | # from pathlib import Path
32 | # import pandas as pd
33 | # fpath = Path('~/Documents/paru/session_ids_fshd.csv')
34 | # df = pd.read_csv(fpath)
35 | # sessionList = df['sid'].dropna().unique()
36 |
37 |
38 | # base directory for downloads. Specify None if you want to go to os.path.join(os.getcwd(),'Data')
39 | downloadPath = os.path.join(os.getcwd(),'Data')
40 |
41 | for session_id in sessionList:
42 | # If only interested in marker and OpenSim data, downladVideos=False will be faster
43 | download_session(session_id,sessionBasePath=downloadPath,downloadVideos=True)
--------------------------------------------------------------------------------
/createAuthenticationEnvFile.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: createAuthenticationEnvFile.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | # Run this script to save authentication token into a .env file.
22 | # DO NOT CHANGE THE FILE NAME OF THE .env FILE, or your credentials will get
23 | # pushed to github with a commit, and anyone could get access to your data.
24 | # We recommend only saving this .env file on your own encrypted machine
25 | # and not while running on google collab.
26 |
27 | from utilsAuthentication import get_token
28 | import os
29 |
30 | get_token(saveEnvPath=os.getcwd())
31 |
--------------------------------------------------------------------------------
/example.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | import os
22 | import utilsKinematics
23 | from utils import download_kinematics
24 | from utilsPlotting import plot_dataframe
25 |
26 | # %% User inputs.
27 | # Specify session id; see end of url in app.opencap.ai/session/.
28 | session_id = "4d5c3eb1-1a59-4ea1-9178-d3634610561c"
29 |
30 | # Specify trial names in a list; use None to process all trials in a session.
31 | specific_trial_names = ['jump']
32 |
33 | # Specify where to download the data.
34 | data_folder = os.path.join("./Data", session_id)
35 |
36 | # %% Download data.
37 | trial_names, modelName = download_kinematics(session_id, folder=data_folder, trialNames=specific_trial_names)
38 |
39 | # %% Process data.
40 | kinematics, coordinates, muscle_tendon_lengths, moment_arms, center_of_mass = {}, {}, {}, {}, {}
41 | coordinates['values'], coordinates['speeds'], coordinates['accelerations'] = {}, {}, {}
42 | center_of_mass['values'], center_of_mass['speeds'], center_of_mass['accelerations'] = {}, {}, {}
43 |
44 | for trial_name in trial_names:
45 | # Create object from class kinematics.
46 | kinematics[trial_name] = utilsKinematics.kinematics(data_folder, trial_name, modelName=modelName, lowpass_cutoff_frequency_for_coordinate_values=10)
47 |
48 | # Get coordinate values, speeds, and accelerations.
49 | coordinates['values'][trial_name] = kinematics[trial_name].get_coordinate_values(in_degrees=True) # already filtered
50 | coordinates['speeds'][trial_name] = kinematics[trial_name].get_coordinate_speeds(in_degrees=True, lowpass_cutoff_frequency=10)
51 | coordinates['accelerations'][trial_name] = kinematics[trial_name].get_coordinate_accelerations(in_degrees=True, lowpass_cutoff_frequency=10)
52 |
53 | # Get muscle-tendon lengths and moment arms.
54 | muscle_tendon_lengths[trial_name] = kinematics[trial_name].get_muscle_tendon_lengths()
55 | # moment_arms[trial_name] = kinematics[trial_name].get_moment_arms()
56 |
57 | # Get center of mass values, speeds, and accelerations.
58 | center_of_mass['values'][trial_name] = kinematics[trial_name].get_center_of_mass_values(lowpass_cutoff_frequency=10)
59 | center_of_mass['speeds'][trial_name] = kinematics[trial_name].get_center_of_mass_speeds(lowpass_cutoff_frequency=10)
60 | center_of_mass['accelerations'][trial_name] = kinematics[trial_name].get_center_of_mass_accelerations(lowpass_cutoff_frequency=10)
61 |
62 |
63 | # %% Print as csv: example.
64 | output_csv_dir = os.path.join(data_folder, 'OpenSimData', 'Kinematics', 'Outputs')
65 | os.makedirs(output_csv_dir, exist_ok=True)
66 | output_csv_path = os.path.join(output_csv_dir, 'coordinate_speeds_{}.csv'.format(trial_names[0]))
67 | coordinates['speeds'][trial_names[0]].to_csv(output_csv_path)
68 |
69 | # %% Plot: examples.
70 | # Plot all coordinate values against time.
71 | plot_dataframe(dataframes = [coordinates['values'][trial_names[0]]],
72 | xlabel = 'Time (s)',
73 | ylabel = 'Pos (m or deg)',
74 | title = 'Coordinate values',
75 | labels = [trial_names[0]])
76 |
77 | # Plot selected coordinate speeds against time.
78 | plot_dataframe(dataframes = [coordinates['speeds'][trial_names[0]]],
79 | y = ['hip_flexion_l', 'knee_angle_l'],
80 | xlabel = 'Time (s)',
81 | ylabel = 'Vel (deg/s)',
82 | title = 'Coordinate speeds',
83 | labels = [trial_names[0]])
84 |
85 | # Plot knee flexion accelerations against hip flexion accelerations.
86 | plot_dataframe(dataframes = [coordinates['accelerations'][trial_names[0]]],
87 | x = 'knee_angle_l',
88 | y = ['hip_flexion_l'],
89 | xlabel = 'Knee angle acceleration (deg/s^2)',
90 | ylabel = 'Hip flexion acceleration (deg/s^2)',
91 | labels = [trial_names[0]])
92 |
93 | # Plot center of mass accelerations.
94 | plot_dataframe(dataframes = [center_of_mass['accelerations'][trial_names[0]]],
95 | xlabel = 'Time (s)',
96 | title = 'Center of mass accelerations',
97 | labels = [trial_names[0]])
98 |
99 | # Plot muscle-tendon lengths against time.
100 | plot_dataframe(dataframes = [muscle_tendon_lengths[trial_names[0]]],
101 | y = ['bflh_r', 'gasmed_r', 'recfem_r'],
102 | xlabel = 'Time (s)',
103 | title = 'Muscle-tendon lengths',
104 | labels = [trial_names[0]])
105 |
--------------------------------------------------------------------------------
/example_kinetics.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: example_kinetics.py
4 | ---------------------------------------------------------------------------
5 | Copyright 2022 Stanford University and the Authors
6 |
7 | Author(s): Antoine Falisse, Scott Uhlrich
8 |
9 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
10 | use this file except in compliance with the License. You may obtain a copy
11 | of the License at http://www.apache.org/licenses/LICENSE-2.0
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 | This code makes use of CasADi, which is licensed under LGPL, Version 3.0;
19 | https://github.com/casadi/casadi/blob/master/LICENSE.txt.
20 |
21 | Install requirements:
22 | - Visit https://github.com/stanfordnmbl/opencap-processing for details.
23 | - Third-party software packages:
24 | - CMake: https://cmake.org/download/.
25 | - (Windows only)
26 | - Visual studio: https://visualstudio.microsoft.com/downloads/.
27 | - Make sure you install C++ support.
28 | - Code tested with community editions 2017-2019-2022.
29 |
30 | Please contact us for any questions: https://www.opencap.ai/#contact
31 | '''
32 |
33 | # %% Directories, paths, and imports. You should not need to change anything.
34 | import os
35 | import sys
36 |
37 | baseDir = os.getcwd()
38 | opensimADDir = os.path.join(baseDir, 'UtilsDynamicSimulations', 'OpenSimAD')
39 | sys.path.append(baseDir)
40 | sys.path.append(opensimADDir)
41 |
42 | from utilsOpenSimAD import processInputsOpenSimAD, plotResultsOpenSimAD
43 | from mainOpenSimAD import run_tracking
44 |
45 | # %% User inputs.
46 | '''
47 | Please provide:
48 |
49 | session_id: This is a 36 character-long string. You can find the ID of
50 | all your sessions at https://app.opencap.ai/sessions.
51 |
52 | trial_name: This is the name of the trial you want to simulate. You can
53 | find all trial names after loading a session.
54 |
55 | motion_type: This is the type of activity you want to simulate. Options
56 | are 'running', 'walking', 'drop_jump', 'sit-to-stand', and
57 | 'squats'. We provide pre-defined settings that worked well
58 | for this set of activities. If your activity is different,
59 | select 'other' to use generic settings or set your own
60 | settings in settingsOpenSimAD. See for example how we tuned
61 | the 'running' settings to include periodic constraints in
62 | the 'my_periodic_running' settings.
63 |
64 | time_window: This is the time interval you want to simulate. It is
65 | recommended to simulate trials shorter than 2s. Set to []
66 | to simulate full trial. For 'squats' or 'sit_to_stand', we
67 | built segmenters to separate the different repetitions. In
68 | such case, instead of providing the time_window, you can
69 | provide the index of the repetition (see below) and the
70 | time_window will be automatically computed.
71 |
72 | repetition: Only if motion_type is 'sit_to_stand' or 'squats'. This
73 | is the index of the repetition you want to simulate (0 is
74 | first). There is no need to set the time_window.
75 |
76 | case: This is a string that will be appended to the file names
77 | of the results. Dynamic simulations are optimization
78 | problems, and it is common to have to play with some
79 | settings to get the problem to converge or converge to a
80 | meaningful solution. It is useful to keep track of which
81 | solution corresponds to which settings; you can then easily
82 | compare results generated with different settings.
83 |
84 | (optional)
85 | treadmill_speed:This an optional parameter that indicates the speed of
86 | the treadmill in m/s. A positive value indicates that the
87 | subject is moving forward. You should ignore this parameter
88 | or set it to 0 if the trial was not measured on a
89 | treadmill. By default, treadmill_speed is set to 0.
90 | (optional)
91 | contact_side: This an optional parameter that indicates on which foot to
92 | add contact spheres to model foot-ground contact. It might
93 | be useful to only add contact spheres on one foot if only
94 | that foot is in contact with the ground. We found this to
95 | be helpful for simulating for instance single leg dropjump
96 | as it might prevent the optimizer to cheat by using the
97 | other foot to stabilize the model. Options are 'all',
98 | 'left', and 'right'. By default, contact_side is set to
99 | 'all', meaning that contact spheres are added to both feet.
100 |
101 | See example inputs below for different activities. Please note that we did not
102 | verify the biomechanical validity of the results; we only made sure the
103 | simulations converged to kinematic solutions that were visually reasonable.
104 |
105 | Please contact us for any questions: https://www.opencap.ai/#contact
106 | '''
107 |
108 | # We provide a few examples for overground and treadmill activities.
109 | # Select which example you would like to run.
110 | session_type = 'overground' # Options are 'overground' and 'treadmill'.
111 | session_id = "4d5c3eb1-1a59-4ea1-9178-d3634610561c"
112 | case = '0' # Change this to compare across settings.
113 | # Options are 'squat', 'STS', and 'jump'.
114 | if session_type == 'overground':
115 | trial_name = 'STS'
116 | if trial_name == 'squat': # Squat
117 | motion_type = 'squats'
118 | repetition = 1
119 | elif trial_name == 'STS': # Sit-to-stand
120 | motion_type = 'sit_to_stand'
121 | repetition = 1
122 | elif trial_name == 'jump': # Jump
123 | motion_type = 'jumping'
124 | time_window = [1.3, 2.2]
125 | # Options are 'walk_1_25ms', 'run_2_5ms', and 'run_4ms'.
126 | elif session_type == 'treadmill':
127 | trial_name = 'walk_1_25ms'
128 | torque_driven_model = False # Example with torque-driven model.
129 | if trial_name == 'walk_1_25ms': # Walking, 1.25 m/s
130 | motion_type = 'walking'
131 | time_window = [1.0, 2.5]
132 | treadmill_speed = 1.25
133 | elif trial_name == 'run_2_5ms': # Running, 2.5 m/s
134 | if torque_driven_model:
135 | motion_type = 'running_torque_driven'
136 | else:
137 | motion_type = 'running'
138 | time_window = [1.4, 2.6]
139 | treadmill_speed = 2.5
140 | elif trial_name == 'run_4ms': # Running with periodic constraints, 4.0 m/s
141 | motion_type = 'my_periodic_running'
142 | time_window = [3.1833333, 3.85]
143 | treadmill_speed = 4.0
144 |
145 | # Set to True to solve the optimal control problem.
146 | solveProblem = True
147 | # Set to True to analyze the results of the optimal control problem. If you
148 | # solved the problem already, and only want to analyze/process the results, you
149 | # can set solveProblem to False and run this script with analyzeResults set to
150 | # True. This is useful if you do additional post-processing but do not want to
151 | # re-run the problem.
152 | analyzeResults = True
153 |
154 | # Path to where you want the data to be downloaded.
155 | dataFolder = os.path.join(baseDir, 'Data')
156 |
157 | # %% Setup.
158 | if not 'time_window' in locals():
159 | time_window = None
160 | if not 'repetition' in locals():
161 | repetition = None
162 | if not 'treadmill_speed' in locals():
163 | treadmill_speed = 0
164 | if not 'contact_side' in locals():
165 | contact_side = 'all'
166 | settings = processInputsOpenSimAD(baseDir, dataFolder, session_id, trial_name,
167 | motion_type, time_window, repetition,
168 | treadmill_speed, contact_side)
169 |
170 | # %% Simulation.
171 | run_tracking(baseDir, dataFolder, session_id, settings, case=case,
172 | solveProblem=solveProblem, analyzeResults=analyzeResults)
173 |
174 | # %% Plots.
175 | # To compare different cases, add to the cases list, eg cases=['0','1'].
176 | plotResultsOpenSimAD(dataFolder, session_id, trial_name, settings, cases=[case])
177 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | scipy==1.10.0
2 | pandas
3 | matplotlib
4 | ipython
5 | python-decouple
6 | maskpass==0.3.6
7 | requests
8 | casadi
9 | pyyaml
10 | joblib
11 | cmake
12 | seaborn
--------------------------------------------------------------------------------
/utilsAPI.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: utilsAPI.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | from decouple import config
22 |
23 | def get_api_url():
24 | if 'API_URL' not in globals():
25 | global API_URL
26 | try: # look in environment file
27 | API_URL = config("API_URL")
28 | except: # default
29 | API_URL = "https://api.opencap.ai/"
30 | if API_URL[-1] != '/':
31 | API_URL= API_URL + '/'
32 |
33 | return API_URL
34 |
--------------------------------------------------------------------------------
/utilsAuthentication.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: utilsAuthentication.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | import requests
22 | from decouple import config
23 | import getpass
24 | import os
25 | import maskpass
26 | from utilsAPI import get_api_url
27 |
28 | API_URL = get_api_url()
29 |
30 | def get_token(saveEnvPath=None):
31 |
32 | if 'API_TOKEN' not in globals():
33 |
34 | try: # look in environment file
35 | token = config("API_TOKEN")
36 |
37 | except:
38 | try:
39 | # If spyder, use maskpass
40 | isSpyder = 'SPY_PYTHONPATH' in os.environ
41 | isPycharm = 'PYCHARM_HOSTED' in os.environ
42 | print('Login with credentials used at app.opencap.ai.\nVisit the website to make an account if you do not have one.\n')
43 |
44 | if isSpyder:
45 | un = maskpass.advpass(prompt="Enter Username:\n",ide=True)
46 | pw = maskpass.advpass(prompt="Enter Password:\n",ide=True)
47 | elif isPycharm:
48 | print('Warning, you are in Pycharm, so the password will show up in the console.\n To avoid this, run createAuthenticationEnvFile.py from the terminal,\nthen re-open PyCharm.')
49 | un = input("Enter Username:")
50 | pw = input("Enter Password (will be shown in console):")
51 | else:
52 | un = getpass.getpass(prompt='Enter Username: ', stream=None)
53 | pw = getpass.getpass(prompt='Enter Password: ', stream=None)
54 |
55 | data = {"username":un,"password":pw}
56 | resp = requests.post(API_URL + 'login/',data=data).json()
57 | token = resp['token']
58 |
59 | print('Login successful.')
60 |
61 | if saveEnvPath is not None:
62 | envPath = os.path.join(saveEnvPath,'.env')
63 |
64 | f = open(envPath, "w")
65 | f.write('API_TOKEN="' + token + '"')
66 | f.close()
67 | print('Authentication token saved to '+ envPath + '. DO NOT CHANGE THIS FILE NAME. If you do, your authentication token will get pushed to github. Restart your terminal for env file to load.')
68 |
69 | except:
70 | raise Exception('Login failed.')
71 |
72 | global API_TOKEN
73 | API_TOKEN = token
74 | else:
75 | token = API_TOKEN
76 |
77 | return token
78 |
--------------------------------------------------------------------------------
/utilsPlotting.py:
--------------------------------------------------------------------------------
1 | '''
2 | ---------------------------------------------------------------------------
3 | OpenCap processing: utilsPlotting.py
4 | ---------------------------------------------------------------------------
5 |
6 | Copyright 2022 Stanford University and the Authors
7 |
8 | Author(s): Antoine Falisse, Scott Uhlrich
9 |
10 | Licensed under the Apache License, Version 2.0 (the "License"); you may not
11 | use this file except in compliance with the License. You may obtain a copy
12 | of the License at http://www.apache.org/licenses/LICENSE-2.0
13 |
14 | Unless required by applicable law or agreed to in writing, software
15 | distributed under the License is distributed on an "AS IS" BASIS,
16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | See the License for the specific language governing permissions and
18 | limitations under the License.
19 | '''
20 |
21 | import numpy as np
22 | import matplotlib.pyplot as plt
23 |
24 | def plot_dataframe(dataframes, x=None, y=[], xlabel=None, ylabel=None,
25 | labels=None, title=None, xrange=None):
26 |
27 | # Handle case specific number of subplots.
28 | if not x and not y:
29 | nRow = int(np.ceil(np.sqrt(dataframes[0].shape[1]-1)))
30 | nCol = int(np.ceil(np.sqrt(dataframes[0].shape[1]-1)))
31 | if not xlabel:
32 | xlabel = list(dataframes[0].columns)[0]
33 | x = 'time'
34 | y = list(dataframes[0].columns)[1:]
35 | elif not x and y:
36 | nRow = int(np.ceil(np.sqrt(len(y))))
37 | nCol = int(np.ceil(np.sqrt(len(y))))
38 | if not xlabel:
39 | xlabel = list(dataframes[0].columns)[0]
40 | x = 'time'
41 | else:
42 | nRow = int(np.ceil(np.sqrt(len(y))))
43 | nCol = int(np.ceil(np.sqrt(len(y))))
44 | if not xlabel:
45 | xlabel = x
46 | if not ylabel:
47 | ylabel = y[0]
48 | if nRow >= len(y):
49 | nRow = 1
50 | nAxs = len(y)
51 |
52 | # Labels for legend.
53 | if not labels:
54 | labels = ['dataframe_' + str(i) for i in range(len(dataframes))]
55 | elif len(labels) != len(dataframes):
56 | print("WARNING: The number of labels ({}) does not match the number of input dataframes ({})".format(len(labels), len(dataframes)))
57 | labels = ['dataframe_' + str(i) for i in range(dataframes)]
58 |
59 | if nCol == 1: # Single plot.
60 | fig = plt.figure()
61 | color=iter(plt.cm.rainbow(np.linspace(0,1,len(dataframes))))
62 | for c, dataframe in enumerate(dataframes):
63 | c_color = next(color)
64 | plt.plot(dataframe[x], dataframe[y], c=c_color, label=labels[c])
65 | if xrange is not None:
66 | plt.xlim(xrange)
67 | else: # Multiple subplots.
68 | fig, axs = plt.subplots(nRow, nCol, sharex=True)
69 | for i, ax in enumerate(axs.flat):
70 | color=iter(plt.cm.rainbow(np.linspace(0,1,len(dataframes))))
71 | if i < nAxs:
72 | for c, dataframe in enumerate(dataframes):
73 | c_color = next(color)
74 | ax.plot(dataframe[x], dataframe[y[i]], c=c_color, label=labels[c])
75 | ax.set_title(y[i])
76 | if xrange is not None:
77 | plt.xlim(xrange)
78 | if i == 0:
79 | handles, labels = ax.get_legend_handles_labels()
80 |
81 | # Axis labels and legend.
82 | if nRow > 1 and nCol > 1:
83 | plt.setp(axs[-1, :], xlabel=xlabel)
84 | plt.setp(axs[:, 0], ylabel=ylabel)
85 | axs[0][0].legend(handles, labels)
86 | elif nRow == 1 and nCol > 1:
87 | plt.setp(axs[:,], xlabel=xlabel)
88 | plt.setp(axs[0,], ylabel=ylabel)
89 | axs[0,].legend(handles, labels)
90 | else:
91 | plt.xlabel(xlabel)
92 | plt.ylabel(ylabel)
93 | plt.legend(labels)
94 |
95 | if nRow == 1 and nCol == 1:
96 | # Add figure title.
97 | if title:
98 | plt.title(title)
99 | else:
100 | # Add figure title.
101 | if title:
102 | fig.suptitle(title)
103 | # Align labels.
104 | fig.align_ylabels()
105 | # Hidde empty subplots.
106 | nEmptySubplots = (nRow*nCol) - len(y)
107 | axs_flat = axs.flat
108 | for ax in (axs_flat[len(axs_flat)-nEmptySubplots:]):
109 | ax.set_visible(False)
110 |
111 | # Tight layout (should make figure big enough first).
112 | # fig.tight_layout()
113 |
114 | # Show plot (needed if running through terminal).
115 | plt.show()
116 |
117 |
118 | def plot_dataframe_with_shading(mean_dataframe, sd_dataframe=None, y=None,
119 | leg=None, xlabel=None, title=None, legend_entries=None):
120 | if not isinstance(mean_dataframe, list):
121 | mean_dataframe = [mean_dataframe]
122 |
123 | if sd_dataframe is not None:
124 | if not isinstance(sd_dataframe, list):
125 | sd_dataframe = [sd_dataframe]
126 |
127 | if not isinstance(leg, list):
128 | leg = [leg] * len(mean_dataframe)
129 |
130 | if y is None:
131 | y = [col for col in mean_dataframe[0].columns if col != 'time']
132 |
133 | columns = [col for col in y if not any(sub in col for sub in ['_beta', 'mtp', 'time'])]
134 |
135 | if leg[0] == 'r':
136 | columns = [col for col in columns if not col.endswith('_l')]
137 | elif leg[0] == 'l':
138 | columns = [col for col in columns if not col.endswith('_r')]
139 |
140 | num_columns = len(columns)
141 | num_rows = (num_columns + 3) // 4 # Always 4 columns per row
142 |
143 | colormap = plt.cm.get_cmap('viridis', len(mean_dataframe))
144 |
145 | fig, axes = plt.subplots(num_rows, 4, figsize=(12, 8))
146 | axes = axes.flatten()
147 |
148 | for i, column in enumerate(columns):
149 | row = i // 4
150 | ax = axes[i]
151 |
152 | for j, (mean_df, sd_df) in enumerate(zip(mean_dataframe, sd_dataframe)):
153 | if len(mean_dataframe) > 1:
154 | color = np.multiply(colormap(j),.7) # avoid yellow at end of viridis
155 | else:
156 | color = 'black'
157 |
158 | if leg[j] is not None and (column.endswith('_r') or column.endswith('_l')):
159 | col=column[:-2] + '_' + leg[j]
160 | colLabel = column[:-2]
161 | else:
162 | col = column
163 | colLabel = column
164 |
165 | mean_values = mean_df[col]
166 |
167 | if legend_entries is None:
168 | thisLegend = []
169 | else:
170 | thisLegend = legend_entries[j]
171 |
172 | ax.plot(mean_values, color=color, label=thisLegend)
173 |
174 | # Check if sd_df is not None before plotting
175 | if sd_df is not None:
176 | sd_column = col
177 | if sd_column in sd_df.columns:
178 | sd_values = sd_df[sd_column]
179 | ax.fill_between(
180 | range(len(mean_values)),
181 | mean_values - sd_values,
182 | mean_values + sd_values,
183 | color=color,
184 | alpha=0.3,
185 | linewidth=0, # Remove surrounding line
186 | )
187 |
188 |
189 | ax.set_xlabel(xlabel if row == num_rows - 1 else None, fontsize=12)
190 | ax.set_ylabel(colLabel, fontsize=12)
191 |
192 | # Increase font size for axis labels
193 | ax.tick_params(axis='both', which='major', labelsize=10)
194 |
195 | # Create the legend in the first subplot if legend_entries is provided
196 | if legend_entries:
197 | axes[0].legend()
198 |
199 | # Remove any unused subplots
200 | if num_columns < num_rows * 4:
201 | for i in range(num_columns, num_rows * 4):
202 | fig.delaxes(axes[i])
203 |
204 | # Title
205 | if title is not None:
206 | fig.suptitle(title)
207 |
208 | plt.tight_layout()
209 | plt.show()
210 |
--------------------------------------------------------------------------------
/utilsTRC.py:
--------------------------------------------------------------------------------
1 | """Manages the movement and use of data files."""
2 |
3 | import os
4 | import warnings
5 | from scipy.spatial.transform import Rotation as R
6 |
7 | import numpy as np
8 | from numpy.lib.recfunctions import append_fields
9 |
10 | class TRCFile(object):
11 | """A plain-text file format for storing motion capture marker trajectories.
12 | TRC stands for Track Row Column.
13 |
14 | The metadata for the file is stored in attributes of this object.
15 |
16 | See
17 | http://simtk-confluence.stanford.edu:8080/display/OpenSim/Marker+(.trc)+Files
18 | for more information.
19 |
20 | """
21 | def __init__(self, fpath=None, **kwargs):
22 | #path=None,
23 | #data_rate=None,
24 | #camera_rate=None,
25 | #num_frames=None,
26 | #num_markers=None,
27 | #units=None,
28 | #orig_data_rate=None,
29 | #orig_data_start_frame=None,
30 | #orig_num_frames=None,
31 | #marker_names=None,
32 | #time=None,
33 | #):
34 | """
35 | Parameters
36 | ----------
37 | fpath : str
38 | Valid file path to a TRC (.trc) file.
39 |
40 | """
41 | self.marker_names = []
42 | if fpath != None:
43 | self.read_from_file(fpath)
44 | else:
45 | for k, v in kwargs.items():
46 | setattr(self, k, v)
47 |
48 | def read_from_file(self, fpath):
49 | # Read the header lines / metadata.
50 | # ---------------------------------
51 | # Split by any whitespace.
52 | # TODO may cause issues with paths that have spaces in them.
53 | f = open(fpath)
54 | # These are lists of each entry on the first few lines.
55 | first_line = f.readline().split()
56 | # Skip the 2nd line.
57 | f.readline()
58 | third_line = f.readline().split()
59 | fourth_line = f.readline().split()
60 | f.close()
61 |
62 | # First line.
63 | if len(first_line) > 3:
64 | self.path = first_line[3]
65 | else:
66 | self.path = ''
67 |
68 | # Third line.
69 | self.data_rate = float(third_line[0])
70 | self.camera_rate = float(third_line[1])
71 | self.num_frames = int(third_line[2])
72 | self.num_markers = int(third_line[3])
73 | self.units = third_line[4]
74 | self.orig_data_rate = float(third_line[5])
75 | self.orig_data_start_frame = int(third_line[6])
76 | self.orig_num_frames = int(third_line[7])
77 |
78 | # Marker names.
79 | # The first and second column names are 'Frame#' and 'Time'.
80 | self.marker_names = fourth_line[2:]
81 |
82 | len_marker_names = len(self.marker_names)
83 | if len_marker_names != self.num_markers:
84 | warnings.warn('Header entry NumMarkers, %i, does not '
85 | 'match actual number of markers, %i. Changing '
86 | 'NumMarkers to match actual number.' % (
87 | self.num_markers, len_marker_names))
88 | self.num_markers = len_marker_names
89 |
90 | # Load the actual data.
91 | # ---------------------
92 | col_names = ['frame_num', 'time']
93 | # This naming convention comes from OpenSim's Inverse Kinematics tool,
94 | # when it writes model marker locations.
95 | for mark in self.marker_names:
96 | col_names += [mark + '_tx', mark + '_ty', mark + '_tz']
97 | dtype = {'names': col_names,
98 | 'formats': ['int'] + ['float64'] * (3 * self.num_markers + 1)}
99 | usecols = [i for i in range(3 * self.num_markers + 1 + 1)]
100 | self.data = np.loadtxt(fpath, delimiter='\t', skiprows=5, dtype=dtype,
101 | usecols=usecols)
102 | self.time = self.data['time']
103 |
104 | # Check the number of rows.
105 | n_rows = self.time.shape[0]
106 | if n_rows != self.num_frames:
107 | warnings.warn('%s: Header entry NumFrames, %i, does not '
108 | 'match actual number of frames, %i, Changing '
109 | 'NumFrames to match actual number.' % (fpath,
110 | self.num_frames, n_rows))
111 | self.num_frames = n_rows
112 |
113 | def __getitem__(self, key):
114 | """See `marker()`.
115 |
116 | """
117 | return self.marker(key)
118 |
119 | def units(self):
120 | return self.units
121 |
122 | def time(self):
123 | this_dat = np.empty((self.num_frames, 1))
124 | this_dat[:, 0] = self.time
125 | return this_dat
126 |
127 | def marker(self, name):
128 | """The trajectory of marker `name`, given as a `self.num_frames` x 3
129 | array. The order of the columns is x, y, z.
130 |
131 | """
132 | this_dat = np.empty((self.num_frames, 3))
133 | this_dat[:, 0] = self.data[name + '_tx']
134 | this_dat[:, 1] = self.data[name + '_ty']
135 | this_dat[:, 2] = self.data[name + '_tz']
136 | return this_dat
137 |
138 | def add_marker(self, name, x, y, z):
139 | """Add a marker, with name `name` to the TRCFile.
140 |
141 | Parameters
142 | ----------
143 | name : str
144 | Name of the marker; e.g., 'R.Hip'.
145 | x, y, z: array_like
146 | Coordinates of the marker trajectory. All 3 must have the same
147 | length.
148 |
149 | """
150 | if (len(x) != self.num_frames or len(y) != self.num_frames or len(z) !=
151 | self.num_frames):
152 | raise Exception('Length of data (%i, %i, %i) is not '
153 | 'NumFrames (%i).', len(x), len(y), len(z), self.num_frames)
154 | self.marker_names += [name]
155 | self.num_markers += 1
156 | if not hasattr(self, 'data'):
157 | self.data = np.array(x, dtype=[('%s_tx' % name, 'float64')])
158 | self.data = append_fields(self.data,
159 | ['%s_t%s' % (name, s) for s in 'yz'],
160 | [y, z], usemask=False)
161 | else:
162 | self.data = append_fields(self.data,
163 | ['%s_t%s' % (name, s) for s in 'xyz'],
164 | [x, y, z], usemask=False)
165 |
166 | def marker_at(self, name, time):
167 | x = np.interp(time, self.time, self.data[name + '_tx'])
168 | y = np.interp(time, self.time, self.data[name + '_ty'])
169 | z = np.interp(time, self.time, self.data[name + '_tz'])
170 | return [x, y, z]
171 |
172 | def marker_exists(self, name):
173 | """
174 | Returns
175 | -------
176 | exists : bool
177 | Is the marker in the TRCFile?
178 |
179 | """
180 | return name in self.marker_names
181 |
182 | def write(self, fpath):
183 | """Write this TRCFile object to a TRC file.
184 |
185 | Parameters
186 | ----------
187 | fpath : str
188 | Valid file path to which this TRCFile is saved.
189 |
190 | """
191 | f = open(fpath, 'w')
192 |
193 | # Line 1.
194 | f.write('PathFileType 4\t(X/Y/Z) %s\n' % os.path.split(fpath)[0])
195 |
196 | # Line 2.
197 | f.write('DataRate\tCameraRate\tNumFrames\tNumMarkers\t'
198 | 'Units\tOrigDataRate\tOrigDataStartFrame\tOrigNumFrames\n')
199 |
200 | # Line 3.
201 | f.write('%.1f\t%.1f\t%i\t%i\t%s\t%.1f\t%i\t%i\n' % (
202 | self.data_rate, self.camera_rate, self.num_frames,
203 | self.num_markers, self.units, self.orig_data_rate,
204 | self.orig_data_start_frame, self.orig_num_frames))
205 |
206 | # Line 4.
207 | f.write('Frame#\tTime\t')
208 | for imark in range(self.num_markers):
209 | f.write('%s\t\t\t' % self.marker_names[imark])
210 | f.write('\n')
211 |
212 | # Line 5.
213 | f.write('\t\t')
214 | for imark in np.arange(self.num_markers) + 1:
215 | f.write('X%i\tY%s\tZ%s\t' % (imark, imark, imark))
216 | f.write('\n')
217 |
218 | # Line 6.
219 | f.write('\n')
220 |
221 | # Data.
222 | for iframe in range(self.num_frames):
223 | f.write('%i' % (iframe + 1))
224 | f.write('\t%.7f' % self.time[iframe])
225 | for mark in self.marker_names:
226 | idxs = [mark + '_tx', mark + '_ty', mark + '_tz']
227 | f.write('\t%.7f\t%.7f\t%.7f' % tuple(
228 | self.data[coln][iframe] for coln in idxs))
229 | f.write('\n')
230 |
231 | f.close()
232 |
233 | def add_noise(self, noise_width):
234 | """ add random noise to each component of the marker trajectory
235 | The noise mean will be zero, with the noise_width being the
236 | standard deviation.
237 |
238 | noise_width : int
239 | """
240 | for imarker in range(self.num_markers):
241 | components = ['_tx', '_ty', '_tz']
242 | for iComponent in range(3):
243 | # generate noise
244 | noise = np.random.normal(0, noise_width, self.num_frames)
245 | # add noise to each component of marker data.
246 | self.data[self.marker_names[imarker] + components[iComponent]] += noise
247 |
248 | def rotate(self, axis, value):
249 | """ rotate the data.
250 |
251 | axis : rotation axis
252 | value : angle in degree
253 | """
254 | for imarker in range(self.num_markers):
255 |
256 | temp = np.zeros((self.num_frames, 3))
257 | temp[:,0] = self.data[self.marker_names[imarker] + '_tx']
258 | temp[:,1] = self.data[self.marker_names[imarker] + '_ty']
259 | temp[:,2] = self.data[self.marker_names[imarker] + '_tz']
260 |
261 | r = R.from_euler(axis, value, degrees=True)
262 | temp_rot = r.apply(temp)
263 |
264 | self.data[self.marker_names[imarker] + '_tx'] = temp_rot[:,0]
265 | self.data[self.marker_names[imarker] + '_ty'] = temp_rot[:,1]
266 | self.data[self.marker_names[imarker] + '_tz'] = temp_rot[:,2]
267 |
268 | def offset(self, axis, value):
269 | """ offset the data.
270 |
271 | axis : rotation axis
272 | value : offset in m
273 | """
274 | for imarker in range(self.num_markers):
275 | if axis.lower() == 'x':
276 | self.data[self.marker_names[imarker] + '_tx'] += value
277 | elif axis.lower() == 'y':
278 | self.data[self.marker_names[imarker] + '_ty'] += value
279 | elif axis.lower() == 'z':
280 | self.data[self.marker_names[imarker] + '_tz'] += value
281 | else:
282 | raise ValueError("Axis not recognized")
283 |
284 | def trc_2_dict(pathFile, rotation=None):
285 | # rotation is a dict, eg. {'y':90} with axis, angle for rotation
286 | trc_dict = {}
287 | trc_file = TRCFile(pathFile)
288 | trc_dict['time'] = trc_file.time
289 | trc_dict['marker_names'] = trc_file.marker_names
290 | trc_dict['markers'] = {}
291 |
292 | if rotation != None:
293 | for axis,angle in rotation.items():
294 | trc_file.rotate(axis,angle)
295 | for count, marker in enumerate(trc_dict['marker_names']):
296 | trc_dict['markers'][marker] = trc_file.marker(marker)
297 |
298 | return trc_dict
299 |
--------------------------------------------------------------------------------