--- a +++ b/Python_tool/MuscleParOptTool/Private_functions.py @@ -0,0 +1,393 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +Created on Thu Mar 25 15:54:09 2021 + +@author: emi +""" +# ------------------------------------------------------------------------ # +# Copyright (c) 2019 Modenese L., Ceseracciu, E., Reggiani M., Lloyd, D.G. # +# # +# Licensed under the Apache License, Version 2.0 (the "License"); # +# you may not use this file except in compliance with the License. # +# You may obtain a copy of the License at # +# http://www.apache.org/licenses/LICENSE-2.0. # +# # +# Unless required by applicable law or agreed to in writing, software # +# distributed under the License is distributed on an "AS IS" BASIS, # +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # +# implied. See the License for the specific language governing # +# permissions and limitations under the License. # +# # +# Author: Luca Modenese, January 2015 # +# email: l.modenese@imperial.ac.uk # +# ------------------------------------------------------------------------ # + +#------ import packages --------- +import numpy as np +from lxml import etree +from itertools import product +import logging + +# adding OpenSim to python script +import sys +sys.path.append('/opt/opensim-core/lib/python3.6/site-packages/') +import opensim + +# adding tool function to python script +from Public_functions import getModelJointDefinitions, \ + getChildBodyJoint, \ + getParentBodyJoint, \ + getMuscleAttachBody +#-------------------------------- + +def getJointsSpannedByMuscle(osimModel, OSMuscleName): + # Given as INPUT a muscle OSMuscleName from an OpenSim model, this function + # returns the OUTPUT list jointNameSet containing the OpenSim jointNames + # crossed by the OSMuscle. + # + # It works through the following steps: + # 1) extracts the GeometryPath + # 2) loops through the single points, determining the body they belong to + # 3) stores the bodies to which the muscle points are attached to + # 4) determines the nr of joints based on body indexes + # 5) stores the crossed OpenSim joints in the output list named jointNameSet + # + # NB this function return the crossed joints independently on the + # constraints applied to the coordinates. Eg patello-femoral is considered as a + # joint, although in Arnold's model it does not have independent + # coordinates, but it is moved in dependency of the knee flexion angle. + + # Written by Emiliano Ravera emiliano.ravera@uner.edu.ar as part of the + # Python version of work by Luca Modenese in the parameterisation of muscle + # tendon properties. + + # useful initializations + BodySet = osimModel.getBodySet() + muscle = osimModel.getMuscles().get(OSMuscleName) + + # additions BAK -> adapted by ER + # load a jointStrucute detailing bone and joint configurations + # osimModel_file = osimModel.getInputFileName() + jointStructure = getModelJointDefinitions(osimModel) + + # Extracting the PathPointSet via GeometryPath + musclePath = muscle.getGeometryPath() + musclePathPointSet = musclePath.getPathPointSet() + + # for loops to get the attachment bodies + muscleAttachBodies = [] + muscleAttachIndex = [] + + for n_point in range(0, musclePathPointSet.getSize()): + + # get the current muscle point + muscelPathPoint_name = musclePathPointSet.get(n_point).getName() + currentAttachBody = getMuscleAttachBody(osimModel, muscelPathPoint_name) + + # Initialize + if n_point == 0: + previousAttachBody = currentAttachBody + muscleAttachBodies.append(currentAttachBody) + muscleAttachIndex.append(BodySet.getIndex(currentAttachBody)) + # building a list of the bodies attached to the muscles + if currentAttachBody != previousAttachBody: + muscleAttachBodies.append(currentAttachBody) + muscleAttachIndex.append(BodySet.getIndex(currentAttachBody)) + previousAttachBody = currentAttachBody + + # end of loops to get the attacement bodies + + # From distal body checking the joint names going up until the desired + # OSJointName is found or the proximal body is reached as parent body. + DistalBodyName = muscleAttachBodies[-1] + bodyName = DistalBodyName + ProximalBodyName = muscleAttachBodies[0] + body = BodySet.get(DistalBodyName) + + spannedJointNameOld = '' + NoDofjointNameSet = [] + jointNameSet = [] + + while bodyName != ProximalBodyName: + + # BAK implementation -> adapted by ER + spannedJointName = getChildBodyJoint(jointStructure, body.getName()) + spannedJoint = osimModel.getJointSet().get(spannedJointName[0]) + + if spannedJointName == spannedJointNameOld: + # BAK implementation -> adapted by ER + body = osimModel.getBodySet().get(bodyName) + spannedJointNameOld = spannedJointName[0] + else: + if spannedJoint.numCoordinates() != 0: + jointNameSet.append(spannedJointName[0]) + else: + NoDofjointNameSet.append(spannedJointName[0]) + + spannedJointNameOld = spannedJointName[0] + bodyName = jointStructure[spannedJointName[0]]['parentBody'] + body = osimModel.getBodySet().get(bodyName) + + bodyName = body.getName() + + + if not jointNameSet: + print('ERORR: ' + 'No joint detected for muscle ' + OSMuscleName) + + if NoDofjointNameSet: + for value in NoDofjointNameSet: + print('Joint ' + value + ' has no dof.') + + varargout = NoDofjointNameSet + + + return jointNameSet, varargout + +# ---------------------------------------------------------------------------- +def getIndipCoordAndJoint(osimModel, constraint_coord_name): + # Function that given a dependent coordinate finds the independent + # coordinate and the associated joint. The function assumes that the + # constraint is a CoordinateCoupleConstraint as used by Arnold, Delp and + # LLLM. The function can be useful to manage the patellar joint for instance. + + # Input: OpenSim model objects + # Output: ind_coord_name and ind_coord_joint_name - the joint with specific constraint + + # Written by Emiliano Ravera emiliano.ravera@uner.edu.ar as part of the + # Python version of work by Luca Modenese in the parameterisation of muscle + # tendon properties. + + ind_coord_name = '' + ind_coord_joint_name = '' + # load model XML file + osimModel_filepath = osimModel.getInputFileName() + osimModel_file = etree.parse(osimModel_filepath) + model = osimModel_file.getroot() + + # double check: if not constrained then function returns + flag = [1 for constraint in model.findall('.//CoordinateCouplerConstraint') if constraint.get('name').find(constraint_coord_name)] + + if flag == []: + # print(constraint_coord_name + ' is not a constrained coordinate.') + logging.error(' ' + constraint_coord_name + ' is not a constrained coordinate.') + return ind_coord_name, ind_coord_joint_name + + # otherwise search through the constraints + for constraint in model.findall('.//CoordinateCouplerConstraint'): + + # this function assumes that the constraint will be a coordinate + # coupler contraint ( Arnold's model and LLLM uses this) + + # get dep coordinate and check if it is the coord of interest + dep_coord_name = constraint.find('dependent_coordinate_name').text + + if dep_coord_name in constraint_coord_name: + # print('WARNING: Only one indipendent coordinate is managed by the "getIndipCoordAndJoint" function yet.') + logging.warning(' Only one indipendent coordinate is managed by the "getIndipCoordAndJoint" function yet.') + + ind_coord_name = constraint.find('independent_coordinate_names').text + ind_coord_joint_name = constraint.find('independent_coordinate_names').text # assume the same name for coordinate and joint + + return ind_coord_name, ind_coord_joint_name + + +# ---------------------------------------------------------------------------- +def sampleMuscleQuantities(osimModel,OSMuscle,muscleQuant, N_EvalPoints): + # Given as INPUT an OpenSim muscle OSModel, OSMuscle, a muscle variable and a nr + # of evaluation points this function returns as + # musOutput a vector of the muscle variable of interest + # obtained by sampling the ROM of the joint spanned by the muscle in + # N_EvalPoints evaluation points. + # For multidof joint the combinations of ROMs are considered. + # For multiarticular muscles the combination of ROM are considered. + # The script is totally general because based on generating strings of code + # correspondent to the encountered code. The strings are evaluated at the end. + + # IMPORTANT 1 + # The function can decrease the N_EvalPoints if there are too many dof + # involved (ASSUMPTION is that a better sampling will be vanified by the + # huge amount of data generated). This is an option that can be controlled + # by the user by deciding if to use it or not (setting limit_discr = 0/1) + # and, in case the sampling is limited, by setting a lower limit to the + # discretization. + + # IMPORTANT 2 + # Another check is done on the dofs: only INDEPENDENT coordinate are + # considered. This is fundamental for patellofemoral joint that both in + # LLLM and Arnold's model are constrained dof, dependent on the knee + # flexion angle. This function assumes to be used with Arnold's model. + # currentState is initialState; + + # IMPORTANT 3 + # At the purposes of the muscle optimizer it is important that here the + # model is initialize every time. So there are no risk of working with an + # old state (important for Schutte muscles for instance, where we observed + # that it was necessary to re-initialize the muscle after updating the + # muscle parameters). + + # Written by Emiliano Ravera emiliano.ravera@uner.edu.ar as part of the + # Python version of work by Luca Modenese in the parameterisation of muscle + # tendon properties. + + # ======= SETTINGS ====== + # limit (1) or not (0) the discretization of the joint space sampling + limit_discr = 0 + # minimum angular discretization + min_increm_in_deg = 1 + # ======================= + + # initialize the model + currentState = osimModel.initSystem() + + # getting the joint crossed by a muscle + muscleCrossedJointSet, _ = getJointsSpannedByMuscle(osimModel, OSMuscle.getName()) + + # index for effective dofs + DOF_Index = [] + CoordinateBoundaries = [] + degIncrem = [] + + for _, curr_joint in enumerate(muscleCrossedJointSet): + # Initial estimation of the nr of Dof of the CoordinateSet for that + # joint before checking for locked and constraint dofs. + nDOF = osimModel.getJointSet().get(curr_joint).numCoordinates() + + # skip welded joint and removes welded joint from muscleCrossedJointSet + if nDOF == 0: + continue + + # calculating effective dof for that joint + effect_DOF = nDOF + for n_coord in range(0,nDOF): + # get coordinate + curr_coord = osimModel.getJointSet().get(curr_joint).get_coordinates(n_coord) + curr_coord_name = curr_coord.getName() + + # skip dof if locked + if curr_coord.getLocked(currentState): + continue + + # if coordinate is constrained then the independent coordinate and + # associated joint will be listed in the sampling "map" + if curr_coord.isConstrained(currentState) and not curr_coord.getLocked(currentState): + constraint_coord_name = curr_coord_name + # finding the independent coordinate + ind_coord_name, ind_coord_joint_name = getIndipCoordAndJoint(osimModel, constraint_coord_name) + # updating the coordinate name to be saved in the list + curr_coord_name = ind_coord_name + effect_DOF -= 1 + # ignoring constrained dof if they point to an independent + # coordinate that has already been stored + if osimModel.getCoordinateSet().getIndex(curr_coord_name) in DOF_Index: + continue + # skip dof if independent coordinate locked (the coord + # correspondent to the name needs to be extracted) + if osimModel.getCoordinateSet().get(curr_coord_name).getLocked(currentState): + continue + + # NB: DOF_Index is used later in the string generated code. + # CRUCIAL: the index of dof now is model based ("global") and + # different from the joint based used until now. + DOF_Index.append(osimModel.getCoordinateSet().getIndex(curr_coord_name)) + + # necessary update/reload the curr_coord to avoid problems with + # dependent coordinates + curr_coord = osimModel.getCoordinateSet().get(DOF_Index[-1]) + + # Getting the values defining the range + jointRange = np.zeros(2) + jointRange[0] = curr_coord.getRangeMin() + jointRange[1] = curr_coord.getRangeMax() + + # Storing range of motion conveniently + CoordinateBoundaries.append(jointRange) + + # increments in the variables when sampling the mtl space. + # Increments are different for each dof and based on N_eval. + # Defining the increments + degIncrem.append((jointRange[1] - jointRange[0]) / (N_EvalPoints-1)) + + # limit or not the discretization of the joint space sampling + # a limit to the increase can be set though + if limit_discr == 1 and degIncrem[-1] < np.radians(min_increm_in_deg): + degIncrem[-1] = np.radians(min_increm_in_deg) + + + # assigns an interval of variation following the initial and final value + # for each dof X + + # setting up for loops in order to explore all the possible combination of + # joint angles (looping on all the dofs of each joint for all the joint + # crossed by the muscle). + # The model pose is updated via: " coordToUpd.setValue(currentState,setAngleDof)" + # The right dof to update is chosen via: "coordToUpd = osimModel.getCoordinateSet.get(n_instr)" + + # generate a dictionary with CoordinateRange for each dof X. + # The dictionary keys are the DOF_Index in the model + CoordinateRange = {} + for pos, dof in enumerate(DOF_Index): + CoordinateRange[str(dof)] = np.linspace(CoordinateBoundaries[pos][0] , CoordinateBoundaries[pos][1], N_EvalPoints) + + # generate a list of dictionaries to explore all the possible combination of + # joit angle + CoordinateCombinations = [dict(zip(CoordinateRange.keys(), element)) for element in product(*CoordinateRange.values())] + + # looping on all the dofs combinations + musOutput = [None] * len(CoordinateCombinations) + + for iteration, DOF_comb in enumerate(CoordinateCombinations): + # Set the model pose + for dof_ind in DOF_comb.keys(): + coordToUpd = osimModel.getCoordinateSet().get(int(dof_ind)) + coordToUpd.setValue(currentState, CoordinateCombinations[iteration][dof_ind]) + + # calculating muscle length for the muscle + if muscleQuant == 'MTL': + musOutput[iteration] = OSMuscle.getGeometryPath().getLength(currentState) + + if muscleQuant == 'LfibNorm': + OSMuscle.setActivation(currentState,1.0) + osimModel.equilibrateMuscles(currentState) + musOutput[iteration] = OSMuscle.getNormalizedFiberLength(currentState) + + if muscleQuant == 'Lten': + OSMuscle.setActivation(currentState,1.0) + osimModel.equilibrateMuscles(currentState) + musOutput[iteration] = OSMuscle.getTendonLength(currentState) + + if muscleQuant == 'Ffib': + OSMuscle.setActivation(currentState,1.0) + osimModel.equilibrateMuscles(currentState) + musOutput[iteration] = OSMuscle.getActiveFiberForce(currentState) + + if muscleQuant == 'all': + OSMuscle.setActivation(currentState,1.0) + osimModel.equilibrateMuscles(currentState) + musOutput[iteration] = [ OSMuscle.getGeometryPath().getLength(currentState), \ + OSMuscle.getNormalizedFiberLength(currentState), \ + OSMuscle.getTendonLength(currentState), \ + OSMuscle.getActiveFiberForce(currentState), \ + OSMuscle.getPennationAngle(currentState) ] + + return musOutput + + + + + + + + + + + + + + + + + + + +