worklab 2.1.0__tar.gz → 2.3.5__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: worklab
3
- Version: 2.1.0
3
+ Version: 2.3.5
4
4
  Summary: Basic scripts for worklab devices
5
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
@@ -695,6 +695,8 @@ Requires-Dist: matplotlib
695
695
  Requires-Dist: xlrd
696
696
  Requires-Dist: scikit-learn
697
697
  Requires-Dist: seaborn
698
+ Requires-Dist: imufusion
699
+ Requires-Dist: openpyxl
698
700
  Provides-Extra: dev
699
701
  Requires-Dist: pytest; extra == "dev"
700
702
  Requires-Dist: Flake8-pyproject; extra == "dev"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "worklab"
3
- version = "2.1.0"
3
+ version = "2.3.5"
4
4
  description = "Basic scripts for worklab devices"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.7"
@@ -25,7 +25,9 @@ dependencies = [
25
25
  "matplotlib",
26
26
  "xlrd",
27
27
  "scikit-learn",
28
- "seaborn"
28
+ "seaborn",
29
+ "imufusion",
30
+ "openpyxl",
29
31
  ]
30
32
 
31
33
  [project.optional-dependencies]
@@ -5,11 +5,15 @@ import math
5
5
  import pandas as pd
6
6
  import matplotlib.pyplot as plt
7
7
  import numpy as np
8
- import sklearn
8
+ import os
9
+ from glob import glob
10
+ import warnings
9
11
  import seaborn as sns
10
12
 
11
- from .plots import plot_power_speed_dist
13
+ from .plots import plot_power_speed_dist, force_velocity_curve, plot_pushes_ergo
12
14
  from .physio import calc_weighted_average
15
+ from .com import load_esseda, load_wheelchair
16
+ from .kin import filter_ergo, process_ergo, push_by_push_ergo
13
17
 
14
18
 
15
19
  def mean_data(data):
@@ -54,7 +58,7 @@ def cut_data(data, start, end, distance=True):
54
58
  Returns
55
59
  -------
56
60
  data : dict
57
- data cutted to time of interest
61
+ data cut to time of interest
58
62
 
59
63
  """
60
64
  for side in data:
@@ -223,7 +227,7 @@ def wingate(data, title=None, box=False, ylim=5):
223
227
  Parameters
224
228
  ----------
225
229
  data : dict
226
- processed and cutted ergometer data dictionary with dataframes
230
+ processed and cut ergometer data dictionary with dataframes
227
231
  title : str
228
232
  title of figure
229
233
  box : bool
@@ -401,7 +405,7 @@ def maximal1min(data, dur, title=None):
401
405
  Parameters
402
406
  ----------
403
407
  data : dict
404
- processed and cutted ergometer data dictionary with dataframes
408
+ processed and cut ergometer data dictionary with dataframes
405
409
  dur : int
406
410
  duration of max test in seconds
407
411
  title : str, optional
@@ -494,9 +498,9 @@ def ana_sprint(data, data_pbp, half=5, title=None):
494
498
  Parameters
495
499
  ----------
496
500
  data : dict
497
- processed and cutted ergometer data dictionary with dataframes
501
+ processed and cut ergometer data dictionary with dataframes
498
502
  data_pbp : dict
499
- processed and cutted push_by_push ergometer data dictionary with dataframes
503
+ processed and cut push_by_push ergometer data dictionary with dataframes
500
504
  half : float, optional
501
505
  half-time of the sprint, default is 5 s
502
506
  title : str, optional
@@ -557,11 +561,11 @@ def ana_submax(data_ergo, data_pbp, data_spiro):
557
561
  Parameters
558
562
  ----------
559
563
  data_ergo : pd.DataFrame
560
- processed and cutted ergometer data
564
+ processed and cut ergometer data
561
565
  data_pbp : pd.DataFrame
562
- processed and cutted ergometer data
566
+ processed and cut ergometer data
563
567
  data_spiro : pd.DataFrame
564
- processed and cutted spirometer data
568
+ processed and cut spirometer data
565
569
 
566
570
  Returns
567
571
  -------
@@ -597,91 +601,234 @@ def ana_submax(data_ergo, data_pbp, data_spiro):
597
601
  return outcomes
598
602
 
599
603
 
600
- def force_velocity_curve(data_pbp, upper_lim=800, var='max'):
604
+ def force_vel_profiling(filename, athlete='PP01', classification='LP',
605
+ sport='WR', duration=10, var='mean', push_cutoff=1,
606
+ minpeak=None, unit='ms', n_sprints=7,
607
+ res_names=None, x_lim=6, y_lim=None):
601
608
  """
