autogaita 0.3.1__tar.gz → 0.4.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.
Files changed (43) hide show
  1. autogaita-0.4.0/PKG-INFO +24 -0
  2. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_dlc.py +225 -78
  3. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_dlc_gui.py +674 -330
  4. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_group.py +313 -153
  5. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_group_gui.py +221 -150
  6. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_simi.py +300 -158
  7. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_simi_gui.py +388 -230
  8. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_dlc_multirun.py +5 -1
  9. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_dlc_singlerun.py +5 -1
  10. autogaita-0.4.0/autogaita/batchrun_scripts/autogaita_group_dlcrun.py +88 -0
  11. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_group_simirun.py +17 -12
  12. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_simi_multirun.py +2 -2
  13. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/dlc_gui_config.json +24 -16
  14. autogaita-0.4.0/autogaita/group_gui_config.json +37 -0
  15. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/simi_gui_config.json +7 -7
  16. autogaita-0.4.0/autogaita.egg-info/PKG-INFO +24 -0
  17. {autogaita-0.3.1 → autogaita-0.4.0}/setup.py +2 -2
  18. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_dlc_approval.py +6 -2
  19. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_dlc_unit1_preparation.py +61 -10
  20. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_dlc_unit2_sc_extraction.py +31 -7
  21. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_dlc_unit3_main_analysis.py +37 -13
  22. autogaita-0.3.1/PKG-INFO +0 -10
  23. autogaita-0.3.1/autogaita/batchrun_scripts/autogaita_group_dlcrun.py +0 -56
  24. autogaita-0.3.1/autogaita/group_gui_config.json +0 -40
  25. autogaita-0.3.1/autogaita.egg-info/PKG-INFO +0 -10
  26. {autogaita-0.3.1 → autogaita-0.4.0}/LICENSE +0 -0
  27. {autogaita-0.3.1 → autogaita-0.4.0}/README.md +0 -0
  28. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/__init__.py +0 -0
  29. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/__main__.py +0 -0
  30. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita.py +0 -0
  31. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_icon.icns +0 -0
  32. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_icon.ico +0 -0
  33. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_logo.png +0 -0
  34. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/autogaita_utils.py +0 -0
  35. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/__init__.py +0 -0
  36. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_simi_singlerun.py +0 -0
  37. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita.egg-info/SOURCES.txt +0 -0
  38. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita.egg-info/dependency_links.txt +0 -0
  39. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita.egg-info/requires.txt +0 -0
  40. {autogaita-0.3.1 → autogaita-0.4.0}/autogaita.egg-info/top_level.txt +0 -0
  41. {autogaita-0.3.1 → autogaita-0.4.0}/setup.cfg +0 -0
  42. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_group_approval.py +0 -0
  43. {autogaita-0.3.1 → autogaita-0.4.0}/tests/test_simi_approval.py +0 -0
@@ -0,0 +1,24 @@
1
+ Metadata-Version: 2.1
2
+ Name: autogaita
3
+ Version: 0.4.0
4
+ Summary: Automatic Gait Analysis in Python. A toolbox to streamline and standardise the analysis of kinematics across species after ML-based body posture tracking. Despite being optimised for gait analyses, AutoGaitA has the potential to be used for any kind of kinematic analysis.
5
+ Home-page: https://github.com/mahan-hosseini/AutoGaitA/
6
+ Author: Mahan Hosseini
7
+ License: GPLv3
8
+ Requires-Python: >=3.10
9
+ License-File: LICENSE
10
+ Requires-Dist: customtkinter>=5.2
11
+ Requires-Dist: pandas>=2.0
12
+ Requires-Dist: numpy>=1.24
13
+ Requires-Dist: seaborn>=0.13
14
+ Requires-Dist: matplotlib>=3.7
15
+ Requires-Dist: scikit-learn>=1.2
16
+ Requires-Dist: pingouin>=0.5
17
+ Requires-Dist: scipy>=1.11
18
+ Requires-Dist: ffmpeg-python>=0.2
19
+ Requires-Dist: openpyxl>=3.1
20
+ Requires-Dist: pillow>=10.3
21
+ Requires-Dist: pyobjc
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest; extra == "dev"
24
+ Requires-Dist: hypothesis; extra == "dev"
@@ -44,6 +44,7 @@ ORIGINAL_XLS_FILENAME = " - Original Stepcycles" # filenames of sheet exports
44
44
  NORMALISED_XLS_FILENAME = " - Normalised Stepcycles"
45
45
  AVERAGE_XLS_FILENAME = " - Average Stepcycle"
