#!/usr/bin/env python # coding: utf-8 # # Introduction # # The purpose of this notebook is to check whether we get similar gain patterns from artificially created steps based on perfectly repeatable cyclic walking with random noise added to each measurement. It also gives some idea of how much we are perturbing the person by comparing the variation in the joint angles, rates, and torques in normal walking and perturbed walking. # # # Imports # In[1]: import numpy as np import pandas from IPython.core.pylabtools import figsize from gaitanalysis import gait from gaitanalysis.controlid import SimpleControlSolver # In[2]: import sys sys.path.append('../src') import utils from gait_landmark_settings import settings # In[3]: get_ipython().run_line_magic('matplotlib', 'inline') # # Load and Process Data # # Load the path to the directory with the experimental data. # In[4]: trial = utils.Trial('019') # In[5]: trial.prep_data('First Normal Walking') # In[6]: trial.prep_data('Longitudinal Perturbation') # Number of valid normal and perturbed steps. # In[7]: normal_cycles, _ = trial._remove_bad_gait_cycles('First Normal Walking') perturbed_cycles, _ = trial._remove_bad_gait_cycles('Longitudinal Perturbation') # In[8]: num_normal_cycles = normal_cycles.shape[0] num_normal_cycles # In[9]: num_perturbed_cycles = perturbed_cycles.shape[0] num_perturbed_cycles # # Compare the standard deviation in the measurements # In[10]: sensors = trial.sensors controls = trial.controls # Now compute the standard deviation of each signal across steps, to show the variation in the steps. # In[11]: normal_std = normal_cycles.std(axis='items')[sensors + controls] # In[12]: perturbed_std = perturbed_cycles.std(axis='items')[sensors + controls] # This table shows the ratio of the standard deviation of the joint angles in perturbed walking and the standard deviation of joint angles in unperturbed walking. # In[13]: ratio_std_angle = (perturbed_std / normal_std)[[s for s in sensors if s.endswith('Angle')]] ratio_std_angle # The minimum and maximum values for each angle give and idea of how much the person is being perturbed. We see a max of 2X the normal walking and lows around 1X (i.e. no change). # In[14]: ratio_std_angle.min() # In[15]: ratio_std_angle.max() # We can also look at the rates. # In[16]: ratio_std_rate = (perturbed_std / normal_std)[[s for s in sensors if s.endswith('Rate')]] ratio_std_rate.min() # In[17]: ratio_std_rate.max() # These also range 1X to 2X. # In[18]: ratio_std_torque = (perturbed_std / normal_std)[controls] ratio_std_torque.min() # In[19]: ratio_std_torque.max() # The torques have higher max values, some close to 3X. The min is still around 1X. # Samin gave me some similar numbers from here results from the 2-link inverted pendulum balancing problem. # In[20]: standing_std_no_external_perturbation = pandas.DataFrame({'Ankle.Angle': [0.0043], 'Ankle.Rate': [0.0122], 'Hip.Angle': [0.0102], 'Hip.Rate': [0.0405], 'Ankle.Torque': [50.11], 'Hip.Torque': [50.04]}) # In[21]: standing_std_min_external_perturbation = pandas.DataFrame({'Ankle.Angle': [0.0107], 'Ankle.Rate': [0.0513], 'Hip.Angle': [0.0157], 'Hip.Rate': [0.0501], 'Ankle.Torque': [54.42], 'Hip.Torque': [50.15]}) # In[22]: standing_std_better_external_perturbation = pandas.DataFrame({'Ankle.Angle': [0.0105], 'Ankle.Rate': [0.0277], 'Hip.Angle': [0.0118], 'Hip.Rate': [0.093], 'Ankle.Torque': [51.228], 'Hip.Torque': [50.07]}) # So at the bare minimum perturbation for reasonable control id with the direct method, these are the ratios of the standard deviations: # In[23]: standing_std_min_external_perturbation / standing_std_no_external_perturbation # So, we seem to have enough perturbations in the walking ID for some of the angles. The minimum perturbations in the walking are all a bit lower than these numbers, with the ankle angle and rate being the poorest. # We can visualize how variable the steps are too. # In[24]: figsize(14, 14) # In[25]: axes = trial.gait_data_objs['First Normal Walking'].plot_gait_cycles( *(sensors[:2] + controls[:2]), mean=False) # In[26]: axes = trial.gait_data_objs['Longitudinal Perturbation'].plot_gait_cycles( *(sensors[:2] + controls[:2]), mean=False) # # Create Artificial Steps # Now we will select one of the steps and build a panel of steps that based on that single step and add random noise to the columns for the sensors and controls. # In[27]: def create_artificial_steps(base_step_number): step_data_frames = [] for i in range(num_normal_cycles): # pick the 20th step df = normal_cycles.iloc[base_step_number].copy() for col in sensors + controls: # normal() doesn't say that you can pass in an array for the scale but I think it works by broadcasting. df[col] = df[col] + np.random.normal(0.0, perturbed_std[col].values, (len(df),)) step_data_frames.append(df) artificial_steps = pandas.Panel(dict(zip(range(len(step_data_frames)), step_data_frames))) return artificial_steps # In[28]: artificial_steps = create_artificial_steps(20) # In[29]: axes = gait.plot_gait_cycles(artificial_steps, *(sensors[:2] + controls[:2])) # # Gains identified from normal walking # In[30]: trial.identify_controller('First Normal Walking', 'joint isolated') # In[31]: figsize(10, 8) # In[32]: fig, axes = trial.plot_joint_isolated_gains('First Normal Walking', 'joint isolated') # # Gains identified from perturbed walking # In[33]: trial.identify_controller('Longitudinal Perturbation', 'joint isolated') # In[34]: fig, axes = trial.plot_joint_isolated_gains('Longitudinal Perturbation', 'joint isolated') # # Gains Identified from Artificial Steps # In[35]: solver = SimpleControlSolver(create_artificial_steps(20), sensors, controls) # In[36]: gain_inclusion_matrix = np.zeros((len(controls), len(sensors))).astype(bool) for i, row in enumerate(gain_inclusion_matrix): row[2 * i:2 * i + 2] = True # In[37]: artificial_result = solver.solve(gain_inclusion_matrix=gain_inclusion_matrix) # In[38]: fig, axes = utils.plot_joint_isolated_gains(trial.sensors, trial.controls, artificial_result[0], artificial_result[3]) # Solve for the gains from the same base step but with different noise applied to ensure we get different results than the first. # In[39]: solver = SimpleControlSolver(create_artificial_steps(20), sensors, controls) # In[40]: artificial_result = solver.solve(gain_inclusion_matrix=gain_inclusion_matrix) # In[41]: fig, axes = utils.plot_joint_isolated_gains(trial.sensors, trial.controls, artificial_result[0], artificial_result[3]) # So this is good news. We are not able to identifying the same gains from this artificial data. And the gains have no apparent, repeatable patterns. It also seems to give gains of zero, which is what you may expect from this atificial data. # # Conclusion # This seems to to show that the gain patterns we get from the real data, both normal walking and perturbed walking, are not some artifact of the nominal cyclic motion and the mathematical method. It may be the case that walking without the longitudinal perturbations is still internally perturbed enough for us to extract some reasonable gains for the controller. This also indicates that the recoveries from perturbations are likely not random, because we would not see the repeatable results in the controller identification. # # Footer # In[42]: get_ipython().system('git rev-parse HEAD') # In[43]: get_ipython().system('git --git-dir=/home/moorepants/src/GaitAnalysisToolKit/.git --work-tree=/home/moorepants/src/GaitAnalysisToolKit rev-parse HEAD') # In[44]: get_ipython().run_line_magic('install_ext', 'http://raw.github.com/jrjohansson/version_information/master/version_information.py') # In[45]: get_ipython().run_line_magic('load_ext', 'version_information') # In[46]: get_ipython().run_line_magic('version_information', 'numpy, scipy, pandas, matplotlib, tables, oct2py, dtk, gaitanalysis') # In[47]: get_ipython().system('conda list') # In[48]: get_ipython().system('pip freeze')