602
- Creates force-velocity curves for wheelchair sports
609
+ Force velocity profiling (individualised) on the LODE wheelchair ergometer.
610
+
603
611
 
604
612
  Parameters
605
613
  ----------
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)
614
+ filename : str
615
+ main path name which contain all athlete data, with all sprints
616
+ as separate Excel files within each athlete folder (e.g., 0, 1, 2, 3, 4, 5, 6)
617
+ athlete : str, default 'PP01'
618
+ The alphanumeric identifier or acronym assigned to the participant
619
+ (e.g., 'AS', 'PP01'). Determines the target data folder name.
620
+ classification : str, default 'LP'
621
+ The sport-specific physical impairment classification tier.
622
+ Accepts 'HP' (High Point) or 'LP' (Low Point)
623
+ sport : str, default 'WR'
624
+ The sport, e.g., WR = Wheelchair Rugby
625
+ duration : int, default 10
626
+ The length of sprints in seconds
610
627
  var : str
611
- 'max' force and velocity or 'mean' force and velocity
612
-
628
+ 'mean' force and velocity, 'peak' force and velocity, or 'impulse' and velocity
629
+ push_cutoff: int
630
+ numbers of pushes to cut off at the start, default is 1
631
+ minpeak : int
632
+ Minimum peak detection on the torque signal
633
+ default is None, which will automatically detect peaks based on mean
634
+ unit : str
635
+ unit of measured 'speed' column; ms, kmh or mph, default is ms
636
+ n_sprints : int
637
+ Total number of sprints, default is 7
638
+ res_names :
639
+ list of sprint names, default ['Low', 'Standard', '25%', '50%', '75%', '100%', '125%']
640
+ x_lim : int
641
+ x limit axis, default is 6
642
+ y_lim : int
643
+ y limit axis, default is 800
613
644
  Returns
614
645
  -------