46
46
  STD_XLS_FILENAME = " - Standard Devs. Stepcycle"
47
+ X_STANDARDISED_XLS_FILENAME = " - X-Standardised Stepcycles"
47
48
  SC_LAT_LEGEND_FONTSIZE = 8
48
49
  # PLOT GUI COLORS
49
50
  FG_COLOR = "#789b73" # grey green
@@ -60,7 +61,7 @@ def dlc(info, folderinfo, cfg):
60
61
  ---------
61
62
  1) import & preparation
62
63
  2) step cycle extraction
63
- 3) y-normalisation & feature computation for individual step cycles
64
+ 3) x/y-standardisation & feature computation for individual step cycles
64
65
  4) step cycle normalisation, dataframe creation & XLS-exportation
65
66
  5) plots
66
67
  """
@@ -82,7 +83,7 @@ def dlc(info, folderinfo, cfg):
82
83
 
83
84
  # ......................... step-cycle extraction ................................
84
85
  all_cycles = extract_stepcycles(data, info, folderinfo, cfg)
85
- if not all_cycles:
86
+ if all_cycles is None:
86
87
  handle_issues("scs_invalid", info)
87
88
  if cfg["dont_show_plots"] is False: # otherwise stuck at loading
88
89
  plot_panel_instance.destroy_plot_panel()
@@ -105,6 +106,7 @@ def some_prep(info, folderinfo, cfg):
105
106
  """Preparation of the data & cfg file for later analyses"""
106
107
 
107
108
  # ............................ unpack stuff ......................................
109
+ # => DON'T unpack (joint) cfg-keys that are tested later by check_and_expand_cfg
108
110
  name = info["name"]
109
111
  results_dir = info["results_dir"]
110
112
  data_string = folderinfo["data_string"]
@@ -113,10 +115,12 @@ def some_prep(info, folderinfo, cfg):
113
115
  subtract_beam = cfg["subtract_beam"]
114
116
  convert_to_mm = cfg["convert_to_mm"]
115
117
  pixel_to_mm_ratio = cfg["pixel_to_mm_ratio"]
116
- normalise_height_at_SC_level = cfg["normalise_height_at_SC_level"]
118
+ standardise_y_at_SC_level = cfg["standardise_y_at_SC_level"]
117
119
  invert_y_axis = cfg["invert_y_axis"]
118
120
  flip_gait_direction = cfg["flip_gait_direction"]
119
121
  analyse_average_x = cfg["analyse_average_x"]
122
+ standardise_x_coordinates = cfg["standardise_x_coordinates"]
123
+ standardise_y_to_a_joint = cfg["standardise_y_to_a_joint"]
120
124
 
121
125
  # ............................. move data ........................................
122
126
  # => see if we can delete a previous runs results folder if existant. if not, it's a
@@ -247,11 +251,10 @@ def some_prep(info, folderinfo, cfg):
247
251
  data = datadf.copy(deep=True)
248
252
 
249
253
  # ................ final data checks, conversions & additions ....................
250
- # IMPORTANT
251
- # ---------
252
- # MAIN TESTS OF USER-INPUT VALIDITY OCCUR HERE!
254
+ # IMPORTANT - MAIN TESTS OF USER-INPUT VALIDITY OCCUR HERE!
255
+ # => UNPACK VARS FROM CFG THAT ARE TESTED BY check_and_expand HERE, NOT EARLIER!
253
256
  cfg = check_and_expand_cfg(data, cfg, info)
254
- if cfg is None: # hind joints were empty
257
+ if cfg is None: # some critical error occured
255
258
  return
256
259
  hind_joints = cfg["hind_joints"]
257
260
  fore_joints = cfg["fore_joints"]
@@ -259,6 +262,9 @@ def some_prep(info, folderinfo, cfg):
259
262
  beam_hind_jointadd = cfg["beam_hind_jointadd"]
260
263
  beam_fore_jointadd = cfg["beam_fore_jointadd"]
261
264
  direction_joint = cfg["direction_joint"]
265
+ # important to unpack to vars hand not to cfg since cfg is overwritten in multiruns!
266
+ x_standardisation_joint = cfg["x_standardisation_joint"][0]
267
+ y_standardisation_joint = cfg["y_standardisation_joint"][0]
262
268
  # store config json file @ group path
263
269
  # !!! NU - do this @ mouse path!
264
270
  group_path = results_dir.split(name)[0]
@@ -266,8 +272,12 @@ def some_prep(info, folderinfo, cfg):
266
272
  config_vars_to_json = {
267
273
  "sampling_rate": sampling_rate,
268
274
  "convert_to_mm": convert_to_mm,
269
- "normalise_height_at_SC_level": normalise_height_at_SC_level,
275
+ "standardise_y_at_SC_level": standardise_y_at_SC_level,
270
276
  "analyse_average_x": analyse_average_x,
277
+ "standardise_x_coordinates": standardise_x_coordinates,
278
+ "x_standardisation_joint": x_standardisation_joint,
279
+ "standardise_y_to_a_joint": standardise_y_to_a_joint,
280
+ "y_standardisation_joint": y_standardisation_joint,
271
281
  "hind_joints": hind_joints,
272
282
  "fore_joints": fore_joints,
273
283
  "angles": angles,
@@ -304,12 +314,15 @@ def some_prep(info, folderinfo, cfg):
304
314
  for col in data.columns:
305
315
  if col.endswith(" y"):
306
316
  data[col] = data[col] * -1
307
- # if we don't have a beam to subtract, normalise y to global y minimum being 0
317
+ # if we don't have a beam to subtract, standardise y to a joint's or global ymin = 0
308
318
  if not subtract_beam:
309
- global_y_min = float("inf")
319
+ y_min = float("inf")
310
320
  y_cols = [col for col in data.columns if col.endswith("y")]
311
- global_y_min = min(data[y_cols].min())
312
- data[y_cols] -= global_y_min
321
+ if standardise_y_to_a_joint:
322
+ y_min = data[y_standardisation_joint + "y"].min()
323
+ else:
324
+ y_min = data[y_cols].min().min()
325
+ data[y_cols] -= y_min
313
326
  # convert pixels to millimeters
314
327
  if convert_to_mm:
315
328
  for column in data.columns:
@@ -319,7 +332,7 @@ def some_prep(info, folderinfo, cfg):
319
332
  data = check_gait_direction(data, direction_joint, flip_gait_direction, info)
320
333
  if data is None: # this means DLC file is broken
321
334
  return
322
- # subtract the beam from the joints to normalise y
335
+ # subtract the beam from the joints to standardise y
323
336
  # => bc. we simulate that all mice run from left to right, we can write:
324
337
  # (note that we also flip beam x columns, but never y-columns!)
325
338
  # => & bc. we multiply y values by *-1 earlier, it's a neg_num - - neg_num
@@ -336,14 +349,8 @@ def some_prep(info, folderinfo, cfg):
336
349
  for joint in list(set(fore_joints + beam_fore_jointadd)):
337
350
  data[joint + "y"] = data[joint + "y"] - data[beam_col_right + "y"]
338
351
  data.drop(columns=list(beamdf.columns), inplace=True) # beam not needed anymore
339
- # add Time and round based on sampling rate
352
+ # add Time
340
353
  data[TIME_COL] = data.index * (1 / sampling_rate)
341
- if sampling_rate <= 100:
342
- data[TIME_COL] = round(data[TIME_COL], 2)
343
- elif 100 < sampling_rate <= 1000:
344
- data[TIME_COL] = round(data[TIME_COL], 3)
345
- else:
346
- data[TIME_COL] = round(data[TIME_COL], 4)
347
354
  # reorder the columns we added
348
355
  cols = [TIME_COL, "Flipped"]
349
356
  data = data[cols + [c for c in data.columns if c not in cols]]
@@ -435,17 +442,15 @@ def check_and_expand_cfg(data, cfg, info):
435
442
  Add plot_joints & direction_joint
436
443
  Make sure to set dont_show_plots to True if Python is not in interactive mode
437
444
  If users subtract a beam, set normalise @ sc level to False
445
+ String-checks for standardisation joints
438
446
  """
