├── .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 | ![image](https://github.com/stanfordnmbl/opencap-processing/assets/43877159/a37b0930-cdc9-47ad-b023-6d9c001c938f) 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 | ![image](https://github.com/stanfordnmbl/opencap-processing/assets/43877159/4cea01ca-b89c-46d4-8c08-fd2f6d16907c) 14 | 15 | -------------------------------------------------------------------------------- /Examples/example_COM_analysis.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "\"Open" 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 | "\"Open" 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 | --------------------------------------------------------------------------------