function checkGuidelines(residualFile,kinematicsFile,grfFile,timeRange,allowableZMPdistance,allowableFreeMoment_pErr,allowableForce_pErr) % Purpose: This function tests if the simulation residuals satisfy the guidelines of Gupta et al. % This function is designed to test residuals from OpenSim simulations, users can modify it based on the software package they use. % % Inputs: % residualFile: Name of the file that contains the residuals (preferably .sto or .mot) % First column should be time % The residuals should be labeled FX, FY, FZ, MX, MY and MZ % kinematicsFile: Name of the file that contains the simulation kinematics (preferably .sto or .mot) % First column should be time % The pelvis coordinates should be labeled pelvis_tx, pelvis_ty and pelvis_tz % grfFile: Name of the file that contains the experimenatal ground reaction forces (GRF) (preferably .sto or .mot) % First column should be time % For each force plate, the order of appearance of the elements should be Forces, Center of pressure (COP) and moments % This code only supports a maximum of 2 force plates. Labels of one of the force plates should start with '1_' % timeRange: Time range over which to test if the residuals follow the Gupta et al. guidelines. % Input should be a 1x2 matrix of the form [startTime endTime] % allowableZMPdistance: User defined maximum allowable distance between ZMP and COP (in cm) [Value of d (Gupta et al.)] % allowableFreeMoment_pErr: User defined maximum allowable percent error between ZMP free moment and GRF experimental GRF free moment[Value of b (Gupta et al.)] % allowableForce_pErr: User defined maximum allowable percent error between ZMP forces and experimental GRF forces [Value of a (Gupta et al.)] % Input should be a 1x3 matrix of the form [aX aY aZ] % % Outputs: A single visual that displays 4 plots: % 1. Trajectories of ZMP and COP, with the allowable area for ZMP (Gupta et al.) % 2. Trajectories of ZMP free moment and experimental free moment with the allowable range for ZMP free moment (Gupta et al.) % 3. Where during the movement each of the guidelines are violated. % 4. Experimenat vertical GRF. % % Credits: This function was written by Dhruv Gupta %% spline all data to 0 to 100% of movement and extract relevant data % Set time vector from 0 to 100% splineTimeStep=(timeRange(1,2)-timeRange(1,1))/100; Time=[timeRange(1,1):splineTimeStep:timeRange(1,2)]'; timeVector=[0:100]'; % read and spline the GRF GRF=read_motionFile(grfFile); for c=1:GRF.nc GRFdata(:,c)=spline(GRF.data(:,1),GRF.data(:,c),Time); end % extract data of each force plate separately c1=1; c2=1; GRF1=zeros(101,9); GRF2=zeros(101,9); for c=1:GRF.nc if strfind(GRF.labels{c},'1_') GRF2(:,c2)=GRFdata(:,c); c2=c2+1; elseif strcmp(GRF.labels{c},'time') else GRF1(:,c1)=GRFdata(:,c); c1=c1+1; end end % read and spline simulation kinematics ikMotion = read_motionFile(kinematicsFile); for c=1:ikMotion.nc Motion(:,c)=spline(ikMotion.data(:,1),ikMotion.data(:,c),Time); end % extract the pelvis kinematics positionPelvis = zeros(101, 3); for c = 1:ikMotion.nc if(strcmp(char(ikMotion.labels(c)), 'pelvis_tx')) positionPelvis(:, 1) = Motion(:, c); elseif(strcmp(char(ikMotion.labels(c)), 'pelvis_ty')) positionPelvis(:, 2) = Motion(:, c); elseif(strcmp(char(ikMotion.labels(c)), 'pelvis_tz')) positionPelvis(:, 3) = Motion(:, c); end end % read and spline residuals Residuals = read_motionFile(residualFile); for c=1:Residuals.nc residualData(:,c)=spline(Residuals.data(:,1),Residuals.data(:,c),Time); end % extract simulation residusals residualForces = zeros(101, 3); for c = 1:Residuals.nc if(strcmp(char(Residuals.labels(c)), 'FX')) residualForces(:, 1) = residualData(:, c); elseif(strcmp(char(Residuals.labels(c)), 'FY')) residualForces(:, 2) = residualData(:, c); elseif(strcmp(char(Residuals.labels(c)), 'FZ')) residualForces(:, 3) = residualData(:, c); end end residualMoments = zeros(101, 3); for c = 1:Residuals.nc if(strcmp(char(Residuals.labels(c)), 'MX')) residualMoments(:, 1) = residualData(:, c); elseif(strcmp(char(Residuals.labels(c)), 'MY')) residualMoments(:, 2) = residualData(:, c); elseif(strcmp(char(Residuals.labels(c)), 'MZ')) residualMoments(:, 3) = residualData(:, c); end end %% find set of forces and moments equivalent to experimental GRF applied at pelvis [FYp and MYp (Gupta et al.)] idealPelvisForces=GRF1(:,1:3)+GRF2(:,1:3); idealPelvisMoments=GRF1(:,7:9)+GRF2(:,7:9)+cross(GRF1(:,4:6),GRF1(:,1:3))+cross(GRF2(:,4:6),GRF2(:,1:3))-cross(positionPelvis,idealPelvisForces); %% find the resultant experimental GRF, that is, net effect of both force plates. resultantGRF(:,1:3)=GRF1(:,1:3)+GRF2(:,1:3); resultantGRF(:,4:6)=((GRF1(:,4:6).*GRF1(:,2))+(GRF2(:,4:6).*GRF2(:,2)))./resultantGRF(:,2); resultantGRF(:,7:9)=GRF1(:,7:9)+GRF2(:,7:9)+cross(GRF1(:,4:6),GRF1(:,1:3))+cross(GRF2(:,4:6),GRF2(:,1:3))-cross(resultantGRF(:,4:6),resultantGRF(:,1:3)); %% Extract the net COP and GRF free moment, based on both force plates. And establish force and free moment thresholds (Gupta et al.) COP_XZ=[resultantGRF(:,4) resultantGRF(:,6)]; experimentalFreeMoment=resultantGRF(:,8); freeMomentLimit=(allowableFreeMoment_pErr/100)*max(abs(experimentalFreeMoment)); forceLimits=(allowableForce_pErr/100).*max(abs(resultantGRF(:,1:3))); %% ZMP computations to find the ZMP forces, ZMP and ZMP free moment and test if they violate any the guidelines (Gupta et al.) errForces=residualForces>forceLimits; violateForceGuideline = ~~sum(errForces,2); forceGroundToPelvis_P = idealPelvisForces + residualForces; momentGroundToPelvis_P = idealPelvisMoments + residualMoments; forceGroundToPelvis_G = forceGroundToPelvis_P; momentGroundToPelvis_G = momentGroundToPelvis_P + cross(positionPelvis, forceGroundToPelvis_P); ZMP_point_XZ(:,1) = momentGroundToPelvis_G(:, 3) ./ forceGroundToPelvis_G(:, 2); % X_ZMP ZMP_point_XZ(:,2) = -momentGroundToPelvis_G(:, 1) ./ forceGroundToPelvis_G(:, 2); % Z_ZMP ZMP_freeMoment = momentGroundToPelvis_G(:, 2) - (forceGroundToPelvis_G(:, 1) .* ZMP_point_XZ(:,2)) + (forceGroundToPelvis_G(:, 3) .* ZMP_point_XZ(:,1)); % freeMoment_ZMP distZMPCOP=100*(sqrt(sum(((ZMP_point_XZ-COP_XZ).^2),2))); % distance between ZMP and COP in cm violateZMPpointGuideline=distZMPCOP>allowableZMPdistance; errFreeMoment = abs(ZMP_freeMoment - experimentalFreeMoment); % error in freeMoment of ZMP violateFreeMomentGuideline=errFreeMoment>freeMomentLimit; violateGuideline = ~~(violateForceGuideline + violateZMPpointGuideline + violateFreeMomentGuideline); %% Visual to display results pauseTime=mean(diff(Time)); for fr=1:size(Time,1) % Trajectories of ZMP and COP, with the allowable area for ZMP (Gupta et al.) subplot(2,2,1) hold off plot(COP_XZ(:,2),COP_XZ(:,1),'k','LineWidth',1.5) hold on plot(ZMP_point_XZ(:,2),ZMP_point_XZ(:,1),'b','LineWidth',1.5) hold on circle(resultantGRF(fr,6),resultantGRF(fr,4),allowableZMPdistance/100); hold on if violateZMPpointGuideline(fr,1)==1 scatter(ZMP_point_XZ(fr,2),ZMP_point_XZ(fr,1),'r') else scatter(ZMP_point_XZ(fr,2),ZMP_point_XZ(fr,1),'g') end hold on scatter(COP_XZ(fr,2),COP_XZ(fr,1),'kx') hold off legend('COP','ZMP') xlim([min([min(COP_XZ(:,2))-0.2 min(ZMP_point_XZ(:,2))-0.2]) max([max(COP_XZ(:,2))+0.2 max(ZMP_point_XZ(:,2))+0.2])]) ylim([min([min(COP_XZ(:,1))-0.2 min(ZMP_point_XZ(:,1))-0.2]) max([max(COP_XZ(:,1))+0.2 max(ZMP_point_XZ(:,1))+0.2])]) axis equal xticks({}) yticks({}) xlabel('Z direction -->') ylabel('X direction -->') title('Trajectories of COP vs ZMP') % Trajectories of ZMP free moment and experimental free moment with the allowable range for ZMP free moment (Gupta et al.) subplot(2,2,2) hold off plot(timeVector(:,1),experimentalFreeMoment,'k','LineWidth',1.5) hold on plot(timeVector(:,1),ZMP_freeMoment,'b','LineWidth',1.5) hold on plot(timeVector(:,1),(experimentalFreeMoment+freeMomentLimit),'--k') hold on plot(timeVector(:,1),(experimentalFreeMoment-freeMomentLimit),'--k') hold on scatter(timeVector(fr,1),experimentalFreeMoment(fr,1),'k') hold on if violateFreeMomentGuideline(fr,1)==1 scatter(timeVector(fr,1),ZMP_freeMoment(fr,1),'r') else scatter(timeVector(fr,1),ZMP_freeMoment(fr,1),'g') end legend('Experimental free moment','ZMP free momemt') xlim([0 100]) ylim([min([min(experimentalFreeMoment)-15 min(ZMP_freeMoment)-15]) max([max(experimentalFreeMoment)+15 max(ZMP_freeMoment)+15])]) xlabel('Time (% Movement)') ylabel('Free moment (Nm)') title('Experimental vs ZMP free moment') % Where during the movement each of the guidelines are violated. subplot(2,2,3) hold off plot(timeVector(:,1),0.25*violateForceGuideline,'k:') hold on plot(timeVector(:,1),0.5*violateZMPpointGuideline,'k-.') hold on plot(timeVector(:,1),0.75*violateFreeMomentGuideline,'k--') hold on plot(timeVector(:,1),violateGuideline,'k','LineWidth',1.5) hold on scatter(timeVector(fr,1),0.5*violateZMPpointGuideline(fr,1),'k') hold on scatter(timeVector(fr,1),0.75*violateFreeMomentGuideline(fr,1),'k') hold on scatter(timeVector(fr,1),0.25*violateForceGuideline(fr,1),'k') hold on scatter(timeVector(fr,1),violateGuideline(fr,1),'k') hold off xlim([0 100]) ylim([0 1.6]) yticks([0.25 0.5 0.75 1]) yticklabels({'Force','ZMP','Free moment','Any violation'}) legend('Force guideline','ZMP guideline','Free moment guideline','Any guideline') xlabel('Time (% Movement)') ylabel('Guideline violated') title('Time of violation of guidelines') % Experimenat vertical GRF. subplot(2,2,4) hold off plot(timeVector(:,1),GRF1(:,2),'c') hold on plot(timeVector(:,1),GRF2(:,2),'m') hold on plot(timeVector(:,1),GRF1(:,2)+GRF2(:,2),'k') hold on scatter(timeVector(fr,1),GRF1(fr,2),'c') hold on scatter(timeVector(fr,1),GRF2(fr,2),'m') hold on scatter(timeVector(fr,1),GRF1(fr,2)+GRF2(fr,2),'k') xlim([0 100]) ylim([0 max(resultantGRF(:,2))+200]) hold off legend({'Force plate 1','Force plate 2','Total'}); xlabel('Time (% Movement)') ylabel('Force (N)') title('Experimental vertical ground reaction force') pause(pauseTime) % pause(0.001) end end function q = read_motionFile(fname) % Purpose: This function reads a file in the format of a SIMM motion file % and returns a data structure % % Input: fname is the name of the ascii datafile to be read % ('character array') % % Output: q returns a structure with the following format: % q.labels = array of column labels % q.data = matrix of data % q.nr = number of matrix rows % q.nc = number of matrix columns % % ASA 12/03 % Modified by Eran Guendelman 09/06 % Open ascii data file for reading. fid = fopen(fname, 'r'); if fid == -1 error(['unable to open ', fname]) end % Process the file header; % store # data rows, # data columns. q.nr = 0; % Added to ensure that the q structures from reading a motion file q.nc = 0; % are always the same, even if nr and nc are different orders in file. nextline = fgetl(fid); while ~strncmpi(nextline, 'endheader', length('endheader')) if strncmpi(nextline, 'datacolumns', length('datacolumns')) q.nc = str2num(nextline(findstr(nextline, ' ')+1 : length(nextline))); elseif strncmpi(nextline, 'datarows', length('datarows')) q.nr = str2num(nextline(findstr(nextline, ' ')+1 : length(nextline))); elseif strncmpi(nextline, 'nColumns', length('nColumns')) q.nc = str2num(nextline(findstr(nextline, '=')+1 : length(nextline))); elseif strncmpi(nextline, 'nRows', length('nRows')) q.nr = str2num(nextline(findstr(nextline, '=')+1 : length(nextline))); end nextline = fgetl(fid); end % Process the column labels. nextline = fgetl(fid); if (all(isspace(nextline))) % Blank line, so the next one must be the one containing the column labels nextline = fgetl(fid); end q.labels = cell(1, q.nc); for j = 1:q.nc [q.labels{j}, nextline] = strtok(nextline); end % Process the data. % Note: transpose is needed since fscanf fills columns before rows. q.data = fscanf(fid, '%f', [q.nc, q.nr])'; fclose(fid); return; end function h = circle(x,y,r) % plot a circle hold on th = 0:pi/50:2*pi; xunit = r * cos(th) + x; yunit = r * sin(th) + y; h = plot(xunit, yunit ,'--'); set(h,'Color', 'k'); hold off end