439
447
 
440
448
  # run the tests first
449
+ # => note that beamcols & standardisation joints are tested separately further down.
441
450
  for cfg_key in [
442
451
  "angles",
443
452
  "hind_joints",
444
453
  "fore_joints",
445
- "beam_col_left", # note beamcols are lists even though len=1 bc. of
446
- "beam_col_right", # check function's procedure
447
- "beam_hind_jointadd",
448
- "beam_fore_jointadd",
449
454
  ]:
450
455
  cfg[cfg_key] = check_and_fix_cfg_strings(data, cfg, cfg_key, info)
451
456
 
@@ -482,9 +487,18 @@ def check_and_expand_cfg(data, cfg, info):
482
487
 
483
488
  # if subtracting beam, check identifier-strings & that beam colnames were valid.
484
489
  if cfg["subtract_beam"]:
490
+ # first, let's check the strings
491
+ # => note beamcols are lists of len=1 bc. of check function
492
+ for cfg_key in [
493
+ "beam_col_left",
494
+ "beam_col_right",
495
+ "beam_hind_jointadd",
496
+ "beam_fore_jointadd",
497
+ ]:
498
+ cfg[cfg_key] = check_and_fix_cfg_strings(data, cfg, cfg_key, info)
485
499
  beam_col_error_message = (
486
500
  "\n******************\n! CRITICAL ERROR !\n******************\n"
487
- + "It seems like you want to normalise heights to a baseline (beam)."
501
+ + "It seems like you want to standardise heights to a baseline (beam)."
488
502
  + "\nUnfortunately we were unable to find the y-columns you listed in "
489
503
  + "your beam's csv-file.\nPlease try again.\nInvalid beam side(s) was/were:"
490
504
  )
