worklab 2.0.0__tar.gz → 2.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {worklab-2.0.0 → worklab-2.1.0}/LICENSE +2 -2
- {worklab-2.0.0 → worklab-2.1.0}/PKG-INFO +15 -11
- {worklab-2.0.0 → worklab-2.1.0}/README.md +4 -4
- {worklab-2.0.0 → worklab-2.1.0}/pyproject.toml +9 -6
- {worklab-2.0.0 → worklab-2.1.0}/worklab/ana.py +94 -1
- {worklab-2.0.0 → worklab-2.1.0}/worklab/com.py +33 -10
- {worklab-2.0.0 → worklab-2.1.0}/worklab/imu.py +31 -22
- {worklab-2.0.0 → worklab-2.1.0}/worklab/kin.py +100 -54
- {worklab-2.0.0 → worklab-2.1.0}/worklab/plots.py +11 -11
- {worklab-2.0.0 → worklab-2.1.0}/worklab/utils.py +1 -1
- {worklab-2.0.0 → worklab-2.1.0}/worklab.egg-info/PKG-INFO +15 -11
- {worklab-2.0.0 → worklab-2.1.0}/worklab.egg-info/requires.txt +3 -0
- {worklab-2.0.0 → worklab-2.1.0}/setup.cfg +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/tests/test_utils.py +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab/__init__.py +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab/move.py +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab/physio.py +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab.egg-info/SOURCES.txt +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab.egg-info/dependency_links.txt +0 -0
- {worklab-2.0.0 → worklab-2.1.0}/worklab.egg-info/top_level.txt +0 -0
|
@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
|
|
632
632
|
the "copyright" line and a pointer to where the full notice is found.
|
|
633
633
|
|
|
634
634
|
Analysis
|
|
635
|
-
Copyright (C) 2018
|
|
635
|
+
Copyright (C) 2018 Sophie de Klerk
|
|
636
636
|
|
|
637
637
|
This program is free software: you can redistribute it and/or modify
|
|
638
638
|
it under the terms of the GNU General Public License as published by
|
|
@@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail.
|
|
|
652
652
|
If the program does terminal interaction, make it output a short
|
|
653
653
|
notice like this when it starts in an interactive mode:
|
|
654
654
|
|
|
655
|
-
Analysis Copyright (C) 2018
|
|
655
|
+
Analysis Copyright (C) 2018 Sophie de Klerk
|
|
656
656
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
657
657
|
This is free software, and you are welcome to redistribute it
|
|
658
658
|
under certain conditions; type `show c' for details.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: worklab
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Basic scripts for worklab devices
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Sophie de Klerk <r.de.klerk@pl.hanze.nl>, Thomas Rietveld <t.rietveld@lboro.ac.uk>, Rowie Janssen <r.j.f.janssen@umcg.nl>, Jelmer Braaksma <j.braaksma01@umcg.nl>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
7
7
|
Version 3, 29 June 2007
|
|
8
8
|
|
|
@@ -637,7 +637,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
637
637
|
the "copyright" line and a pointer to where the full notice is found.
|
|
638
638
|
|
|
639
639
|
Analysis
|
|
640
|
-
Copyright (C) 2018
|
|
640
|
+
Copyright (C) 2018 Sophie de Klerk
|
|
641
641
|
|
|
642
642
|
This program is free software: you can redistribute it and/or modify
|
|
643
643
|
it under the terms of the GNU General Public License as published by
|
|
@@ -657,7 +657,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
657
657
|
If the program does terminal interaction, make it output a short
|
|
658
658
|
notice like this when it starts in an interactive mode:
|
|
659
659
|
|
|
660
|
-
Analysis Copyright (C) 2018
|
|
660
|
+
Analysis Copyright (C) 2018 Sophie de Klerk
|
|
661
661
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
662
662
|
This is free software, and you are welcome to redistribute it
|
|
663
663
|
under certain conditions; type `show c' for details.
|
|
@@ -678,8 +678,8 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
678
678
|
Public License instead of this License. But first, please read
|
|
679
679
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
680
680
|
|
|
681
|
-
Project-URL: Homepage, https://github.com/
|
|
682
|
-
Project-URL: Bug Reports, https://github.com/
|
|
681
|
+
Project-URL: Homepage, https://github.com/sophiedkk/worklab
|
|
682
|
+
Project-URL: Bug Reports, https://github.com/sophiedkk/worklab/issues
|
|
683
683
|
Keywords: biomechanics,ergometry,physiology
|
|
684
684
|
Classifier: Intended Audience :: Science/Research
|
|
685
685
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
@@ -693,16 +693,20 @@ Requires-Dist: numpy
|
|
|
693
693
|
Requires-Dist: pandas
|
|
694
694
|
Requires-Dist: matplotlib
|
|
695
695
|
Requires-Dist: xlrd
|
|
696
|
+
Requires-Dist: scikit-learn
|
|
697
|
+
Requires-Dist: seaborn
|
|
696
698
|
Provides-Extra: dev
|
|
697
699
|
Requires-Dist: pytest; extra == "dev"
|
|
698
700
|
Requires-Dist: Flake8-pyproject; extra == "dev"
|
|
699
701
|
Requires-Dist: black; extra == "dev"
|
|
700
702
|
Requires-Dist: jupyter-book; extra == "dev"
|
|
703
|
+
Requires-Dist: build; extra == "dev"
|
|
704
|
+
Dynamic: license-file
|
|
701
705
|
|
|
702
706
|
# Worklab: a wheelchair biomechanics mini-package
|
|
703
707
|
|
|
704
708
|
[](https://doi.org/10.5281/zenodo.8362962)
|
|
705
|
-
[](https://badge.fury.io/py/worklab) [](https://
|
|
709
|
+
[](https://badge.fury.io/py/worklab) [](https://github.com/sophiedkk/worklab/blob/main/LICENSE)
|
|
706
710
|
|
|
707
711
|
Essential data analysis and (pre-)processing scripts used in projects
|
|
708
712
|
researching the Lode
|
|
@@ -722,7 +726,7 @@ the worklab, which means:
|
|
|
722
726
|
# Documentation
|
|
723
727
|
|
|
724
728
|
For more detailed documentation you can look at the
|
|
725
|
-
[docs](https://
|
|
729
|
+
[docs](https://sophiedkk.github.io/worklab/).
|
|
726
730
|
|
|
727
731
|
# Prerequisites
|
|
728
732
|
|
|
@@ -739,7 +743,7 @@ You can install this package with pip:
|
|
|
739
743
|
# Examples
|
|
740
744
|
|
|
741
745
|
You can find some Jupyter Notebook examples
|
|
742
|
-
[here](https://
|
|
746
|
+
[here](https://sophiedkk.github.io/worklab/chapters/examples.html).
|
|
743
747
|
|
|
744
748
|
# Reporting errors
|
|
745
749
|
|
|
@@ -751,4 +755,4 @@ me or submit an issue through this page.
|
|
|
751
755
|
If you want to refer to this package please use this DOI:
|
|
752
756
|
10.5281/zenodo.8362962, or cite:
|
|
753
757
|
|
|
754
|
-
|
|
758
|
+
Sophie de Klerk, Thomas Rietveld, Rowie Janssen, & Jelmer Braaksma. (2023). Worklab: a wheelchair biomechanics mini-package. Zenodo. https://doi.org/10.5281/zenodo.8362963
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Worklab: a wheelchair biomechanics mini-package
|
|
2
2
|
|
|
3
3
|
[](https://doi.org/10.5281/zenodo.8362962)
|
|
4
|
-
[](https://badge.fury.io/py/worklab) [](https://
|
|
4
|
+
[](https://badge.fury.io/py/worklab) [](https://github.com/sophiedkk/worklab/blob/main/LICENSE)
|
|
5
5
|
|
|
6
6
|
Essential data analysis and (pre-)processing scripts used in projects
|
|
7
7
|
researching the Lode
|
|
@@ -21,7 +21,7 @@ the worklab, which means:
|
|
|
21
21
|
# Documentation
|
|
22
22
|
|
|
23
23
|
For more detailed documentation you can look at the
|
|
24
|
-
[docs](https://
|
|
24
|
+
[docs](https://sophiedkk.github.io/worklab/).
|
|
25
25
|
|
|
26
26
|
# Prerequisites
|
|
27
27
|
|
|
@@ -38,7 +38,7 @@ You can install this package with pip:
|
|
|
38
38
|
# Examples
|
|
39
39
|
|
|
40
40
|
You can find some Jupyter Notebook examples
|
|
41
|
-
[here](https://
|
|
41
|
+
[here](https://sophiedkk.github.io/worklab/chapters/examples.html).
|
|
42
42
|
|
|
43
43
|
# Reporting errors
|
|
44
44
|
|
|
@@ -50,4 +50,4 @@ me or submit an issue through this page.
|
|
|
50
50
|
If you want to refer to this package please use this DOI:
|
|
51
51
|
10.5281/zenodo.8362962, or cite:
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
Sophie de Klerk, Thomas Rietveld, Rowie Janssen, & Jelmer Braaksma. (2023). Worklab: a wheelchair biomechanics mini-package. Zenodo. https://doi.org/10.5281/zenodo.8362963
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worklab"
|
|
3
|
-
version = "2.
|
|
3
|
+
version = "2.1.0"
|
|
4
4
|
description = "Basic scripts for worklab devices"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.7"
|
|
7
7
|
license = {file = "LICENSE"}
|
|
8
8
|
keywords = ["biomechanics", "ergometry", "physiology"]
|
|
9
9
|
authors = [
|
|
10
|
-
{name = "
|
|
10
|
+
{name = "Sophie de Klerk", email = "r.de.klerk@pl.hanze.nl"},
|
|
11
11
|
{name = "Thomas Rietveld", email = "t.rietveld@lboro.ac.uk"},
|
|
12
12
|
{name = "Rowie Janssen", email = "r.j.f.janssen@umcg.nl"},
|
|
13
13
|
{name = "Jelmer Braaksma", email = "j.braaksma01@umcg.nl"}
|
|
@@ -23,7 +23,9 @@ dependencies = [
|
|
|
23
23
|
"numpy",
|
|
24
24
|
"pandas",
|
|
25
25
|
"matplotlib",
|
|
26
|
-
"xlrd"
|
|
26
|
+
"xlrd",
|
|
27
|
+
"scikit-learn",
|
|
28
|
+
"seaborn"
|
|
27
29
|
]
|
|
28
30
|
|
|
29
31
|
[project.optional-dependencies]
|
|
@@ -31,12 +33,13 @@ dev = [
|
|
|
31
33
|
"pytest",
|
|
32
34
|
"Flake8-pyproject",
|
|
33
35
|
"black",
|
|
34
|
-
"jupyter-book"
|
|
36
|
+
"jupyter-book",
|
|
37
|
+
"build"
|
|
35
38
|
]
|
|
36
39
|
|
|
37
40
|
[project.urls]
|
|
38
|
-
"Homepage" = "https://github.com/
|
|
39
|
-
"Bug Reports" = "https://github.com/
|
|
41
|
+
"Homepage" = "https://github.com/sophiedkk/worklab"
|
|
42
|
+
"Bug Reports" = "https://github.com/sophiedkk/worklab/issues"
|
|
40
43
|
|
|
41
44
|
[build-system]
|
|
42
45
|
requires = ["setuptools>=43.0.0", "wheel"]
|
|
@@ -4,6 +4,9 @@ import copy
|
|
|
4
4
|
import math
|
|
5
5
|
import pandas as pd
|
|
6
6
|
import matplotlib.pyplot as plt
|
|
7
|
+
import numpy as np
|
|
8
|
+
import sklearn
|
|
9
|
+
import seaborn as sns
|
|
7
10
|
|
|
8
11
|
from .plots import plot_power_speed_dist
|
|
9
12
|
from .physio import calc_weighted_average
|
|
@@ -58,7 +61,7 @@ def cut_data(data, start, end, distance=True):
|
|
|
58
61
|
data[side] = data[side][(data[side]["time"] > start) & (data[side]["time"] < end)]
|
|
59
62
|
data[side]["time"] = data[side]["time"] - start
|
|
60
63
|
if distance:
|
|
61
|
-
data[side]["dist"]
|
|
64
|
+
data[side]["dist"] -= data[side]["dist"].iloc[0]
|
|
62
65
|
data[side].reset_index(drop=True, inplace=True)
|
|
63
66
|
|
|
64
67
|
return data
|
|
@@ -592,3 +595,93 @@ def ana_submax(data_ergo, data_pbp, data_spiro):
|
|
|
592
595
|
outcomes = pd.concat([mean_ergo, mean_spiro], axis=1)
|
|
593
596
|
outcomes["me"] = (outcomes["meanpower"] / outcomes["EE"]) * 100
|
|
594
597
|
return outcomes
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def force_velocity_curve(data_pbp, upper_lim=800, var='max'):
|
|
601
|
+
"""
|
|
602
|
+
Creates force-velocity curves for wheelchair sports
|
|
603
|
+
|
|
604
|
+
Parameters
|
|
605
|
+
----------
|
|
606
|
+
data_pbp : pd.DataFrame
|
|
607
|
+
processed push-by-push ergometer dataframe with output for all 6 sprints
|
|
608
|
+
upper_lim : int
|
|
609
|
+
upper limit recommendations for LP (800) and HP (1400)
|
|
610
|
+
var : str
|
|
611
|
+
'max' force and velocity or 'mean' force and velocity
|
|
612
|
+
|
|
613
|
+
Returns
|
|
614
|
+
-------
|
|
615
|
+
fig : figure
|
|
616
|
+
force-velocity plot
|
|
617
|
+
variables : pd.DataFrame
|
|
618
|
+
r2, optimal velocity/power, x/y coordinates and coefficient
|
|
619
|
+
|
|
620
|
+
"""
|
|
621
|
+
if var == 'max':
|
|
622
|
+
speed = 'maxspeed'
|
|
623
|
+
force = 'maxuforce'
|
|
624
|
+
else:
|
|
625
|
+
speed = 'meanspeed'
|
|
626
|
+
force = 'meanuforce'
|
|
627
|
+
|
|
628
|
+
data_pbp = data_pbp[data_pbp.index > 0]
|
|
629
|
+
x = np.array(data_pbp[speed]).reshape((-1, 1))
|
|
630
|
+
y = np.array(data_pbp[force])
|
|
631
|
+
data_pbp['x'] = np.array(data_pbp[speed]).reshape((-1, 1))
|
|
632
|
+
data_pbp['y'] = np.array(data_pbp[force])
|
|
633
|
+
model = sklearn.LinearRegression()
|
|
634
|
+
model.fit(x, y)
|
|
635
|
+
model = sklearn.LinearRegression().fit(x, y)
|
|
636
|
+
r_sq = model.score(x, y)
|
|
637
|
+
x1 = np.linspace(0, float(abs(model.intercept_ / model.coef_)), 100)
|
|
638
|
+
xx = np.linspace(x.min(), x.max(), 100)
|
|
639
|
+
|
|
640
|
+
pred_y = model.intercept_ + model.coef_ * x
|
|
641
|
+
pred_y1 = model.intercept_ + model.coef_ * x1
|
|
642
|
+
pred_y2 = model.intercept_ + model.coef_ * xx
|
|
643
|
+
power = xx * pred_y2
|
|
644
|
+
power1 = x1 * pred_y1
|
|
645
|
+
parabola = pd.DataFrame({'POmax': power1, 'vmax': x1})
|
|
646
|
+
pomax_pos = parabola['POmax'].idxmax()
|
|
647
|
+
pomax_opt = parabola['POmax'].max()
|
|
648
|
+
vmax_opt = parabola['vmax'][pomax_pos]
|
|
649
|
+
|
|
650
|
+
variables = pd.DataFrame([])
|
|
651
|
+
variables['R2'] = [r_sq]
|
|
652
|
+
variables['opt_vel'] = vmax_opt
|
|
653
|
+
variables['opt_pow'] = pomax_opt
|
|
654
|
+
variables['y_cor'] = model.intercept_
|
|
655
|
+
variables['x_cor'] = x1[-1]
|
|
656
|
+
variables['coef'] = model.coef_
|
|
657
|
+
variables = round(variables, 2)
|
|
658
|
+
|
|
659
|
+
sns.set_style('darkgrid')
|
|
660
|
+
col_pal = sns.color_palette("dark:#5A9_r")
|
|
661
|
+
sns.set_palette(col_pal)
|
|
662
|
+
fig, ax = plt.subplots(1, 1, figsize=(14, 8))
|
|
663
|
+
ax.set_ylabel('Force [N]', fontsize=14)
|
|
664
|
+
ax.set_ylim(0, upper_lim)
|
|
665
|
+
ax.set_xlabel('Velocity [ms]', fontsize=14)
|
|
666
|
+
ax.set_xlim(0, 6)
|
|
667
|
+
ax.tick_params(axis='both', labelsize=12)
|
|
668
|
+
ax = sns.scatterplot(data=data_pbp, x='x', y='y', hue='Resistance')
|
|
669
|
+
ax.plot(x1, pred_y1, color='k', linestyle='--')
|
|
670
|
+
ax.plot(x, pred_y, color='k')
|
|
671
|
+
ax.annotate('R2 = ' + str(round(r_sq, 2)), xy=(0.75, 0.90), xycoords='axes fraction')
|
|
672
|
+
ax.annotate('y = ' + str(round(model.intercept_, 1)) + ' ' + str(round(model.coef_[0], 1)) + ' * x',
|
|
673
|
+
xy=(0.75, 0.85), xycoords='axes fraction')
|
|
674
|
+
|
|
675
|
+
ax1 = ax.twinx()
|
|
676
|
+
ax1.grid(False)
|
|
677
|
+
ax1.plot(x1, power1, color='grey', linestyle='--')
|
|
678
|
+
ax1.plot(xx, power, color='grey')
|
|
679
|
+
ax1.set_ylim(0, upper_lim)
|
|
680
|
+
ax1.set_ylabel('Power [W]', color='grey', fontsize=14)
|
|
681
|
+
ax1.yaxis.label.set_color('grey')
|
|
682
|
+
ax1.spines['right'].set_color('grey')
|
|
683
|
+
ax1.tick_params(axis='y', colors='grey')
|
|
684
|
+
ax1.tick_params(axis='both', labelsize=12)
|
|
685
|
+
ax1.annotate('Optimal velocity (' + str(round(vmax_opt, 1)) + ' ms)', xy=(0.75, 0.95), xycoords='axes fraction')
|
|
686
|
+
|
|
687
|
+
return fig, variables
|
|
@@ -87,7 +87,7 @@ def load_spiro(filename):
|
|
|
87
87
|
|
|
88
88
|
Loads spirometer data to a pandas DataFrame, converts time to seconds (not datetime), computes energy expenditure,
|
|
89
89
|
computes weights from the time difference between samples, if no heart rate data is available it fills
|
|
90
|
-
the column with
|
|
90
|
+
the column with nans. Returns a DataFrame with:
|
|
91
91
|
|
|
92
92
|
+------------+----------------------------+-----------+
|
|
93
93
|
| Column | Data | Unit |
|
|
@@ -139,7 +139,7 @@ def load_spiro(filename):
|
|
|
139
139
|
data["VO2"] = data["VO2"] / 1000 # to l/min
|
|
140
140
|
data["VCO2"] = data["VCO2"] / 1000 # to l/min
|
|
141
141
|
data["RER"] = data["VCO2"] / data["VO2"]
|
|
142
|
-
data["HR"] = np.
|
|
142
|
+
data["HR"] = np.nan if "HR" not in data else data["HR"] # missing when sensor is not detected
|
|
143
143
|
data["O2pulse"] = data["VO2"] / data["HR"]
|
|
144
144
|
data["VE/VO2"] = data["VE"] / data["VO2"]
|
|
145
145
|
data["VE/VCO2"] = data["VE"] / data["VCO2"]
|
|
@@ -165,7 +165,7 @@ def load_spiro(filename):
|
|
|
165
165
|
]
|
|
166
166
|
|
|
167
167
|
|
|
168
|
-
def load_spiro_metamax(filename):
|
|
168
|
+
def load_spiro_metamax(filename, sheet_name=0):
|
|
169
169
|
"""
|
|
170
170
|
Loads metamax 3B spirometer data from Excel file.
|
|
171
171
|
|
|
@@ -209,6 +209,8 @@ def load_spiro_metamax(filename):
|
|
|
209
209
|
----------
|
|
210
210
|
filename : str
|
|
211
211
|
full file path or file in existing path from metamax 3B spirometer
|
|
212
|
+
sheet_name : str or int
|
|
213
|
+
sheet name, default = 0
|
|
212
214
|
|
|
213
215
|
Returns
|
|
214
216
|
-------
|
|
@@ -217,11 +219,19 @@ def load_spiro_metamax(filename):
|
|
|
217
219
|
|
|
218
220
|
|
|
219
221
|
"""
|
|
220
|
-
|
|
222
|
+
excel_data = pd.ExcelFile(filename)
|
|
223
|
+
|
|
224
|
+
# Loop through rows to find the one containing 't' in the first row
|
|
225
|
+
for index, row in enumerate(excel_data.parse(sheet_name=sheet_name, header=None).itertuples(index=False)):
|
|
226
|
+
if 't' in row:
|
|
227
|
+
skip_until = index
|
|
228
|
+
break
|
|
229
|
+
|
|
230
|
+
data = pd.read_excel(filename, skiprows=[*range(0, skip_until, 1)], sheet_name=sheet_name)
|
|
221
231
|
# units = data.iloc[0, :]
|
|
222
232
|
data.drop(0, inplace=True)
|
|
223
233
|
|
|
224
|
-
data.replace("-", np.
|
|
234
|
+
data.replace("-", np.nan, inplace=True)
|
|
225
235
|
|
|
226
236
|
data.rename(columns={"t": "time", "V'O2": "VO2", "V'E": "VE"}, inplace=True)
|
|
227
237
|
data["VCO2"] = data["V'E/V'CO2"] * (data["VE"])
|
|
@@ -233,7 +243,7 @@ def load_spiro_metamax(filename):
|
|
|
233
243
|
data["VO2"] = data["VO2"].astype(float)
|
|
234
244
|
data["EE"] = ((4.94 * data["RER"] + 16.04) * (1000 * data["VO2"])) / 60
|
|
235
245
|
data["weights"] = np.insert(np.diff(data["time"]), 0, 0) # used for calculating weighted average
|
|
236
|
-
data["HR"] = (np.
|
|
246
|
+
data["HR"] = (np.nan if "HR" not in data else data["HR"]).astype(int) # missing when sensor is not detected
|
|
237
247
|
data["O2pulse"] = data["VO2"] / data["HR"]
|
|
238
248
|
data["VCO2"].replace(0, 0.01, inplace=True)
|
|
239
249
|
data["VO2"].replace(0, 0.01, inplace=True)
|
|
@@ -946,26 +956,39 @@ def load_ximu3(root_dir, filenames=None, inplace=False):
|
|
|
946
956
|
|
|
947
957
|
if 'right' in sessiondata.keys():
|
|
948
958
|
sessiondata["right"] = sessiondata["right"]["Inertial"]
|
|
949
|
-
sessiondata["right"]["timestamp"] -= sessiondata["right"]["timestamp"][0]
|
|
950
959
|
else:
|
|
951
960
|
print('No right sensor imported')
|
|
952
961
|
if 'frame' in sessiondata.keys():
|
|
953
962
|
sessiondata["frame"] = sessiondata["frame"]["Inertial"]
|
|
954
|
-
sessiondata["frame"]["timestamp"] -= sessiondata["frame"]["timestamp"][0]
|
|
955
963
|
else:
|
|
956
964
|
print('No frame sensor imported')
|
|
957
965
|
if 'left' in sessiondata.keys():
|
|
958
966
|
sessiondata["left"] = sessiondata["left"]["Inertial"]
|
|
959
|
-
sessiondata["left"]["timestamp"] -= sessiondata["left"]["timestamp"][0]
|
|
960
967
|
else:
|
|
961
968
|
print('No left sensor imported')
|
|
962
969
|
if 'trunk' in sessiondata.keys():
|
|
963
970
|
sessiondata["trunk"] = sessiondata["trunk"]["Inertial"]
|
|
964
|
-
sessiondata["trunk"]["timestamp"] -= sessiondata["trunk"]["timestamp"][0]
|
|
965
971
|
else:
|
|
966
972
|
print('No trunk sensor imported')
|
|
967
973
|
|
|
974
|
+
start_time = np.max([sessiondata['frame']['timestamp'].min(), sessiondata['left']['timestamp'].min(),
|
|
975
|
+
sessiondata['right']['timestamp'].min()])
|
|
976
|
+
stop_time = np.min([sessiondata['frame']['timestamp'].max(), sessiondata['left']['timestamp'].max(),
|
|
977
|
+
sessiondata['right']['timestamp'].max()])
|
|
978
|
+
|
|
979
|
+
for sensor in sessiondata:
|
|
980
|
+
sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] >= start_time]
|
|
981
|
+
sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] <= stop_time]
|
|
982
|
+
|
|
983
|
+
start_time = np.max([sessiondata['frame']['timestamp'].min(), sessiondata['left']['timestamp'].min(),
|
|
984
|
+
sessiondata['right']['timestamp'].min()])
|
|
985
|
+
stop_time = np.min([sessiondata['frame']['timestamp'].max(), sessiondata['left']['timestamp'].max(),
|
|
986
|
+
sessiondata['right']['timestamp'].max()])
|
|
987
|
+
|
|
968
988
|
for sensor in sessiondata:
|
|
989
|
+
sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] >= start_time]
|
|
990
|
+
sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] <= stop_time]
|
|
991
|
+
sessiondata[sensor] = sessiondata[sensor].reset_index(drop=True)
|
|
969
992
|
sessiondata[sensor]['time'] = pd.to_datetime(sessiondata[sensor]['timestamp'], unit='us')
|
|
970
993
|
sessiondata[sensor]['time'] -= sessiondata[sensor]['time'][0]
|
|
971
994
|
sessiondata[sensor]['time'] = sessiondata[sensor]['time'].dt.total_seconds()
|
|
@@ -2,8 +2,8 @@ import copy
|
|
|
2
2
|
from warnings import warn
|
|
3
3
|
|
|
4
4
|
import numpy as np
|
|
5
|
-
from scipy.integrate import
|
|
6
|
-
from scipy.signal import periodogram, find_peaks
|
|
5
|
+
from scipy.integrate import cumulative_trapezoid
|
|
6
|
+
from scipy.signal import periodogram, find_peaks, savgol_filter
|
|
7
7
|
|
|
8
8
|
from .utils import lowpass_butter, pd_interp
|
|
9
9
|
|
|
@@ -32,10 +32,10 @@ def resample_imu(sessiondata, sfreq=400.0):
|
|
|
32
32
|
https://github.com/xioTechnologies/NGIMU-MATLAB-Import-Logged-Data-Example
|
|
33
33
|
|
|
34
34
|
"""
|
|
35
|
-
end_time =
|
|
35
|
+
end_time = np.inf
|
|
36
36
|
for device in sessiondata:
|
|
37
37
|
max_time = sessiondata[device]["time"].max()
|
|
38
|
-
end_time = max_time if max_time
|
|
38
|
+
end_time = max_time if max_time < end_time else end_time
|
|
39
39
|
|
|
40
40
|
new_time = np.arange(0, end_time, 1 / sfreq)
|
|
41
41
|
|
|
@@ -85,6 +85,7 @@ def process_imu(sessiondata, camber=18, wsize=0.32, wbase=0.80, n_sensors=3, sen
|
|
|
85
85
|
|
|
86
86
|
sfreq = 1 / frame["time"].diff().mean()
|
|
87
87
|
frame["rot_vel"] = lowpass_butter(frame["gyroscope_z"], sfreq=sfreq, cutoff=6)
|
|
88
|
+
frame['rot_vel'] = savgol_filter(frame['rot_vel'], window_length=100, polyorder=3)
|
|
88
89
|
right['gyroscope_y'] = lowpass_butter(right['gyroscope_y'], sfreq=sfreq, cutoff=10)
|
|
89
90
|
|
|
90
91
|
# Wheelchair camber correction
|
|
@@ -101,16 +102,16 @@ def process_imu(sessiondata, camber=18, wsize=0.32, wbase=0.80, n_sensors=3, sen
|
|
|
101
102
|
frame["gyro_cor"] = right["gyro_cor"]
|
|
102
103
|
|
|
103
104
|
# Calculation of rotations, rotational velocity and rotational acceleration
|
|
104
|
-
frame["rot"] =
|
|
105
|
+
frame["rot"] = cumulative_trapezoid(abs(frame["rot_vel"]) / sfreq, initial=0.0)
|
|
105
106
|
frame["rot_acc"] = np.gradient(frame["rot_vel"]) * sfreq
|
|
106
107
|
|
|
107
108
|
# Calculation of velocity, acceleration and distance
|
|
108
109
|
right["vel"] = right["gyro_cor"] * wsize * deg2rad # angular velocity to linear velocity
|
|
109
|
-
right["dist"] =
|
|
110
|
+
right["dist"] = cumulative_trapezoid(right["vel"] / sfreq, initial=0.0) # integral of velocity gives distance
|
|
110
111
|
|
|
111
112
|
if n_sensors == 3:
|
|
112
113
|
left["vel"] = left["gyro_cor"] * wsize * deg2rad
|
|
113
|
-
left["dist"] =
|
|
114
|
+
left["dist"] = cumulative_trapezoid(left["vel"] / sfreq, initial=0.0)
|
|
114
115
|
frame["vel_wheel"] = (right["vel"] + left["vel"]) / 2 # mean velocity both sides
|
|
115
116
|
frame["dist_wheel"] = (right["dist"] + left["dist"]) / 2 # mean distance
|
|
116
117
|
else:
|
|
@@ -148,17 +149,17 @@ def process_imu(sessiondata, camber=18, wsize=0.32, wbase=0.80, n_sensors=3, sen
|
|
|
148
149
|
comb_ratio = np.clip(comb_ratio, 0, 1) # clamp Combine ratio values, not in df
|
|
149
150
|
frame["skid_vel"] = (frame["vel_right"] * comb_ratio) + (frame["vel_left"] * (1 - comb_ratio))
|
|
150
151
|
frame["vel"] = (frame["vel_right"] + frame["vel_left"]) / 2
|
|
151
|
-
frame['dist'] =
|
|
152
|
+
frame['dist'] = cumulative_trapezoid(frame["skid_vel"], initial=0.0) / sfreq
|
|
152
153
|
else:
|
|
153
154
|
frame["vel"] = frame["vel_right"]
|
|
154
|
-
frame["dist"] =
|
|
155
|
+
frame["dist"] = cumulative_trapezoid(frame["vel"], initial=0.0) / sfreq # Combined distance
|
|
155
156
|
|
|
156
157
|
# distance in the x and y direction
|
|
157
|
-
frame["dist_y"] =
|
|
158
|
-
|
|
158
|
+
frame["dist_y"] = cumulative_trapezoid(
|
|
159
|
+
frame['vel'] / sfreq * np.sin(np.deg2rad(cumulative_trapezoid(frame["rot_vel"] / sfreq, initial=0.0))),
|
|
159
160
|
initial=0.0)
|
|
160
|
-
frame["dist_x"] =
|
|
161
|
-
|
|
161
|
+
frame["dist_x"] = cumulative_trapezoid(
|
|
162
|
+
frame['vel'] / sfreq * np.cos(np.deg2rad(cumulative_trapezoid(frame["rot_vel"] / sfreq, initial=0.0))),
|
|
162
163
|
initial=0.0)
|
|
163
164
|
|
|
164
165
|
return sessiondata
|
|
@@ -200,7 +201,8 @@ def process_imu_left(sessiondata, camber=18, wsize=0.32, wbase=0.80,
|
|
|
200
201
|
# Calculation of rotations, rotational velocity and acceleration
|
|
201
202
|
frame["rot_vel"] = lowpass_butter(frame["gyroscope_z"],
|
|
202
203
|
sfreq=sfreq, cutoff=10)
|
|
203
|
-
frame[
|
|
204
|
+
frame['rot_vel'] = savgol_filter(frame['rot_vel'], window_length=100, polyorder=3)
|
|
205
|
+
frame["rot"] = cumulative_trapezoid(abs(frame["rot_vel"]) / sfreq, initial=0.0)
|
|
204
206
|
frame["rot_acc"] = np.gradient(frame["rot_vel"]) * sfreq
|
|
205
207
|
|
|
206
208
|
# Wheelchair camber correction
|
|
@@ -211,10 +213,10 @@ def process_imu_left(sessiondata, camber=18, wsize=0.32, wbase=0.80,
|
|
|
211
213
|
frame["gyro_cor"] = left["gyro_cor"]
|
|
212
214
|
|
|
213
215
|
left["vel"] = left["gyro_cor"] * wsize * deg2rad
|
|
214
|
-
left["dist"] =
|
|
216
|
+
left["dist"] = cumulative_trapezoid(left["vel"] / sfreq, initial=0.0)
|
|
215
217
|
frame["vel_wheel"] = left["vel"]
|
|
216
218
|
frame["vel_wheel"] = lowpass_butter(frame["vel_wheel"], sfreq=sfreq, cutoff=10)
|
|
217
|
-
frame["dist_wheel"] =
|
|
219
|
+
frame["dist_wheel"] = cumulative_trapezoid(frame["vel_wheel"] / sfreq, initial=0.0)
|
|
218
220
|
|
|
219
221
|
frame["acc_wheel"] = np.gradient(frame["vel_wheel"]) * sfreq
|
|
220
222
|
frame['acc_wheel'] = lowpass_butter(frame['acc_wheel'],
|
|
@@ -228,14 +230,14 @@ def process_imu_left(sessiondata, camber=18, wsize=0.32, wbase=0.80,
|
|
|
228
230
|
frame["vel_left"] = left["vel"]
|
|
229
231
|
frame["vel_left"] += np.tan(np.deg2rad(frame["rot_vel"] / sfreq)) * wbase / 2 * sfreq
|
|
230
232
|
frame["vel"] = frame["vel_left"]
|
|
231
|
-
frame["dist"] =
|
|
233
|
+
frame["dist"] = cumulative_trapezoid(frame["vel"], initial=0.0) / sfreq
|
|
232
234
|
|
|
233
235
|
# distance in the x and y direction
|
|
234
|
-
frame["dist_y"] =
|
|
235
|
-
|
|
236
|
+
frame["dist_y"] = cumulative_trapezoid(
|
|
237
|
+
frame['vel'] / sfreq * np.sin(np.deg2rad(cumulative_trapezoid(frame["rot_vel"] / sfreq, initial=0.0))),
|
|
236
238
|
initial=0.0)
|
|
237
|
-
frame["dist_x"] =
|
|
238
|
-
|
|
239
|
+
frame["dist_x"] = cumulative_trapezoid(
|
|
240
|
+
frame['vel'] / sfreq * np.cos(np.deg2rad(cumulative_trapezoid(frame["rot_vel"] / sfreq, initial=0.0))),
|
|
239
241
|
initial=0.0)
|
|
240
242
|
|
|
241
243
|
return sessiondata
|
|
@@ -311,7 +313,7 @@ def push_imu(acceleration, sfreq=400.0):
|
|
|
311
313
|
return push_idx, acc_filt, n_pushes, cycle_time, push_freq
|
|
312
314
|
|
|
313
315
|
|
|
314
|
-
def movesense_offset(sessiondata, n_sensors=2, right_wheel=True):
|
|
316
|
+
def movesense_offset(sessiondata, n_sensors=2, right_wheel=True, gyro_offset=False):
|
|
315
317
|
"""
|
|
316
318
|
Remove offset MoveSense sensors
|
|
317
319
|
|
|
@@ -324,6 +326,8 @@ def movesense_offset(sessiondata, n_sensors=2, right_wheel=True):
|
|
|
324
326
|
n_sensors: float
|
|
325
327
|
number of sensors used, 2: right wheel and frame,
|
|
326
328
|
3: right, left wheel and frame
|
|
329
|
+
gyro_offset: boolean
|
|
330
|
+
if set to True, an additional gyroscope offset will be used
|
|
327
331
|
|
|
328
332
|
Returns
|
|
329
333
|
-------
|
|
@@ -363,5 +367,10 @@ def movesense_offset(sessiondata, n_sensors=2, right_wheel=True):
|
|
|
363
367
|
sessiondata['left']['gyroscope_x'] -= offset_left_x
|
|
364
368
|
else:
|
|
365
369
|
print('No offset corrected')
|
|
370
|
+
if gyro_offset is True:
|
|
371
|
+
sessiondata['frame']['gyroscope_z'] = np.sign(
|
|
372
|
+
sessiondata['frame']['gyroscope_z']) * np.sqrt(sessiondata['frame']['gyroscope_x']**2
|
|
373
|
+
+ sessiondata['frame']['gyroscope_y']**2
|
|
374
|
+
+ sessiondata['frame']['gyroscope_z']**2)
|
|
366
375
|
|
|
367
376
|
return sessiondata
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
import pandas as pd
|
|
3
|
-
from scipy.integrate import
|
|
3
|
+
from scipy.integrate import cumulative_trapezoid
|
|
4
4
|
from scipy.signal import savgol_filter
|
|
5
5
|
from .utils import lowpass_butter, find_peaks
|
|
6
6
|
from .move import rotate_matrix
|
|
@@ -220,7 +220,7 @@ def process_mw(data, wheelsize=0.31, rimsize=0.275, sfreq=200):
|
|
|
220
220
|
"""
|
|
221
221
|
data["aspeed"] = np.gradient(data["angle"]) * sfreq
|
|
222
222
|
data["speed"] = data["aspeed"] * wheelsize
|
|
223
|
-
data["dist"] =
|
|
223
|
+
data["dist"] = cumulative_trapezoid(data["speed"], initial=0.0) / sfreq
|
|
224
224
|
data["acc"] = np.gradient(data["speed"]) * sfreq
|
|
225
225
|
data["ftot"] = (data["fx"] ** 2 + data["fy"] ** 2 + data["fz"] ** 2) ** 0.5
|
|
226
226
|
data["uforce"] = data["torque"] / rimsize
|
|
@@ -231,31 +231,37 @@ def process_mw(data, wheelsize=0.31, rimsize=0.275, sfreq=200):
|
|
|
231
231
|
return data
|
|
232
232
|
|
|
233
233
|
|
|
234
|
-
def process_ergo(data, wheelsize=0.31, rimsize=0.275):
|
|
234
|
+
def process_ergo(data, wheelsize=0.31, rimsize=0.275, unit="ms"):
|
|
235
235
|
"""
|
|
236
236
|
Basic processing for ergometer data.
|
|
237
237
|
|
|
238
238
|
Basic processing for ergometer data (e.g. speed to distance). Should be performed after filtering.
|
|
239
|
-
|
|
239
|
+
Returned columns:
|
|
240
240
|
|
|
241
241
|
+------------+----------------------+-----------+
|
|
242
242
|
| Column | Data | Unit |
|
|
243
243
|
+============+======================+===========+
|
|
244
|
-
|
|
|
244
|
+
| time | time | s |
|
|
245
245
|
+------------+----------------------+-----------+
|
|
246
|
-
|
|
|
246
|
+
| force | force (on wheel) | N |
|
|
247
|
+
+------------+----------------------+-----------+
|
|
248
|
+
| speed | speed | m/s |
|
|
247
249
|
+------------+----------------------+-----------+
|
|
248
250
|
| acc | acceleration | m/s^2 |
|
|
249
251
|
+------------+----------------------+-----------+
|
|
252
|
+
| aspeed | angular velocity | rad/s |
|
|
253
|
+
+------------+----------------------+-----------+
|
|
254
|
+
| angle | angle | rad |
|
|
255
|
+
+------------+----------------------+-----------+
|
|
250
256
|
| dist | cumulative distance | m |
|
|
251
257
|
+------------+----------------------+-----------+
|
|
252
258
|
| power | power | W |
|
|
253
259
|
+------------+----------------------+-----------+
|
|
254
|
-
|
|
|
260
|
+
| torque | torque around wheel | Nm |
|
|
255
261
|
+------------+----------------------+-----------+
|
|
256
262
|
| uforce | effective force | N |
|
|
257
263
|
+------------+----------------------+-----------+
|
|
258
|
-
|
|
|
264
|
+
| work | instantaneous work | J |
|
|
259
265
|
+------------+----------------------+-----------+
|
|
260
266
|
|
|
261
267
|
.. note:: the force column contains force on the wheels, uforce (user force) is force on the handrim
|
|
@@ -268,6 +274,8 @@ def process_ergo(data, wheelsize=0.31, rimsize=0.275):
|
|
|
268
274
|
wheel radius [m]
|
|
269
275
|
rimsize : float
|
|
270
276
|
handrim radius [m]
|
|
277
|
+
unit : str
|
|
278
|
+
unit of measured 'speed' column; ms, kmh or mph
|
|
271
279
|
|
|
272
280
|
Returns
|
|
273
281
|
-------
|
|
@@ -280,14 +288,23 @@ def process_ergo(data, wheelsize=0.31, rimsize=0.275):
|
|
|
280
288
|
"""
|
|
281
289
|
sfreq = 100 # ergometer is always 100Hz
|
|
282
290
|
for side in data:
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
291
|
+
if unit == "kmh":
|
|
292
|
+
data[side]["speed"] /= 3.6
|
|
293
|
+
elif unit == "mph":
|
|
294
|
+
data[side]["speed"] /= 2.23694
|
|
295
|
+
elif unit == "ms":
|
|
296
|
+
data[side]["speed"] = data[side]["speed"]
|
|
297
|
+
else:
|
|
298
|
+
raise Exception("Please specify either 'ms', 'kmh' or 'mph', data not processed!")
|
|
299
|
+
|
|
286
300
|
data[side]["acc"] = np.gradient(data[side]["speed"]) * sfreq
|
|
301
|
+
data[side]["aspeed"] = data[side]["speed"] / wheelsize
|
|
302
|
+
data[side]["angle"] = cumulative_trapezoid(data[side]["aspeed"], initial=0.0) / sfreq
|
|
303
|
+
data[side]["dist"] = cumulative_trapezoid(data[side]["speed"], initial=0.0) / sfreq
|
|
287
304
|
data[side]["power"] = data[side]["speed"] * data[side]["force"]
|
|
288
|
-
data[side]["
|
|
289
|
-
data[side]["work"] = data[side]["power"] / sfreq
|
|
305
|
+
data[side]["torque"] = data[side]["force"] * wheelsize
|
|
290
306
|
data[side]["uforce"] = data[side]["force"] * (wheelsize / rimsize)
|
|
307
|
+
data[side]["work"] = data[side]["power"] / sfreq
|
|
291
308
|
return data
|
|
292
309
|
|
|
293
310
|
|
|
@@ -389,7 +406,7 @@ def push_by_push_mw(data, variable="torque", cutoff=0.0, minpeak=5.0, mindist=5,
|
|
|
389
406
|
"cwork",
|
|
390
407
|
"negwork",
|
|
391
408
|
]
|
|
392
|
-
pbp = pd.DataFrame(data=np.full((len(peaks["start"]), len(keys)), np.
|
|
409
|
+
pbp = pd.DataFrame(data=np.full((len(peaks["start"]), len(keys)), np.nan), columns=keys) # preallocate dataframe
|
|
393
410
|
|
|
394
411
|
pbp["start"] = peaks["start"]
|
|
395
412
|
pbp["peak"] = peaks["peak"]
|
|
@@ -421,7 +438,7 @@ def push_by_push_mw(data, variable="torque", cutoff=0.0, minpeak=5.0, mindist=5,
|
|
|
421
438
|
negative_work = data["work"].copy()
|
|
422
439
|
negative_work[negative_work >= 0] = 0
|
|
423
440
|
pbp["negwork"] = negative_work.groupby(cycle_bins).sum()[1:].reset_index(drop=True)
|
|
424
|
-
pbp.loc[len(pbp) - 1, ["cwork", "negwork"]] = np.
|
|
441
|
+
pbp.loc[len(pbp) - 1, ["cwork", "negwork"]] = np.nan
|
|
425
442
|
|
|
426
443
|
if verbose:
|
|
427
444
|
print("\n" + "=" * 80 + f"\nFound {len(pbp)} pushes!\n" + "=" * 80 + "\n")
|
|
@@ -434,43 +451,51 @@ def push_by_push_ergo(data, variable="power", cutoff=0.0, minpeak=50.0, mindist=
|
|
|
434
451
|
|
|
435
452
|
Push detection and push-by-push analysis for ergometer data. Returns a pandas DataFrame with:
|
|
436
453
|
|
|
437
|
-
|
|
438
|
-
| Column | Data
|
|
439
|
-
|
|
440
|
-
| start/stop/peak | respective indices
|
|
441
|
-
|
|
442
|
-
| tstart/tstop/tpeak | respective samples
|
|
443
|
-
|
|
444
|
-
| cangle | contact angle
|
|
445
|
-
|
|
446
|
-
| cangle_deg | contact angle
|
|
447
|
-
|
|
448
|
-
| mean/maxpower | power per push
|
|
449
|
-
|
|
450
|
-
| mean/maxtorque | torque per push
|
|
451
|
-
|
|
452
|
-
| mean/maxforce | force per push
|
|
453
|
-
|
|
454
|
-
| mean/maxuforce | (rim) force per push
|
|
455
|
-
|
|
456
|
-
| mean/maxspeed | velocity per push
|
|
457
|
-
|
|
458
|
-
| work | work per push
|
|
459
|
-
|
|
460
|
-
| cwork | work per cycle
|
|
461
|
-
|
|
462
|
-
| negwork | negative work/cycle
|
|
463
|
-
|
|
464
|
-
| slope | slope onset to peak
|
|
465
|
-
|
|
466
|
-
| smoothness | mean/peak force
|
|
467
|
-
|
|
468
|
-
| ptime | push time
|
|
469
|
-
|
|
470
|
-
| ctime | cycle time
|
|
471
|
-
|
|
472
|
-
| reltime | relative push/cycle
|
|
473
|
-
|
|
454
|
+
+--------------------+-----------------------+-----------+
|
|
455
|
+
| Column | Data | Unit |
|
|
456
|
+
+====================+=======================+===========+
|
|
457
|
+
| start/stop/peak | respective indices | |
|
|
458
|
+
+--------------------+-----------------------+-----------+
|
|
459
|
+
| tstart/tstop/tpeak | respective samples | s |
|
|
460
|
+
+--------------------+-----------------------+-----------+
|
|
461
|
+
| cangle | contact angle | rad |
|
|
462
|
+
+--------------------+-----------------------+-----------+
|
|
463
|
+
| cangle_deg | contact angle | degrees |
|
|
464
|
+
+--------------------+-----------------------+-----------+
|
|
465
|
+
| mean/maxpower | power per push | W |
|
|
466
|
+
+--------------------+-----------------------+-----------+
|
|
467
|
+
| mean/maxtorque | torque per push | Nm |
|
|
468
|
+
+--------------------+-----------------------+-----------+
|
|
469
|
+
| mean/maxforce | force per push | N |
|
|
470
|
+
+--------------------+-----------------------+-----------+
|
|
471
|
+
| mean/maxuforce | (rim) force per push | N |
|
|
472
|
+
+--------------------+-----------------------+-----------+
|
|
473
|
+
| mean/maxspeed | velocity per push | ms |
|
|
474
|
+
+--------------------+-----------------------+-----------+
|
|
475
|
+
| work | work per push | J |
|
|
476
|
+
+--------------------+-----------------------+-----------+
|
|
477
|
+
| cwork | work per cycle | J |
|
|
478
|
+
+--------------------+-----------------------+-----------+
|
|
479
|
+
| negwork | negative work/cycle | J |
|
|
480
|
+
+--------------------+-----------------------+-----------+
|
|
481
|
+
| slope | slope onset to peak | Nm/s |
|
|
482
|
+
+--------------------+-----------------------+-----------+
|
|
483
|
+
| smoothness | mean/peak force | N |
|
|
484
|
+
+--------------------+-----------------------+-----------+
|
|
485
|
+
| ptime | push time | s |
|
|
486
|
+
+--------------------+-----------------------+-----------+
|
|
487
|
+
| ctime | cycle time | s |
|
|
488
|
+
+--------------------+-----------------------+-----------+
|
|
489
|
+
| reltime | relative push/cycle | % |
|
|
490
|
+
+--------------------+-----------------------+-----------+
|
|
491
|
+
| pnegpos | neg power start push | index |
|
|
492
|
+
+--------------------+-----------------------+-----------+
|
|
493
|
+
| negpos | neg power start push | W |
|
|
494
|
+
+--------------------+-----------------------+-----------+
|
|
495
|
+
| pnegpoe | neg power end push | index |
|
|
496
|
+
+--------------------+-----------------------+-----------+
|
|
497
|
+
| negpoe | neg power end push | W |
|
|
498
|
+
+--------------------+-----------------------+-----------+
|
|
474
499
|
|
|
475
500
|
Parameters
|
|
476
501
|
----------
|
|
@@ -520,6 +545,10 @@ def push_by_push_ergo(data, variable="power", cutoff=0.0, minpeak=50.0, mindist=
|
|
|
520
545
|
"reltime",
|
|
521
546
|
"cwork",
|
|
522
547
|
"negwork",
|
|
548
|
+
"pnegpos",
|
|
549
|
+
"negpos",
|
|
550
|
+
"pnegpoe",
|
|
551
|
+
"negpoe",
|
|
523
552
|
]
|
|
524
553
|
|
|
525
554
|
for side in data:
|
|
@@ -527,7 +556,7 @@ def push_by_push_ergo(data, variable="power", cutoff=0.0, minpeak=50.0, mindist=
|
|
|
527
556
|
peaks = find_peaks(data[side][variable], cutoff, minpeak, mindist)
|
|
528
557
|
else:
|
|
529
558
|
peaks = find_peaks(data[side][variable], cutoff, (minpeak * 2), mindist)
|
|
530
|
-
pbp = pd.DataFrame(data=np.full((len(peaks["start"]), len(keys)), np.
|
|
559
|
+
pbp = pd.DataFrame(data=np.full((len(peaks["start"]), len(keys)), np.nan), columns=keys) # preallocate
|
|
531
560
|
|
|
532
561
|
pbp["start"] = peaks["start"]
|
|
533
562
|
pbp["peak"] = peaks["peak"]
|
|
@@ -559,7 +588,24 @@ def push_by_push_ergo(data, variable="power", cutoff=0.0, minpeak=50.0, mindist=
|
|
|
559
588
|
negative_work = data[side]["work"].copy()
|
|
560
589
|
negative_work[negative_work >= 0] = 0
|
|
561
590
|
pbp["negwork"] = negative_work.groupby(cycle_bins).sum()[1:].reset_index(drop=True)
|
|
562
|
-
pbp.loc[len(pbp) - 1, ["cwork", "negwork"]] = np.
|
|
591
|
+
pbp.loc[len(pbp) - 1, ["cwork", "negwork"]] = np.nan
|
|
592
|
+
|
|
593
|
+
neg_before_all = []
|
|
594
|
+
neg_after_all = []
|
|
595
|
+
|
|
596
|
+
for sample, sample2 in zip(pbp['start'], pbp['stop']):
|
|
597
|
+
neg_before = data['mean'].loc[sample - 10:sample]
|
|
598
|
+
neg_before_min = neg_before[neg_before['power'] == min(neg_before['power'])]
|
|
599
|
+
neg_before_all.append(neg_before_min)
|
|
600
|
+
neg_after = data['mean'].loc[sample2:sample2 + 10]
|
|
601
|
+
neg_after_min = neg_after[neg_after['power'] == min(neg_after['power'])]
|
|
602
|
+
neg_after_all.append(neg_after_min)
|
|
603
|
+
|
|
604
|
+
for push in range(0, len(pbp)):
|
|
605
|
+
pbp.loc[push, "pnegpos"] = neg_before_all[push].index.item()
|
|
606
|
+
pbp.loc[push, "negpos"] = neg_before_all[push]['power'].item()
|
|
607
|
+
pbp.loc[push, "pnegpoe"] = neg_after_all[push].index.item()
|
|
608
|
+
pbp.loc[push, "negpoe"] = neg_after_all[push]['power'].item()
|
|
563
609
|
|
|
564
610
|
pbp_sides[side] = pd.DataFrame(pbp)
|
|
565
611
|
|
|
@@ -33,7 +33,7 @@ def plot_pushes(data, pushes, var="torque", start=True, stop=True, peak=True, ax
|
|
|
33
33
|
ax : axis object
|
|
34
34
|
|
|
35
35
|
"""
|
|
36
|
-
with plt.style.context("seaborn-white"):
|
|
36
|
+
with plt.style.context("seaborn-v0_8-white"):
|
|
37
37
|
if not ax:
|
|
38
38
|
_, ax = plt.subplots(1, 1)
|
|
39
39
|
ax.plot(data["time"], data[var])
|
|
@@ -120,7 +120,7 @@ def bland_altman_plot(data1, data2, ax=None, condition=None):
|
|
|
120
120
|
md = np.mean(diff) # Mean of the difference
|
|
121
121
|
sd = np.std(diff, axis=0) # Standard deviation of the difference
|
|
122
122
|
|
|
123
|
-
with plt.style.context("seaborn-white"):
|
|
123
|
+
with plt.style.context("seaborn-v0_8-white"):
|
|
124
124
|
ax.scatter(mean, diff)
|
|
125
125
|
ax.axhline(0, color="dimgray", linestyle="-")
|
|
126
126
|
ax.axhline(md, color="darkgray", linestyle="--")
|
|
@@ -152,7 +152,7 @@ def vel_plot(time, vel, name=""):
|
|
|
152
152
|
ax: axis object
|
|
153
153
|
|
|
154
154
|
"""
|
|
155
|
-
plt.style.use("seaborn-darkgrid")
|
|
155
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
156
156
|
fig, ax = plt.subplots(1, 1, figsize=[10, 6])
|
|
157
157
|
ax.plot(time, vel, "r")
|
|
158
158
|
ax.set_xlabel("Time [s]", fontsize=12)
|
|
@@ -190,7 +190,7 @@ def vel_peak_plot(time, vel, name=""):
|
|
|
190
190
|
y_max_vel_value = np.max(vel)
|
|
191
191
|
|
|
192
192
|
# Create time vs. velocity figure with vel_peak
|
|
193
|
-
plt.style.use("seaborn-darkgrid")
|
|
193
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
194
194
|
fig, ax = plt.subplots(1, 1, figsize=[10, 6])
|
|
195
195
|
ax.plot(time, vel, "r")
|
|
196
196
|
ax.plot(time[y_max_vel], vel[y_max_vel], "ko", label="Vel$_{peak}$: " + str(round(y_max_vel_value, 2)) + " m/s")
|
|
@@ -232,7 +232,7 @@ def vel_peak_dist_plot(time, vel, dist, name=""):
|
|
|
232
232
|
y_max_vel_value = np.max(vel)
|
|
233
233
|
|
|
234
234
|
# Create time vs. velocity figure with vel_peak
|
|
235
|
-
plt.style.use("seaborn-darkgrid")
|
|
235
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
236
236
|
fig, ax1 = plt.subplots(1, 1, figsize=[10, 6])
|
|
237
237
|
ax1.set_ylim(0, y_max_vel_value + 0.5)
|
|
238
238
|
ax1.plot(time, vel, "r")
|
|
@@ -278,7 +278,7 @@ def acc_plot(time, acc, name=""):
|
|
|
278
278
|
|
|
279
279
|
"""
|
|
280
280
|
# Create time vs. acceleration figure
|
|
281
|
-
plt.style.use("seaborn-darkgrid")
|
|
281
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
282
282
|
fig, ax = plt.subplots(1, 1, figsize=[10, 6])
|
|
283
283
|
ax.plot(time, acc, "g")
|
|
284
284
|
ax.set_xlabel("Time [s]", fontsize=12)
|
|
@@ -316,7 +316,7 @@ def acc_peak_plot(time, acc, name=""):
|
|
|
316
316
|
y_max_acc = acc.idxmax()
|
|
317
317
|
|
|
318
318
|
# Create time vs. acceleration figure with acc_peak
|
|
319
|
-
plt.style.use("seaborn-darkgrid")
|
|
319
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
320
320
|
fig, ax = plt.subplots(1, 1, figsize=[10, 6])
|
|
321
321
|
ax.plot(time, acc, "g")
|
|
322
322
|
ax.plot(time[y_max_acc], acc[y_max_acc], "k.", label="Acc$_{peak}$: " + str(round(y_max_acc_value, 2)) + " m/$s^2$")
|
|
@@ -358,7 +358,7 @@ def acc_peak_dist_plot(time, acc, dist, name=""):
|
|
|
358
358
|
y_max_acc = acc.idxmax()
|
|
359
359
|
|
|
360
360
|
# Create time vs. acceleration figure with acc_peak
|
|
361
|
-
plt.style.use("seaborn-darkgrid")
|
|
361
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
362
362
|
fig, ax1 = plt.subplots(1, 1, figsize=[10, 6])
|
|
363
363
|
ax1.set_ylim(np.min(acc) - 1, y_max_acc_value + 1)
|
|
364
364
|
ax1.plot(time, acc, "g")
|
|
@@ -406,7 +406,7 @@ def rot_vel_plot(time, rot_vel, name=""):
|
|
|
406
406
|
|
|
407
407
|
"""
|
|
408
408
|
# Create time vs. rotational velocity figure
|
|
409
|
-
plt.style.use("seaborn-darkgrid")
|
|
409
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
410
410
|
fig, ax = plt.subplots(1, 1, figsize=[10, 6])
|
|
411
411
|
ax.plot(time, rot_vel, "b")
|
|
412
412
|
ax.set_xlabel("Time [s]", fontsize=12)
|
|
@@ -477,7 +477,7 @@ def imu_push_plot(sessiondata, acc_frame=True, name='', dec=False):
|
|
|
477
477
|
push_idx, acc_filt, n_pushes, cycle_time, push_freq = push_imu(acc, sfreq)
|
|
478
478
|
|
|
479
479
|
# Create time vs. velocity with push detection figure
|
|
480
|
-
plt.style.use("seaborn-darkgrid")
|
|
480
|
+
plt.style.use("seaborn-v0_8-darkgrid")
|
|
481
481
|
fig, ax1 = plt.subplots(1, 1, figsize=[10, 6])
|
|
482
482
|
ax1.set_ylim(-2, np.max(sessiondata['vel']) + 0.5)
|
|
483
483
|
ax1.plot(sessiondata['time'], sessiondata['vel'], 'r')
|
|
@@ -532,7 +532,7 @@ def plot_power_speed_dist(data, title="", ylim_power=None, ylim_speed=None, ylim
|
|
|
532
532
|
the three axes objects
|
|
533
533
|
|
|
534
534
|
"""
|
|
535
|
-
plt.style.use("seaborn-ticks")
|
|
535
|
+
plt.style.use("seaborn-v0_8-ticks")
|
|
536
536
|
fig = plt.figure()
|
|
537
537
|
|
|
538
538
|
# Generate three axes
|
|
@@ -558,7 +558,7 @@ def binned_stats(array, bins=10, pad=True, func=np.mean, nan_func=np.nanmean):
|
|
|
558
558
|
"""
|
|
559
559
|
array = np.array(array, dtype=float) # make sure we have an array
|
|
560
560
|
if pad:
|
|
561
|
-
array = np.pad(array, (0, bins - array.size % bins), mode="constant", constant_values=np.
|
|
561
|
+
array = np.pad(array, (0, bins - array.size % bins), mode="constant", constant_values=np.nan)
|
|
562
562
|
means = nan_func(array.reshape(-1, bins), axis=1)
|
|
563
563
|
else:
|
|
564
564
|
means = func(array[: (len(array) // bins) * bins].reshape(-1, bins), axis=1)
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: worklab
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Basic scripts for worklab devices
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Sophie de Klerk <r.de.klerk@pl.hanze.nl>, Thomas Rietveld <t.rietveld@lboro.ac.uk>, Rowie Janssen <r.j.f.janssen@umcg.nl>, Jelmer Braaksma <j.braaksma01@umcg.nl>
|
|
6
6
|
License: GNU GENERAL PUBLIC LICENSE
|
|
7
7
|
Version 3, 29 June 2007
|
|
8
8
|
|
|
@@ -637,7 +637,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
637
637
|
the "copyright" line and a pointer to where the full notice is found.
|
|
638
638
|
|
|
639
639
|
Analysis
|
|
640
|
-
Copyright (C) 2018
|
|
640
|
+
Copyright (C) 2018 Sophie de Klerk
|
|
641
641
|
|
|
642
642
|
This program is free software: you can redistribute it and/or modify
|
|
643
643
|
it under the terms of the GNU General Public License as published by
|
|
@@ -657,7 +657,7 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
657
657
|
If the program does terminal interaction, make it output a short
|
|
658
658
|
notice like this when it starts in an interactive mode:
|
|
659
659
|
|
|
660
|
-
Analysis Copyright (C) 2018
|
|
660
|
+
Analysis Copyright (C) 2018 Sophie de Klerk
|
|
661
661
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
662
662
|
This is free software, and you are welcome to redistribute it
|
|
663
663
|
under certain conditions; type `show c' for details.
|
|
@@ -678,8 +678,8 @@ License: GNU GENERAL PUBLIC LICENSE
|
|
|
678
678
|
Public License instead of this License. But first, please read
|
|
679
679
|
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
680
680
|
|
|
681
|
-
Project-URL: Homepage, https://github.com/
|
|
682
|
-
Project-URL: Bug Reports, https://github.com/
|
|
681
|
+
Project-URL: Homepage, https://github.com/sophiedkk/worklab
|
|
682
|
+
Project-URL: Bug Reports, https://github.com/sophiedkk/worklab/issues
|
|
683
683
|
Keywords: biomechanics,ergometry,physiology
|
|
684
684
|
Classifier: Intended Audience :: Science/Research
|
|
685
685
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
@@ -693,16 +693,20 @@ Requires-Dist: numpy
|
|
|
693
693
|
Requires-Dist: pandas
|
|
694
694
|
Requires-Dist: matplotlib
|
|
695
695
|
Requires-Dist: xlrd
|
|
696
|
+
Requires-Dist: scikit-learn
|
|
697
|
+
Requires-Dist: seaborn
|
|
696
698
|
Provides-Extra: dev
|
|
697
699
|
Requires-Dist: pytest; extra == "dev"
|
|
698
700
|
Requires-Dist: Flake8-pyproject; extra == "dev"
|
|
699
701
|
Requires-Dist: black; extra == "dev"
|
|
700
702
|
Requires-Dist: jupyter-book; extra == "dev"
|
|
703
|
+
Requires-Dist: build; extra == "dev"
|
|
704
|
+
Dynamic: license-file
|
|
701
705
|
|
|
702
706
|
# Worklab: a wheelchair biomechanics mini-package
|
|
703
707
|
|
|
704
708
|
[](https://doi.org/10.5281/zenodo.8362962)
|
|
705
|
-
[](https://badge.fury.io/py/worklab) [](https://
|
|
709
|
+
[](https://badge.fury.io/py/worklab) [](https://github.com/sophiedkk/worklab/blob/main/LICENSE)
|
|
706
710
|
|
|
707
711
|
Essential data analysis and (pre-)processing scripts used in projects
|
|
708
712
|
researching the Lode
|
|
@@ -722,7 +726,7 @@ the worklab, which means:
|
|
|
722
726
|
# Documentation
|
|
723
727
|
|
|
724
728
|
For more detailed documentation you can look at the
|
|
725
|
-
[docs](https://
|
|
729
|
+
[docs](https://sophiedkk.github.io/worklab/).
|
|
726
730
|
|
|
727
731
|
# Prerequisites
|
|
728
732
|
|
|
@@ -739,7 +743,7 @@ You can install this package with pip:
|
|
|
739
743
|
# Examples
|
|
740
744
|
|
|
741
745
|
You can find some Jupyter Notebook examples
|
|
742
|
-
[here](https://
|
|
746
|
+
[here](https://sophiedkk.github.io/worklab/chapters/examples.html).
|
|
743
747
|
|
|
744
748
|
# Reporting errors
|
|
745
749
|
|
|
@@ -751,4 +755,4 @@ me or submit an issue through this page.
|
|
|
751
755
|
If you want to refer to this package please use this DOI:
|
|
752
756
|
10.5281/zenodo.8362962, or cite:
|
|
753
757
|
|
|
754
|
-
|
|
758
|
+
Sophie de Klerk, Thomas Rietveld, Rowie Janssen, & Jelmer Braaksma. (2023). Worklab: a wheelchair biomechanics mini-package. Zenodo. https://doi.org/10.5281/zenodo.8362963
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|