615
- fig : figure
616
- force-velocity plot
617
- variables : pd.DataFrame
618
- r2, optimal velocity/power, x/y coordinates and coefficient
619
-
646
+ var_fvc : pd.DataFrame()
647
+ Containing the force-velocity outcomes (R2, optimal vel/power, coordinates)
648
+ outcomes_mean_all: pd.DataFrame()
649
+ Mean outcome variables of the sprints (mean, right, left)
650
+ data_pbp_all: pd.DataFrame()
651
+ Push-by-push outcomes for all sprints (mean, right, left)
620
652
  """
621
- if var == 'max':
622
- speed = 'maxspeed'
623
- force = 'maxuforce'
653
+ warnings.filterwarnings("ignore", category=UserWarning, module="openpyxl")
654
+
655
+ outcomes_mean_all, outcomes_right_all, outcomes_left_all = (pd.DataFrame() for _ in range(3))
656
+
657
+ upper_lim = 1400 if classification == 'HP' else 800
658
+ upper_lim = y_lim if y_lim is not None else upper_lim
659
+ low_lim = 6
660
+ low_lim = x_lim if x_lim is not None else low_lim
661
+ if res_names is None:
662
+ res_names = ['Low', 'Standard', '25%', '50%', '75%', '100%', '125%']
663
+ # Reading files and creating resistance names
664
+ if os.path.basename(filename) != athlete:
665
+ filenames = filename + '\\' + athlete + "\\*.xlsx*"
624
666
  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")
667
+ filenames = filename + '\\*.xlsx*'
668
+ save_loc = filename + "\\Outcomes"
669
+ fig_loc = save_loc + '\\' + athlete
670
+
671
+ if not os.path.exists(save_loc):
672
+ os.makedirs(save_loc)
673
+ if not os.path.exists(fig_loc):
674
+ os.makedirs(fig_loc)
675
+
676
+ folder = sorted(glob(filenames))
677
+ if not folder:
678
+ # Prints a single, clean text line and stops the function immediately
679
+ print(f"Could not find any sprint data files for athlete '{athlete}' at location: {filename}\\{athlete}\\")
680
+ return None
681
+
682
+ if len(res_names) != n_sprints:
683
+ print(
684
+ f"The number of sprints defined in n_sprints '{n_sprints}' and the list of resistance "
685
+ f"names defined in res_names '{res_names}' do not match")
686
+ return None
687
+ data_pbp_all, data_pbp_right, data_pbp_left = pd.DataFrame([]), pd.DataFrame([]), pd.DataFrame([])
688
+ col_pal = sns.color_palette("dark:#5A9_r", 7)
661
689
  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
690
+ sns.set_style('darkgrid')
691
+ var_names = ['dist', 'max_speed', 'mean_power', 'mean_force', 'mean_speed', 'max_power_push', 'max_force_push',
692
+ 'max_speed_push', 'mean_power_push', 'mean_force_push', 'mean_speed_push',
693
+ 'work_push', 'ptime', 'negwork_push', 'slope', 'smoothness', 'push_freq', 'tot_work', 'mu']
694
+ for sprint, res_name in zip(folder, res_names):
695
+ wheelchair = load_wheelchair(sprint)
696
+ data = load_esseda(sprint)
697
+ data = filter_ergo(data)
698
+ data = process_ergo(data, wheelsize=wheelchair['wheelsize'], rimsize=wheelchair['rimsize'], unit=unit)
699
+ data = mean_data(data)
700
+ res_data = pd.read_excel(sprint, sheet_name=4)
701
+ res_data = res_data[res_data['Time'] > 10]
702
+ mu = np.mean((res_data['Resistance left'] + res_data['Resistance right']) / 2)
703
+ m = int(len(data['mean']["speed"]) - (0.5 * 100))
704
+ start = None
705
+ for st in range(1, m):
706
+ if data['mean']["speed"][st] > 0.1:
707
+ if data['mean']["speed"][int(st + (1.0 * 100))] > 0.5:
708
+ start = st
709
+ break
710
+ if start is None:
711
+ print('Can not determine start point!')
712
+ return None
713
+ data = cut_data(data, start / 100, start / 100 + duration)
714
+ if minpeak is None:
715
+ minpeak = (((np.mean(data['left']['torque']) + np.mean(data['right']['torque'])) / 2)
716
+ + 4)
717
+ data_pbp = push_by_push_ergo(data, variable='torque', cutoff=1.0,
718
+ minpeak=minpeak)
719
+
720
+ res_label = res_name
721
+
722
+ title_suffix = "resistance"
723
+ sprint_title = f"Sprint at {res_label} {title_suffix}"
724
+
725
+ fig, outcomes = ana_sprint(data, data_pbp, half=5)
726
+ fig[0].suptitle(sprint_title)
727
+ fig[1][1].set_ylabel("Velocity [m/s]")
728
+ fig[1][0].set_ylim(0, upper_lim) # Uses upper_lim automatically (800 or 1400)
729
+ fig[1][1].set_ylim(0, 5)
730
+ fig[0].savefig(f"{fig_loc}\\Sprint_{res_label}.png", dpi=600)
731
+ plt.close(fig[0])
732
+
733
+ plot_pushes_ergo(data, data_pbp, var="torque", start=True, stop=True, peak=True)
734
+ plt.savefig(f"{fig_loc}\\Sprint_{res_label}_pbp.png", dpi=600)
735
+ plt.close('all')
736
+
737
+ max_cols = ['dist', 'speed']
738
+ mean_cols = ['power', 'uforce', 'speed']
739
+ pbp_cols = ['maxpower', 'maxuforce', 'maxspeed', 'meanpower', 'meanuforce',
740
+ 'meanspeed', 'work', 'ptime', 'negwork', 'slope', 'smoothness']
741
+ for mode in ['mean', 'right', 'left']:
742
+ data_pbp[mode]['Resistance'] = res_label
743
+ processed_outcomes = {}
744
+
745
+ for side in ['mean', 'right', 'left']:
746
+ data_pbp[side] = data_pbp[side].round(2)
747
+
748
+ stats = [
749
+ data[side][max_cols].max(),
750
+ data[side][mean_cols].mean(),
751
+ data_pbp[side][pbp_cols].mean()
752
+ ]
753
+ outcome_series = pd.concat(stats)
754
+ outcome_series['push_freq'] = len(data_pbp[side]) / duration
755
+ outcome_series['tot_work'] = np.trapezoid(data[side]['work'])
756
+ outcome_series['mu'] = mu
757
+
758
+ processed_outcomes[side] = outcome_series
759
+
760
+ outcomes_mean = processed_outcomes['mean']
761
+ outcomes_right = processed_outcomes['right']
762
+ outcomes_left = processed_outcomes['left']
763
+
764
+ if outcomes_right_all.empty:
765
+ outcomes_right_all = outcomes_right
766
+ outcomes_left_all = outcomes_left
767
+ outcomes_mean_all = outcomes_mean
768
+ else:
769
+ outcomes_right_all = pd.concat([outcomes_right_all, outcomes_right], axis=1)
770
+ outcomes_left_all = pd.concat([outcomes_left_all, outcomes_left], axis=1)
771
+ outcomes_mean_all = pd.concat([outcomes_mean_all, outcomes_mean], axis=1)
772
+
773
+ if data_pbp_all.empty:
774
+ data_pbp_all = data_pbp['mean']
775
+ data_pbp_right = data_pbp['right']
776
+ data_pbp_left = data_pbp['left']
777
+ else:
778
+ data_pbp_all = pd.concat([data_pbp_all, data_pbp['mean']])
779
+ data_pbp_right = pd.concat([data_pbp_right, data_pbp['right']])
780
+ data_pbp_left = pd.concat([data_pbp_left, data_pbp['left']])
781
+
782
+ with pd.ExcelWriter(fig_loc + r'\pbp_outcomes.xlsx') as writer:
783
+ data_pbp_all.to_excel(writer, sheet_name='mean')
784
+ data_pbp_right.to_excel(writer, sheet_name='right')
785
+ data_pbp_left.to_excel(writer, sheet_name='left')
786
+
787
+ dfs = {'mean': outcomes_mean_all, 'right': outcomes_right_all, 'left': outcomes_left_all}
788
+
789
+ for side, df in dfs.items():
790
+ df_new = df.T.reset_index(drop=True)
791
+ df_new.columns = var_names
792
+ df_new['athlete'], df_new['side'], df_new['sport'] = athlete, side, sport
793
+ dfs[side] = df_new
794
+
795
+ outcomes_mean_all, outcomes_right_all, outcomes_left_all = dfs.values()
796
+
797
+ with pd.ExcelWriter(fig_loc + r'\pbp_outcomes_mean.xlsx') as writer:
798
+ outcomes_mean_all.to_excel(writer, sheet_name='mean')
799
+ outcomes_right_all.to_excel(writer, sheet_name='right')
800
+ outcomes_left_all.to_excel(writer, sheet_name='left')
801
+
802
+ # Create force_velocity curves for mean/right/left
803
+ fig_fvc_right, var_fvc_right = force_velocity_curve(data_pbp_right, y_lim=upper_lim,
804
+ push_cutoff=push_cutoff,
805
+ n_sprints=n_sprints,
806
+ var=var, x_lim=low_lim)
807
+ var_fvc_right.index = [athlete]
808
+ var_fvc_right['side'] = 'right'
809
+ plt.savefig(fig_loc + r'\Force_velocity_curve_right.png', dpi=600)
810
+ plt.close('all')
811
+
812
+ fig_fvc_left, var_fvc_left = force_velocity_curve(data_pbp_left, y_lim=upper_lim,
813
+ push_cutoff=push_cutoff,
814
+ n_sprints=n_sprints,
815
+ var=var, x_lim=low_lim)
816
+ var_fvc_left.index = [athlete]
817
+ var_fvc_left['side'] = 'left'
818
+ plt.savefig(fig_loc + r'\Force_velocity_curve_left.png', dpi=600)
819
+ plt.close('all')
820
+
821
+ fig_fvc, var_fvc = force_velocity_curve(data_pbp_all, y_lim=upper_lim,
822
+ push_cutoff=push_cutoff,
823
+ n_sprints=n_sprints,
824
+ var=var, x_lim=low_lim)
825
+ var_fvc.index = [athlete]
826
+ var_fvc['side'] = 'mean'
827
+ plt.savefig(fig_loc + r'\Force_velocity_curve.png', dpi=600)
828
+
829
+ with pd.ExcelWriter(fig_loc + r'\force_vel_outcomes.xlsx') as writer:
830
+ var_fvc.to_excel(writer, sheet_name='mean')
831
+ var_fvc_right.to_excel(writer, sheet_name='right')
832
+ var_fvc_left.to_excel(writer, sheet_name='left')
833
+
834
+ return var_fvc, outcomes_mean_all, data_pbp_all
@@ -282,7 +282,7 @@ def load_opti(filename, rotate=True):
282
282
  | angle | unwrapped wheel angle| rad |
283
283
  +------------+----------------------+-----------+
284
284
 
285
- .. note:: Optipush uses a local coordinate system, option to rotate Fx and Fy available in >1.6
285
+ note:: Optipush uses a local coordinate system, option to rotate Fx and Fy available in >1.6
286
286
 
287
287
  Parameters
288
288
  ----------
@@ -341,7 +341,7 @@ def load_sw(filename, sfreq=200):
341
341
  | angle | unwrapped wheel angle | rad |
342
342
  +------------+-----------------------+-----------+
343
343
 
344
- .. note:: SMARTwheel uses a global coordinate system
344
+ note:: SMARTwheel uses a global coordinate system
345
345
 
346
346
  Parameters
347
347
  ----------
@@ -899,7 +899,7 @@ def load_movesense(root_dir, right=None, frame=None, left=None):
899
899
 
900
900
  def load_ximu3(root_dir, filenames=None, inplace=False):
901
901
  """