@@ -503,9 +517,38 @@ def check_and_expand_cfg(data, cfg, info):
503
517
  print(beam_col_error_message)
504
518
  return
505
519
 
506
- # never normalise @ SC level if user subtracted a beam
520
+ # never standardise @ SC level if user subtracted a beam
507
521
  if cfg["subtract_beam"]:
508
- cfg["normalise_height_at_SC_level"] = False
522
+ cfg["standardise_y_at_SC_level"] = False
523
+
524
+ # test x/y standardisation joints if needed
525
+ broken_standardisation_joint = ""
526
+ if cfg["standardise_x_coordinates"]:
527
+ cfg["x_standardisation_joint"] = check_and_fix_cfg_strings(
528
+ data, cfg, "x_standardisation_joint", info
529
+ )
530
+ if not cfg["x_standardisation_joint"]:
531
+ broken_standardisation_joint += "x"
532
+ if cfg["standardise_y_to_a_joint"]:
533
+ cfg["y_standardisation_joint"] = check_and_fix_cfg_strings(
534
+ data, cfg, "y_standardisation_joint", info
535
+ )
536
+ if not cfg["y_standardisation_joint"]:
537
+ if broken_standardisation_joint:
538
+ broken_standardisation_joint += " & y"
539
+ else:
540
+ broken_standardisation_joint += "y"
541
+ if broken_standardisation_joint:
542
+ no_standardisation_joint_message = (
543
+ "\n******************\n! CRITICAL ERROR !\n******************\n"
544
+ + "After testing your standardisation joints we found an issue with "
545
+ + broken_standardisation_joint
546
+ + "-coordinate standardisation joint."
547
+ + "\n Cancelling AutoGaitA - please try again!"
548
+ )
549
+ write_issues_to_textfile(no_standardisation_joint_message, info)
550
+ print(no_standardisation_joint_message)
551
+ return
509
552
 
510
553
  return cfg
511
554
 
@@ -513,8 +556,12 @@ def check_and_expand_cfg(data, cfg, info):
513
556
  def check_and_fix_cfg_strings(data, cfg, cfg_key, info):
514
557
  """Check and fix strings in our joint & angle lists so that:
515
558
  1) They don't include empty strings
516
- 2) All strings end with the space character (since we do string + "y")
559
+ 2) All strings end with the space character
560
+ => Important note: strings should never have the coordinate in them (since we do
561
+ string + "y" for example throughout this code)
517
562
  3) All strings are valid columns of the DLC dataset
563
+ => Note that x_standardisation_joint is tested against ending with "x" - rest
564
+ against "y"
518
565
  """
519
566
 
520
567
  # work on this variable (we return to cfg[key] outside of here)
@@ -526,11 +573,17 @@ def check_and_fix_cfg_strings(data, cfg, cfg_key, info):
526
573
  string_variable = [s if s.endswith(" ") else s + " " for s in string_variable]
527
574
  clean_string_list = []
528
575
  invalid_strings_message = ""
529
- for s, string in enumerate(string_variable):
530
- if string + "y" in data.columns:
531
- clean_string_list.append(string)
532
- else:
533
- invalid_strings_message += "\n" + string
576
+ for string in string_variable:
577
+ if cfg_key == "x_standardisation_joint":
578
+ if string + "x" in data.columns:
579
+ clean_string_list.append(string)
580
+ else:
581
+ invalid_strings_message += "\n" + string
582
+ else: # for y_standardisation_joint (& all other cases)!!
583
+ if string + "y" in data.columns:
584
+ clean_string_list.append(string)
585
+ else:
586
+ invalid_strings_message += "\n" + string
534
587
  if invalid_strings_message:
535
588
  # print and save warning
