Pushrim Kinetics

The pushrimkinetics module allows processing kinetics data from instrumented wheelchair wheels such as the SmartWheel.

[1]:
import kineticstoolkit.lab as ktk
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

Read data from file

The first step is to load data from a file, using the ktk.pushrimkinetics.read_file() function.

[2]:
filename = ('data/pushrimkinetics/'
            'sample_swl_overground_propulsion_withrubber.csv')

kinetics = ktk.pushrimkinetics.read_file(filename, file_format='smartwheel')

Let see what we loaded:

[3]:
kinetics
[3]:
TimeSeries with attributes:
             data: <dict with 5 entries>,
        data_info: <dict with 4 entries>,
           events: <list of 0 items>,
             time: <array of shape (7682,)>,
        time_info: <dict with 1 entries>
[4]:
kinetics.data
[4]:
{
         'Angle': <array of shape (7682,)>,
      'Channels': <array of shape (7682, 6)>,
        'Forces': <array of shape (7682, 4)>,
         'Index': <array of shape (7682,)>,
       'Moments': <array of shape (7682, 4)>
}
[5]:
plt.subplot(2, 1, 1)
kinetics.plot('Forces')
plt.subplot(2, 1, 2)
kinetics.plot('Moments')
plt.tight_layout()
_images/pushrimkinetics_7_0.png

Calculate forces and moments

If the loaded data doesn’t include forces and moments but only raw data (for exemple, when loading data from a SmartWheel’s SD card), we must calculate the forces and moments based on a calibration matrix. The function ktk.pushrimkinetics.calculate_forces_and_moments() performs this calculation. In this example, we use a calibration matrix that is included in ktk.pushrimkinetics.CALIBRATION_MATRICES, and we express the calculated forces and moments in a reference frame that is orthogonal to the ground (levelled using the wheel’s angular encoder).

[6]:
calibration_matrices = ktk.pushrimkinetics.CALIBRATION_MATRICES['SmartWheel_123']
calibration_matrices
[6]:
{
           'gains': <array of shape (6,)>,
         'offsets': <array of shape (6,)>,
      'transducer': 'smartwheel'
}
[7]:
new_kinetics = ktk.pushrimkinetics.calculate_forces_and_moments(
    kinetics,
    gains=calibration_matrices['gains'],
    offsets=calibration_matrices['offsets'],
    transducer='smartwheel',
    reference_frame='hub')

plt.subplot(2, 1, 1)
new_kinetics.plot('Forces')
plt.subplot(2, 1, 2)
new_kinetics.plot('Moments')
plt.tight_layout()
_images/pushrimkinetics_10_0.png

We observe some sign differences here. In fact, the SmartWheel softwares inverts some signals based on the SmartWheel’s sign convention and the side of the wheel. When the forces and moments are reconstructed from raw data, these sign changes do not happen, which explains these discrepancies.

Removing dynamic offsets in forces and moments

We observe in the last graphs that dynamic (sinusoidal) offsets are presents mostly in the forces but also in the moments. We can auto-remove these offsets using ktk.pushrimkinetics.remove_offsets().

Let’s apply this function on the data we just loaded.

[8]:
plt.subplot(2, 1, 1)
kinetics.plot('Forces')
plt.title('Before removing offsets')
plt.tight_layout()
_images/pushrimkinetics_13_0.png
[9]:
kinetics = ktk.pushrimkinetics.remove_offsets(kinetics)

plt.subplot(2, 1, 2)
kinetics.plot('Forces')
plt.title('After removing offsets')
plt.tight_layout()
_images/pushrimkinetics_14_0.png

This automatic method has only be validated for straight-line, level-ground propulsion. For any other condition, a baseline trial is required. A baseline trial is a trial where an operator pushes the wheelchair but no external force appart from gravity is applied on the instrumented wheel. Please consult the ktk.pushrimkinetics.remove_offsets help for more information.

Calculate velocity and power

Thee wheel velocity is calculated from the wheel angle with a derivative Savitsky-Golay filter, using the ktk.pushrimkinetics.calculate_velocity() function. Once the velocity has been calculated, the output power can also be calculated by multiplying the velocity by the propulsion moment, using the ktk.pushrimkinetics.calculate_power() function.

[10]:
kinetics = ktk.pushrimkinetics.calculate_velocity(kinetics)
kinetics = ktk.pushrimkinetics.calculate_power(kinetics)

plt.subplot(2, 1, 1)
kinetics.plot('Velocity')
plt.subplot(2, 1, 2)
kinetics.plot('Power')
plt.tight_layout()
_images/pushrimkinetics_17_0.png

Detecting the pushes