902
- Imports X-IMU3 session in nested dictionary with all devices with all sensors. Translated from xio-Technologies.
902
+ Imports X-IMU3 session in nested dictionary with all devices with all sensors.
903
903
 
904
904
  Parameters
905
905
  ----------
@@ -924,6 +924,8 @@ def load_ximu3(root_dir, filenames=None, inplace=False):
924
924
  raise Exception("No contents in directory")
925
925
  directories = glob(f"{root_dir}/*/") # folders of all devices
926
926
  sessiondata = dict()
927
+ timestamp_min = []
928
+ timestamp_max = []
927
929
  if not filenames:
928
930
  filenames = ["Inertial"]
929
931
 
@@ -936,7 +938,6 @@ def load_ximu3(root_dir, filenames=None, inplace=False):
936
938
  device_name = "trunk" if "trunk" in device_name.lower() else device_name
937
939
 
938
940
  sessiondata[device_name] = dict()
939
-
940
941
  for sensor_file in sensor_files: # loop through all csv files
941
942
  sensor_name = path.split(sensor_file)[-1].split(".csv")[0] # sensor without path or extension
942
943
 
@@ -971,19 +972,21 @@ def load_ximu3(root_dir, filenames=None, inplace=False):
971
972
  else:
972
973
  print('No trunk sensor imported')
973
974
 
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()])
975
+ for sensor in sessiondata:
976
+ timestamp_min.append(sessiondata[sensor]['timestamp'].min().item())
977
+ timestamp_max.append(sessiondata[sensor]['timestamp'].max().item())
978
+ start_time = max(timestamp_min)
979
+ stop_time = min(timestamp_max)
978
980
 
979
981
  for sensor in sessiondata:
980
982
  sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] >= start_time]
981
983
  sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] <= stop_time]
982
984
 
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()])
985
+ for sensor in sessiondata:
986
+ timestamp_min.append(sessiondata[sensor]['timestamp'].min().item())
987
+ timestamp_max.append(sessiondata[sensor]['timestamp'].max().item())
988
+ start_time = max(timestamp_min)
989
+ stop_time = min(timestamp_max)
987
990
 
988
991
  for sensor in sessiondata:
989
992
  sessiondata[sensor] = sessiondata[sensor][sessiondata[sensor]['timestamp'] >= start_time]