536
589
  strings_warning = (
@@ -862,26 +915,43 @@ def extract_stepcycles(data, info, folderinfo, cfg):
862
915
  # extract the SC times
863
916
  start_in_s = float(SCdf.iloc[info_row, start_col].values[0])
864
917
  end_in_s = float(SCdf.iloc[info_row, end_col].values[0])
865
- if sampling_rate <= 100:
866
- float_precision = 2 # how many decimals we round to
867
- elif 100 < sampling_rate <= 1000:
868
- float_precision = 3
918
+ # see if we are rounding to fix inaccurate user input
919
+ # => account for python's float precision leading to inaccuracies
920
+ # => two important steps here (sanity_check_vals only used for these checks)
921
+ # 1. round to 10th decimal to fix python making
922
+ # 3211.999999999999999995 out of 3212
923
+ sanity_check_start = round(start_in_s * sampling_rate, 10)
924
+ sanity_check_end = round(end_in_s * sampling_rate, 10)
925
+ # 2. comparing abs(sanity check vals) to 1e-7 just to be 1000% sure
926
+ if (abs(sanity_check_start % 1) > 1e-7) | (abs(sanity_check_end % 1) > 1e-7):
927
+ round_message = (
928
+ "\n***********\n! WARNING !\n***********\n"
929
+ + "SC latencies of "
930
+ + str(start_in_s)
931
+ + "s to "
932
+ + str(end_in_s)
933
+ + "s were not provided in units of the frame rate!"
934
+ + "\nWe thus use the previous possible frame(s)."
935
+ + "\nDouble check if this worked as expected or fix annotation table!"
936
+ )
937
+ print(round_message)
938
+ write_issues_to_textfile(round_message, info)
939
+ # assign to all_cycles (note int() rounds down!)
940
+ all_cycles[s][0] = int(start_in_s * sampling_rate)
941
+ all_cycles[s][1] = int(end_in_s * sampling_rate)
942
+ # check if we are in data-bounds
943
+ if (all_cycles[s][0] in data.index) & (all_cycles[s][1] in data.index):
944
+ pass
869
945
  else:
870
- float_precision = 4
871
- start_in_s = round(start_in_s, float_precision)
872
- end_in_s = round(end_in_s, float_precision)
873
- try:
874
- all_cycles[s][0] = np.where(data[TIME_COL] == start_in_s)[0][0]
875
- all_cycles[s][1] = np.where(data[TIME_COL] == end_in_s)[0][0]
876
- except IndexError:
946
+ all_cycles[s] = [None, None] # so they can be cleaned later
877
947
  this_message = (
878
948
  "\n***********\n! WARNING !\n***********\n"
879
949
  + "SC latencies of: "
880
950
  + str(start_in_s)
881
951
  + "s to "
882
952
  + str(end_in_s)
883
- + "s not in data/video range!\n"
884
- + "Skipping!"
953
+ + "s not in data/video range!"
954
+ + "\nSkipping!"
885
955
  )
886
956
  print(this_message)
887
957
  write_issues_to_textfile(this_message, info)
@@ -889,25 +959,28 @@ def extract_stepcycles(data, info, folderinfo, cfg):
889
959
  # ............................ clean all_cycles ..................................
890
960
  # check if we skipped latencies because they were out of data-bounds
891
961
  all_cycles = check_cycle_out_of_bounds(all_cycles)
892
- # check if there are any duplicates (e.g., SC2's start-lat == SC1's end-lat)
893
- all_cycles = check_cycle_duplicates(all_cycles)
894
- # check if user input progressively later latencies
895
- all_cycles = check_cycle_order(all_cycles, info)
896
- # check if DLC tracking broke for any SCs - if so remove them
897
- all_cycles = check_DLC_tracking(data, info, all_cycles, cfg)
962
+ if all_cycles: # can be None if all SCs were out of bounds
963
+ # check if there are any duplicates (e.g., SC2's start-lat == SC1's end-lat)
964
+ all_cycles = check_cycle_duplicates(all_cycles)
965
+ # check if user input progressively later latencies
966
+ all_cycles = check_cycle_order(all_cycles, info)
967
+ # check if DLC tracking broke for any SCs - if so remove them
968
+ all_cycles = check_DLC_tracking(data, info, all_cycles, cfg)
898
969
  return all_cycles
899
970
 
900
971
 
901
972
  # .............................. helper functions ....................................
902
973
  def check_cycle_out_of_bounds(all_cycles):
903
974
  """Check if user provided SC latencies that were not in video/data bounds"""
904
- clean_cycles = []
975
+ clean_cycles = None
905
976
  for cycle in all_cycles:
906
977
  # below checks if values are any type of int (just in case this should
907
978
  # for some super random reason change...)
908
979
  if isinstance(cycle[0], (int, np.integer)) & isinstance(
909
980
  cycle[1], (int, np.integer)
910
981
  ):
982
+ if clean_cycles is None:
983
+ clean_cycles = []
911
984
  clean_cycles.append(cycle)
912
985
  return clean_cycles
913
986
 
@@ -918,7 +991,6 @@ def check_cycle_duplicates(all_cycles):
918
991
  all indices of all_cycles have to be unique. If any duplicates found, add one
919
992
  datapoint to the start latency.
920
993
  """
921
-
922
994
  for c, cycle in enumerate(all_cycles):
923
995
  if c > 0:
924
996
  if cycle[0] == all_cycles[c - 1][1]:
@@ -933,7 +1005,6 @@ def check_cycle_order(all_cycles, info):
933
1005
  1. Start latency earlier than end latency of previous SC
934
1006
  2. End latency earlier then start latency of current SC
935
1007
  """
936
-
937
1008
  clean_cycles = []
938
1009
  current_max_time = 0
939
1010
  for c, cycle in enumerate(all_cycles):
@@ -1061,7 +1132,7 @@ def handle_issues(condition, info):
1061
1132
  # 2) for each step's data we normalise all y (height) values to the body's minimum
1062
1133
  # if wanted
1063
1134
  # 3) we compute and add features (angles, velocities, accelerations)
1064
- # ==> see norm_y_and_add_features_to_one_step & helper functions a
1135
+ # ==> see standardise_x_y_and_add_features_to_one_step & helper functions a
1065
1136
  # 4) immediately after adding features, we normalise a step to bin_num
1066
1137
  # ==> see normalise_one_steps_data & helper functions b
1067
1138
  # 5) we add original and normalised steps to all_steps_data and normalised_steps_data
@@ -1078,20 +1149,40 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1078
1149
  save_to_xls = cfg["save_to_xls"]
1079
1150
  analyse_average_x = cfg["analyse_average_x"]
1080
1151
  bin_num = cfg["bin_num"]
1152
+ standardise_x_coordinates = cfg["standardise_x_coordinates"]
1081
1153
  # do everything on a copy of the data df
1082
1154
  data_copy = data.copy()
1083
1155
  # exactly 1 step
1084
1156
  if len(all_cycles) == 1:
1085
1157
  this_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
1086
- all_steps_data = norm_y_and_add_features_to_one_step(this_step, cfg)
1087
- normalised_steps_data = normalise_one_steps_data(all_steps_data, bin_num)
1158
+ if standardise_x_coordinates:
1159
+ all_steps_data, x_standardised_steps_data = (
1160
+ standardise_x_y_and_add_features_to_one_step(this_step, cfg)
1161
+ )
1162
+ normalised_steps_data = normalise_one_steps_data(
1163
+ x_standardised_steps_data, bin_num
1164
+ )
1165
+ else:
1166
+ all_steps_data = standardise_x_y_and_add_features_to_one_step(
1167
+ this_step, cfg
1168
+ )
1169
+ normalised_steps_data = normalise_one_steps_data(all_steps_data, bin_num)
1088
1170
  # 2 or more steps - build dataframe
1089
1171
  elif len(all_cycles) > 1:
1090
1172
  # first- step is added manually
1091
1173
  first_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
1092
- first_step = norm_y_and_add_features_to_one_step(first_step, cfg)
1093
- all_steps_data = first_step
1094
- normalised_steps_data = normalise_one_steps_data(first_step, bin_num)
1174
+ if standardise_x_coordinates:
1175
+ all_steps_data, x_standardised_steps_data = (
1176
+ standardise_x_y_and_add_features_to_one_step(first_step, cfg)
1177
+ )
1178
+ normalised_steps_data = normalise_one_steps_data(
1179
+ x_standardised_steps_data, bin_num
1180
+ )
1181
+ else:
1182
+ all_steps_data = standardise_x_y_and_add_features_to_one_step(
1183
+ first_step, cfg
1184
+ )
1185
+ normalised_steps_data = normalise_one_steps_data(all_steps_data, bin_num)
1095
1186
  # some prep for addition of further steps
1096
1187
  sc_num = len(all_cycles)
1097
1188
  nanvector = data_copy.loc[[1]]
@@ -1107,13 +1198,29 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1107
1198
  with warnings.catch_warnings():
1108
1199
  warnings.simplefilter("ignore")
1109
1200
  numvector[:] = s + 1
1110
- all_steps_data = add_step_separators(all_steps_data, nanvector, numvector)
1111
1201
  # this_step
1112
1202
  this_step = data_copy.loc[all_cycles[s][0] : all_cycles[s][1]]
1113
- this_step = norm_y_and_add_features_to_one_step(this_step, cfg)
1203
+ if standardise_x_coordinates:
1204
+ this_step, this_x_standardised_step = (
1205
+ standardise_x_y_and_add_features_to_one_step(this_step, cfg)
1206
+ )
1207
+ this_normalised_step = normalise_one_steps_data(
1208
+ this_x_standardised_step, bin_num
1209
+ )
1210
+ else:
1211
+ this_step = standardise_x_y_and_add_features_to_one_step(this_step, cfg)
1212
+ this_normalised_step = normalise_one_steps_data(this_step, bin_num)
1213
+ # step separators & step-to-rest-concatenation
1214
+ # => note that normalised_step is already based on x-stand if required
1215
+ all_steps_data = add_step_separators(all_steps_data, nanvector, numvector)
1114
1216
  all_steps_data = pd.concat([all_steps_data, this_step], axis=0)
1115
- # this_normalised_step
1116
- this_normalised_step = normalise_one_steps_data(this_step, bin_num)
1217
+ if standardise_x_coordinates:
1218
+ x_standardised_steps_data = add_step_separators(
1219
+ x_standardised_steps_data, nanvector, numvector
1220
+ )
1221
+ x_standardised_steps_data = pd.concat(
1222
+ [x_standardised_steps_data, this_x_standardised_step], axis=0
1223
+ )
1117
1224
  normalised_steps_data = add_step_separators(
1118
1225
  normalised_steps_data, nanvector, numvector
1119
1226
  )
@@ -1121,6 +1228,8 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1121
1228
  [normalised_steps_data, this_normalised_step], axis=0
1122
1229
  )