The kineticstoolkit.cycles module provides powerful tools to detect and manage cycles. Here we use ktk.cycles.detect_cycles to detect the pushes using these specifications: - a push starts when the total force crosses 5 N upward; - a push ends when the total force crosses 2 N downward; - for a push to be valid, it must last at least 100 ms; - to be valid, the total force must reach 25 N.

[11]:
# Create the total force data
kinetics.data['Ftot'] = np.sqrt(np.sum(kinetics.data['Forces'] ** 2, axis=1))

kinetics = ktk.cycles.detect_cycles(
    kinetics, 'Ftot',
    event_name1='push',
    event_name2='recovery',
    threshold1=5.0,
    threshold2=2.0,
    min_duration1=0.1,
    min_peak_height1=25.0
)

kinetics.plot('Forces')
plt.tight_layout()
_images/pushrimkinetics_19_0.png

Extracting spatiotemporal and kinetic parameters

As a conclusion to this tutorial, we will now extract some key spatiotemporal and kinetic parameters from these data, and express those parameters as a pandas DataFrame. Obviously, this is only an example and many other parameters can be calculated using a similar procedure.

[12]:
n_cycles = 15  # Number of cycles to analyze

records = []  # Init a list that will contains the results of the analysis

for i_cycle in range(n_cycles):

    # Get a TimeSeries that spans only the push i_push
    ts_push = kinetics.get_ts_between_events('push', 'recovery', i_cycle, i_cycle)

    # Get a TimeSeries that spans the entire cycle i_push
    ts_cycle = kinetics.get_ts_between_events('push', '_', i_cycle, i_cycle)

    # Get some spatiotemporal parameters
    push_time = ts_push.time[-1] - ts_push.time[0]
    cycle_time = ts_cycle.time[-1] - ts_cycle.time[0]
    recovery_time = cycle_time - push_time

    push_angle = ts_push.data['Angle'][-1] - ts_push.data['Angle'][0]

    # Get some kinetic parameters
    propulsion_moment_mean = np.mean(ts_push.data['Moments'][:, 2])
    propulsion_moment_max = np.max(ts_push.data['Moments'][:, 2])

    total_force_mean = np.mean(ts_push.data['Ftot'])
    total_force_max = np.max(ts_push.data['Ftot'])

    # Record this information in the records list
    records.append({
        'Push time (s)': push_time,
        'Recovery time (s)': recovery_time,
        'Cycle time (s)': cycle_time,
        'Push angle (deg)': np.rad2deg(push_angle),
        'Mean propulsion moment (Nm)': propulsion_moment_mean,
        'Max propulsion moment (Nm)': propulsion_moment_max,
        'Mean total force (N)': total_force_mean,
        'Max total force (N)': total_force_max,
    })

# Create and show a DataFrame of this information
df = pd.DataFrame.from_dict(records)

# Copy the dataframe to the clipboard for pasting into Excel (facultative)
df.to_clipboard()

# Print the dataframe here
df
[12]:
Push time (s) Recovery time (s) Cycle time (s) Push angle (deg) Mean propulsion moment (Nm) Max propulsion moment (Nm) Mean total force (N) Max total force (N)
0 1.404167 0.358333 1.762500 75.85 9.106452 12.716142 34.117531 44.257457
1 0.654167 0.579167 1.233333 88.33 6.992656 11.005038 30.669055 47.699743
2 0.554167 0.583333 1.137500 88.51 6.244814 9.327931 25.279054 37.581425
3 0.495833 0.633333 1.129167 79.80 4.328190 7.851847 23.573272 39.199059
4 0.533333 0.629167 1.162500 90.70 5.271423 8.831854 22.133097 40.147530
5 0.587500 0.675000 1.262500 97.12 5.611598 8.544609 25.567489 41.677356
6 0.550000 0.570833 1.120833 93.60 4.906104 7.642254 23.327076 37.582148
7 0.537500 0.637500 1.175000 90.00 5.233192 7.801526 23.085664 36.135763
8 0.487500 0.654167 1.141667 86.13 4.663521 6.557192 25.296090 40.663234
9 0.479167 0.658333 1.137500 87.18 3.506395 5.401392 21.393315 35.221362
10 0.491667 0.591667 1.083333 87.63 4.315006 6.866585 21.510567 35.246447
11 0.525000 0.637500 1.162500 93.87 4.903095 8.089712 22.627376 36.680779
12 0.491667 0.650000 1.141667 85.43 3.821445 6.286805 20.963591 35.007896
13 0.550000 0.608333 1.158333 95.36 4.967660 7.923602 24.124500 38.413724
14 0.508333 0.641667 1.150000 90.18 4.261794 6.951129 21.341208 38.651713

For more information, please check the API Reference for the pushrimkinetics module.