1123
1230
  # compute average & std data
1231
+ # => note that normalised_steps_data is automatically based on x-standardisation
1232
+ # which translates to average_data & std_data
1124
1233
  average_data, std_data = compute_average_and_std_data(
1125
1234
  name, normalised_steps_data, bin_num, analyse_average_x
1126
1235
  )
@@ -1130,6 +1239,8 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1130
1239
  results["average_data"] = average_data
1131
1240
  results["std_data"] = std_data
1132
1241
  results["all_cycles"] = all_cycles
1242
+ if standardise_x_coordinates:
1243
+ results["x_standardised_steps_data"] = x_standardised_steps_data
1133
1244
  # save to files
1134
1245
  save_results_sheet(
1135
1246
  all_steps_data,
@@ -1149,27 +1260,47 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1149
1260
  save_results_sheet(
1150
1261
  std_data, save_to_xls, os.path.join(results_dir, name + STD_XLS_FILENAME)
1151
1262
  )
1263
+ if standardise_x_coordinates:
1264
+ save_results_sheet(
1265
+ x_standardised_steps_data,
1266
+ save_to_xls,
1267
+ os.path.join(results_dir, name + X_STANDARDISED_XLS_FILENAME),
1268
+ )
1152
1269
  return results
1153
1270
 
1154
1271
 
1155
1272
  # ......................................................................................
1156
- # ............... helper functions a - norm z and add features .......................
1273
+ # ............ helper functions a - standardise x, y and add features ................
1157
1274
  # ......................................................................................
1158
1275
 
1159
1276
 
1160
- def norm_y_and_add_features_to_one_step(step, cfg):
1161
- """For a single step cycle's data, normalise z if wanted, flip y columns if needed
1162
- (to simulate equal run direction) and add features (angles & velocities)
1163
- """
1164
- # if user wanted this, normalise z (height) at step-cycle level
1277
+ def standardise_x_y_and_add_features_to_one_step(step, cfg):
1278
+ """For a single step cycle's data, standardise x & y if wanted and add features"""
1279
+ # if user wanted this, standardise y (height) at step-cycle level
1165
1280
  step_copy = step.copy()
1166
- if cfg["normalise_height_at_SC_level"] is True:
1281
+ if cfg["standardise_y_at_SC_level"] is True:
1167
1282
  y_cols = [col for col in step_copy.columns if col.endswith("y")]
1168
- this_y_min = step_copy[y_cols].min().min()
1283
+ if cfg["standardise_y_to_a_joint"] is True:
1284
+ # note the [0] here is important because it's still a list of len=1!!
1285
+ this_y_min = step_copy[cfg["y_standardisation_joint"][0] + "y"].min()
1286
+ else:
1287
+ this_y_min = step_copy[y_cols].min().min()
1169
1288
  step_copy[y_cols] -= this_y_min
1170
1289
  # add angles and velocities
1171
1290
  step_copy = add_features(step_copy, cfg)
1172
- return step_copy
1291
+ # standardise x (horizontal dimension) at step-cycle level too
1292
+ if cfg["standardise_x_coordinates"] is True:
1293
+ x_norm_step = step_copy.copy()
1294
+ x_cols = [col for col in x_norm_step.columns if col.endswith("x")]
1295
+ # note the [0] here is important because it's still a list of len=1!!
1296
+ min_x_standardisation_joint = x_norm_step[
1297
+ cfg["x_standardisation_joint"][0] + "x"
1298
+ ].min()
1299
+ x_norm_step[x_cols] -= min_x_standardisation_joint
1300
+ x_norm_step = add_features(x_norm_step, cfg)
1301
+ return step_copy, x_norm_step
1302
+ else:
1303
+ return step_copy
1173
1304
 
1174
1305
 
1175
1306
  def add_features(step, cfg):
@@ -1399,6 +1530,22 @@ def add_step_separators(dataframe, nanvector, numvector):
1399
1530
 
1400
1531
  # .............................. master function .............................
1401
1532
 
1533
+ # A note on x-standardisation (14.08.2024)
1534
+ # => If x-standardisation was performed, original as well as x-standardised dfs are
1535
+ # generated and exported to xls
1536
+ # => Our plotting functions only used all_steps_data, average_data & std_data
1537
+ # => Conveniently, if x-standardisation is performed, all_steps_data DOES NOT include
1538
+ # x-standardisation and is thus still the correct choice for the step-cycle level
1539
+ # plots (#1-#5)
1540
+ # => On the other hand, average_data & std_data (plots #6-12) DO and SHOULD include
1541
+ # x-standardisation
1542
+ # => Note that there is a slight discrepancy because angle-plots are based on
1543
+ # x-standardised values for the step-cycle level plots (#3) but not for the average
1544
+ # plots (#8)
1545
+ # -- This is not an issue since x-standardisation does not affect angles, even though
1546
+ # the values of x change because they change by a constant for all joints (there
1547
+ # is a unit test for this)
1548
+
1402
1549
  # A note on updated colour cyclers after pull request that was merged 20.06.2024
1403
1550
  # => Using color palettes instead of colour maps as I had previously means that
1404
1551
  # we cycle through neighbouring colours
@@ -1770,7 +1917,7 @@ def plot_hindlimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1770
1917
  ax.plot(this_xs, this_ys, color=this_color, label=this_label)
1771
1918
  else:
1772
1919
  ax.plot(this_xs, this_ys, color=this_color)
1773
- ax.set_title(name + " - Hindlimb Stick Diagram")
1920
+ ax.set_title(name + " - Primary Stick Diagram")
1774
1921
  # legend adjustments
1775
1922
  if legend_outside is True:
1776
1923
  ax.legend(
@@ -1786,7 +1933,7 @@ def plot_hindlimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1786
1933
  else:
1787
1934
  ax.set_xlabel("x (pixels)")
1788
1935
  ax.set_ylabel("y (pixels)")
1789
- figure_file_string = " - Hindlimb Stick Diagram"
1936
+ figure_file_string = " - Primary Stick Diagram"
1790
1937
  save_figures(f, results_dir, name, figure_file_string)
1791
1938
  if dont_show_plots:
1792
1939
  plt.close(f)
@@ -1834,7 +1981,7 @@ def plot_forelimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1834
1981
  ax.plot(this_xs, this_ys, color=this_color, label=this_label)
1835
1982
  else:
1836
1983
  ax.plot(this_xs, this_ys, color=this_color)
1837
- ax.set_title(name + " - Forelimb Stick Diagram")
1984
+ ax.set_title(name + " - Secondary Stick Diagram")
1838
1985
  if convert_to_mm:
1839
1986
  tickconvert_mm_to_cm(ax, "both")
1840
1987
  # legend adjustments
@@ -1852,7 +1999,7 @@ def plot_forelimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1852
1999
  else:
1853
2000
  ax.set_xlabel("x (pixels)")
1854
2001
  ax.set_ylabel("y (pixels)")
1855
- figure_file_string = " - Forelimb Stick Diagram"
2002
+ figure_file_string = " - Secondary Stick Diagram"
1856
2003
  save_figures(f, results_dir, name, figure_file_string)
1857
2004
  if dont_show_plots:
1858
2005
  plt.close(f)