autogaita 0.3.0__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 (41) hide show
  1. {autogaita-0.3.0 → autogaita-0.4.0}/PKG-INFO +2 -2
  2. {autogaita-0.3.0 → autogaita-0.4.0}/README.md +1 -1
  3. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_dlc.py +238 -80
  4. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_dlc_gui.py +674 -330
  5. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_group.py +323 -164
  6. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_group_gui.py +221 -150
  7. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_simi.py +313 -154
  8. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_simi_gui.py +388 -230
  9. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_dlc_multirun.py +34 -7
  10. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_dlc_singlerun.py +6 -2
  11. autogaita-0.4.0/autogaita/batchrun_scripts/autogaita_group_dlcrun.py +88 -0
  12. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_group_simirun.py +17 -12
  13. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_simi_multirun.py +2 -2
  14. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/dlc_gui_config.json +24 -16
  15. autogaita-0.4.0/autogaita/group_gui_config.json +37 -0
  16. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/simi_gui_config.json +7 -7
  17. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita.egg-info/PKG-INFO +2 -2
  18. {autogaita-0.3.0 → autogaita-0.4.0}/setup.py +2 -2
  19. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_dlc_approval.py +6 -2
  20. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_dlc_unit1_preparation.py +61 -10
  21. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_dlc_unit2_sc_extraction.py +31 -7
  22. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_dlc_unit3_main_analysis.py +37 -13
  23. autogaita-0.3.0/autogaita/batchrun_scripts/autogaita_group_dlcrun.py +0 -56
  24. autogaita-0.3.0/autogaita/group_gui_config.json +0 -40
  25. {autogaita-0.3.0 → autogaita-0.4.0}/LICENSE +0 -0
  26. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/__init__.py +0 -0
  27. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/__main__.py +0 -0
  28. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita.py +0 -0
  29. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_icon.icns +0 -0
  30. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_icon.ico +0 -0
  31. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_logo.png +0 -0
  32. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/autogaita_utils.py +0 -0
  33. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/__init__.py +0 -0
  34. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita/batchrun_scripts/autogaita_simi_singlerun.py +0 -0
  35. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita.egg-info/SOURCES.txt +0 -0
  36. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita.egg-info/dependency_links.txt +0 -0
  37. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita.egg-info/requires.txt +0 -0
  38. {autogaita-0.3.0 → autogaita-0.4.0}/autogaita.egg-info/top_level.txt +0 -0
  39. {autogaita-0.3.0 → autogaita-0.4.0}/setup.cfg +0 -0
  40. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_group_approval.py +0 -0
  41. {autogaita-0.3.0 → autogaita-0.4.0}/tests/test_simi_approval.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogaita
3
- Version: 0.3.0
4
- Summary: Automatic Gait Analysis in Python
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
5
  Home-page: https://github.com/mahan-hosseini/AutoGaitA/
6
6
  Author: Mahan Hosseini
7
7
  License: GPLv3
@@ -31,7 +31,7 @@ It is strongly recommended that a separate virtual environment for AutoGaitA is
31
31
 
32
32
  - Access the main user interface via `python -m autogaita`.
33
33
 
34
- - To update to the latest release (see the *Releases* panel on the right for the current version) activate your virtual environment & enter `pip install autogaita -U`.
34
+ - To update to the latest release (see the *Releases* panel on the right for the latest release) activate the virtual environment and: `pip install autogaita -U`.
35
35
 
36
36
  ## Tutorials and Examples
37
37
 
@@ -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,8 +83,10 @@ 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)
88
+ if cfg["dont_show_plots"] is False: # otherwise stuck at loading
89
+ plot_panel_instance.destroy_plot_panel()
87
90
  return
88
91
 
89
92
  # ......... main analysis: sc-lvl y-norm, features, df-creation & export ..........
@@ -103,6 +106,7 @@ def some_prep(info, folderinfo, cfg):
103
106
  """Preparation of the data & cfg file for later analyses"""
104
107
 
105
108
  # ............................ unpack stuff ......................................
109
+ # => DON'T unpack (joint) cfg-keys that are tested later by check_and_expand_cfg
106
110
  name = info["name"]
107
111
  results_dir = info["results_dir"]
108
112
  data_string = folderinfo["data_string"]
@@ -111,10 +115,12 @@ def some_prep(info, folderinfo, cfg):
111
115
  subtract_beam = cfg["subtract_beam"]
112
116
  convert_to_mm = cfg["convert_to_mm"]
113
117
  pixel_to_mm_ratio = cfg["pixel_to_mm_ratio"]
114
- normalise_height_at_SC_level = cfg["normalise_height_at_SC_level"]
118
+ standardise_y_at_SC_level = cfg["standardise_y_at_SC_level"]
115
119
  invert_y_axis = cfg["invert_y_axis"]
116
120
  flip_gait_direction = cfg["flip_gait_direction"]
117
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"]
118
124
 
119
125
  # ............................. move data ........................................
120
126
  # => see if we can delete a previous runs results folder if existant. if not, it's a
@@ -245,11 +251,10 @@ def some_prep(info, folderinfo, cfg):
245
251
  data = datadf.copy(deep=True)
246
252
 
247
253
  # ................ final data checks, conversions & additions ....................
248
- # IMPORTANT
249
- # ---------
250
- # 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!
251
256
  cfg = check_and_expand_cfg(data, cfg, info)
252
- if cfg is None: # hind joints were empty
257
+ if cfg is None: # some critical error occured
253
258
  return
254
259
  hind_joints = cfg["hind_joints"]
255
260
  fore_joints = cfg["fore_joints"]
@@ -257,6 +262,9 @@ def some_prep(info, folderinfo, cfg):
257
262
  beam_hind_jointadd = cfg["beam_hind_jointadd"]
258
263
  beam_fore_jointadd = cfg["beam_fore_jointadd"]
259
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]
260
268
  # store config json file @ group path
261
269
  # !!! NU - do this @ mouse path!
262
270
  group_path = results_dir.split(name)[0]
@@ -264,8 +272,12 @@ def some_prep(info, folderinfo, cfg):
264
272
  config_vars_to_json = {
265
273
  "sampling_rate": sampling_rate,
266
274
  "convert_to_mm": convert_to_mm,
267
- "normalise_height_at_SC_level": normalise_height_at_SC_level,
275
+ "standardise_y_at_SC_level": standardise_y_at_SC_level,
268
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,
269
281
  "hind_joints": hind_joints,
270
282
  "fore_joints": fore_joints,
271
283
  "angles": angles,
@@ -302,12 +314,15 @@ def some_prep(info, folderinfo, cfg):
302
314
  for col in data.columns:
303
315
  if col.endswith(" y"):
304
316
  data[col] = data[col] * -1
305
- # 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
306
318
  if not subtract_beam:
307
- global_y_min = float("inf")
319
+ y_min = float("inf")
308
320
  y_cols = [col for col in data.columns if col.endswith("y")]
309
- global_y_min = min(data[y_cols].min())
310
- 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
311
326
  # convert pixels to millimeters
312
327
  if convert_to_mm:
313
328
  for column in data.columns:
@@ -317,7 +332,7 @@ def some_prep(info, folderinfo, cfg):
317
332
  data = check_gait_direction(data, direction_joint, flip_gait_direction, info)
318
333
  if data is None: # this means DLC file is broken
319
334
  return
320
- # subtract the beam from the joints to normalise y
335
+ # subtract the beam from the joints to standardise y
321
336
  # => bc. we simulate that all mice run from left to right, we can write:
322
337
  # (note that we also flip beam x columns, but never y-columns!)
323
338
  # => & bc. we multiply y values by *-1 earlier, it's a neg_num - - neg_num
@@ -334,14 +349,8 @@ def some_prep(info, folderinfo, cfg):
334
349
  for joint in list(set(fore_joints + beam_fore_jointadd)):
335
350
  data[joint + "y"] = data[joint + "y"] - data[beam_col_right + "y"]
336
351
  data.drop(columns=list(beamdf.columns), inplace=True) # beam not needed anymore
337
- # add Time and round based on sampling rate
352
+ # add Time
338
353
  data[TIME_COL] = data.index * (1 / sampling_rate)
339
- if sampling_rate <= 100:
340
- data[TIME_COL] = round(data[TIME_COL], 2)
341
- elif 100 < sampling_rate <= 1000:
342
- data[TIME_COL] = round(data[TIME_COL], 3)
343
- else:
344
- data[TIME_COL] = round(data[TIME_COL], 4)
345
354
  # reorder the columns we added
346
355
  cols = [TIME_COL, "Flipped"]
347
356
  data = data[cols + [c for c in data.columns if c not in cols]]
@@ -433,17 +442,15 @@ def check_and_expand_cfg(data, cfg, info):
433
442
  Add plot_joints & direction_joint
434
443
  Make sure to set dont_show_plots to True if Python is not in interactive mode
435
444
  If users subtract a beam, set normalise @ sc level to False
445
+ String-checks for standardisation joints
436
446
  """
437
447
 
438
448
  # run the tests first
449
+ # => note that beamcols & standardisation joints are tested separately further down.
439
450
  for cfg_key in [
440
451
  "angles",
441
452
  "hind_joints",
442
453
  "fore_joints",
443
- "beam_col_left", # note beamcols are lists even though len=1 bc. of
444
- "beam_col_right", # check function's procedure
445
- "beam_hind_jointadd",
446
- "beam_fore_jointadd",
447
454
  ]:
448
455
  cfg[cfg_key] = check_and_fix_cfg_strings(data, cfg, cfg_key, info)
449
456
 
@@ -480,9 +487,18 @@ def check_and_expand_cfg(data, cfg, info):
480
487
 
481
488
  # if subtracting beam, check identifier-strings & that beam colnames were valid.
482
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)
483
499
  beam_col_error_message = (
484
500
  "\n******************\n! CRITICAL ERROR !\n******************\n"
485
- + "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)."
486
502
  + "\nUnfortunately we were unable to find the y-columns you listed in "
487
503
  + "your beam's csv-file.\nPlease try again.\nInvalid beam side(s) was/were:"
488
504
  )
@@ -501,9 +517,38 @@ def check_and_expand_cfg(data, cfg, info):
501
517
  print(beam_col_error_message)
502
518
  return
503
519
 
504
- # never normalise @ SC level if user subtracted a beam
520
+ # never standardise @ SC level if user subtracted a beam
505
521
  if cfg["subtract_beam"]:
506
- 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
507
552
 
508
553
  return cfg
509
554
 
@@ -511,8 +556,12 @@ def check_and_expand_cfg(data, cfg, info):
511
556
  def check_and_fix_cfg_strings(data, cfg, cfg_key, info):
512
557
  """Check and fix strings in our joint & angle lists so that:
513
558
  1) They don't include empty strings
514
- 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)
515
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"
516
565
  """
517
566
 
518
567
  # work on this variable (we return to cfg[key] outside of here)
@@ -524,11 +573,17 @@ def check_and_fix_cfg_strings(data, cfg, cfg_key, info):
524
573
  string_variable = [s if s.endswith(" ") else s + " " for s in string_variable]
525
574
  clean_string_list = []
526
575
  invalid_strings_message = ""
527
- for s, string in enumerate(string_variable):
528
- if string + "y" in data.columns:
529
- clean_string_list.append(string)
530
- else:
531
- 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
532
587
  if invalid_strings_message:
533
588
  # print and save warning
534
589
  strings_warning = (
@@ -860,26 +915,43 @@ def extract_stepcycles(data, info, folderinfo, cfg):
860
915
  # extract the SC times
861
916
  start_in_s = float(SCdf.iloc[info_row, start_col].values[0])
862
917
  end_in_s = float(SCdf.iloc[info_row, end_col].values[0])
863
- if sampling_rate <= 100:
864
- float_precision = 2 # how many decimals we round to
865
- elif 100 < sampling_rate <= 1000:
866
- 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
867
945
  else:
868
- float_precision = 4
869
- start_in_s = round(start_in_s, float_precision)
870
- end_in_s = round(end_in_s, float_precision)
871
- try:
872
- all_cycles[s][0] = np.where(data[TIME_COL] == start_in_s)[0][0]
873
- all_cycles[s][1] = np.where(data[TIME_COL] == end_in_s)[0][0]
874
- except IndexError:
946
+ all_cycles[s] = [None, None] # so they can be cleaned later
875
947
  this_message = (
876
948
  "\n***********\n! WARNING !\n***********\n"
877
949
  + "SC latencies of: "
878
950
  + str(start_in_s)
879
951
  + "s to "
880
952
  + str(end_in_s)
881
- + "s not in data/video range!\n"
882
- + "Skipping!"
953
+ + "s not in data/video range!"
954
+ + "\nSkipping!"
883
955
  )
884
956
  print(this_message)
885
957
  write_issues_to_textfile(this_message, info)
@@ -887,25 +959,28 @@ def extract_stepcycles(data, info, folderinfo, cfg):
887
959
  # ............................ clean all_cycles ..................................
888
960
  # check if we skipped latencies because they were out of data-bounds
889
961
  all_cycles = check_cycle_out_of_bounds(all_cycles)
890
- # check if there are any duplicates (e.g., SC2's start-lat == SC1's end-lat)
891
- all_cycles = check_cycle_duplicates(all_cycles)
892
- # check if user input progressively later latencies
893
- all_cycles = check_cycle_order(all_cycles, info)
894
- # check if DLC tracking broke for any SCs - if so remove them
895
- 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)
896
969
  return all_cycles
897
970
 
898
971
 
899
972
  # .............................. helper functions ....................................
900
973
  def check_cycle_out_of_bounds(all_cycles):
901
974
  """Check if user provided SC latencies that were not in video/data bounds"""
902
- clean_cycles = []
975
+ clean_cycles = None
903
976
  for cycle in all_cycles:
904
977
  # below checks if values are any type of int (just in case this should
905
978
  # for some super random reason change...)
906
979
  if isinstance(cycle[0], (int, np.integer)) & isinstance(
907
980
  cycle[1], (int, np.integer)
908
981
  ):
982
+ if clean_cycles is None:
983
+ clean_cycles = []
909
984
  clean_cycles.append(cycle)
910
985
  return clean_cycles
911
986
 
@@ -916,7 +991,6 @@ def check_cycle_duplicates(all_cycles):
916
991
  all indices of all_cycles have to be unique. If any duplicates found, add one
917
992
  datapoint to the start latency.
918
993
  """
919
-
920
994
  for c, cycle in enumerate(all_cycles):
921
995
  if c > 0:
922
996
  if cycle[0] == all_cycles[c - 1][1]:
@@ -931,7 +1005,6 @@ def check_cycle_order(all_cycles, info):
931
1005
  1. Start latency earlier than end latency of previous SC
932
1006
  2. End latency earlier then start latency of current SC
933
1007
  """
934
-
935
1008
  clean_cycles = []
936
1009
  current_max_time = 0
937
1010
  for c, cycle in enumerate(all_cycles):
@@ -1059,7 +1132,7 @@ def handle_issues(condition, info):
1059
1132
  # 2) for each step's data we normalise all y (height) values to the body's minimum
1060
1133
  # if wanted
1061
1134
  # 3) we compute and add features (angles, velocities, accelerations)
1062
- # ==> 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
1063
1136
  # 4) immediately after adding features, we normalise a step to bin_num
1064
1137
  # ==> see normalise_one_steps_data & helper functions b
1065
1138
  # 5) we add original and normalised steps to all_steps_data and normalised_steps_data
@@ -1076,20 +1149,40 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1076
1149
  save_to_xls = cfg["save_to_xls"]
1077
1150
  analyse_average_x = cfg["analyse_average_x"]
1078
1151
  bin_num = cfg["bin_num"]
1152
+ standardise_x_coordinates = cfg["standardise_x_coordinates"]
1079
1153
  # do everything on a copy of the data df
1080
1154
  data_copy = data.copy()
1081
1155
  # exactly 1 step
1082
1156
  if len(all_cycles) == 1:
1083
1157
  this_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
1084
- all_steps_data = norm_y_and_add_features_to_one_step(this_step, cfg)
1085
- 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)
1086
1170
  # 2 or more steps - build dataframe
1087
1171
  elif len(all_cycles) > 1:
1088
1172
  # first- step is added manually
1089
1173
  first_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
1090
- first_step = norm_y_and_add_features_to_one_step(first_step, cfg)
1091
- all_steps_data = first_step
1092
- 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)
1093
1186
  # some prep for addition of further steps
1094
1187
  sc_num = len(all_cycles)
1095
1188
  nanvector = data_copy.loc[[1]]
@@ -1105,13 +1198,29 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1105
1198
  with warnings.catch_warnings():
1106
1199
  warnings.simplefilter("ignore")
1107
1200
  numvector[:] = s + 1
1108
- all_steps_data = add_step_separators(all_steps_data, nanvector, numvector)
1109
1201
  # this_step
1110
1202
  this_step = data_copy.loc[all_cycles[s][0] : all_cycles[s][1]]
1111
- 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)
1112
1216
  all_steps_data = pd.concat([all_steps_data, this_step], axis=0)
1113
- # this_normalised_step
1114
- 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
+ )
1115
1224
  normalised_steps_data = add_step_separators(
1116
1225
  normalised_steps_data, nanvector, numvector
1117
1226
  )
@@ -1119,6 +1228,8 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1119
1228
  [normalised_steps_data, this_normalised_step], axis=0
1120
1229
  )
1121
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
1122
1233
  average_data, std_data = compute_average_and_std_data(
1123
1234
  name, normalised_steps_data, bin_num, analyse_average_x
1124
1235
  )
@@ -1128,6 +1239,8 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1128
1239
  results["average_data"] = average_data
1129
1240
  results["std_data"] = std_data
1130
1241
  results["all_cycles"] = all_cycles
1242
+ if standardise_x_coordinates:
1243
+ results["x_standardised_steps_data"] = x_standardised_steps_data
1131
1244
  # save to files
1132
1245
  save_results_sheet(
1133
1246
  all_steps_data,
@@ -1147,27 +1260,47 @@ def analyse_and_export_stepcycles(data, all_cycles, info, folderinfo, cfg):
1147
1260
  save_results_sheet(
1148
1261
  std_data, save_to_xls, os.path.join(results_dir, name + STD_XLS_FILENAME)
1149
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
+ )
1150
1269
  return results
1151
1270
 
1152
1271
 
1153
1272
  # ......................................................................................
1154
- # ............... helper functions a - norm z and add features .......................
1273
+ # ............ helper functions a - standardise x, y and add features ................
1155
1274
  # ......................................................................................
1156
1275
 
1157
1276
 
1158
- def norm_y_and_add_features_to_one_step(step, cfg):
1159
- """For a single step cycle's data, normalise z if wanted, flip y columns if needed
1160
- (to simulate equal run direction) and add features (angles & velocities)
1161
- """
1162
- # 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
1163
1280
  step_copy = step.copy()
1164
- if cfg["normalise_height_at_SC_level"] is True:
1281
+ if cfg["standardise_y_at_SC_level"] is True:
1165
1282
  y_cols = [col for col in step_copy.columns if col.endswith("y")]
1166
- 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()
1167
1288
  step_copy[y_cols] -= this_y_min
1168
1289
  # add angles and velocities
1169
1290
  step_copy = add_features(step_copy, cfg)
1170
- 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
1171
1304
 
1172
1305
 
1173
1306
  def add_features(step, cfg):
@@ -1397,6 +1530,22 @@ def add_step_separators(dataframe, nanvector, numvector):
1397
1530
 
1398
1531
  # .............................. master function .............................
1399
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
+
1400
1549
  # A note on updated colour cyclers after pull request that was merged 20.06.2024
1401
1550
  # => Using color palettes instead of colour maps as I had previously means that
1402
1551
  # we cycle through neighbouring colours
@@ -1443,6 +1592,9 @@ def plot_results(info, results, folderinfo, cfg, plot_panel_instance):
1443
1592
  # ...............................3 - angles by time.................................
1444
1593
  if angles["name"]:
1445
1594
  plot_angles_by_time(all_steps_data, sc_idxs, info, cfg, plot_panel_instance)
1595
+ # regularly closing figures to save memory
1596
+ # => no problem to do this since we pass figure-vars to save-functions and PlotPanel
1597
+ plt.close("all")
1446
1598
 
1447
1599
  # ..........................4 - hindlimb stick diagram..............................
1448
1600
  plot_hindlimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_instance)
@@ -1461,6 +1613,7 @@ def plot_results(info, results, folderinfo, cfg, plot_panel_instance):
1461
1613
  plot_joint_x_by_average_SC(
1462
1614
  average_data, std_data, info, cfg, plot_panel_instance
1463
1615
  )
1616
+ plt.close("all")
1464
1617
 
1465
1618
  # ........................8 - average angles over SC percentage.....................
1466
1619
  if angles["name"]:
@@ -1491,11 +1644,12 @@ def plot_results(info, results, folderinfo, cfg, plot_panel_instance):
1491
1644
  plot_angular_acceleration_by_average_SC(
1492
1645
  average_data, std_data, info, cfg, plot_panel_instance
1493
1646
  )
1647
+ plt.close("all")
1494
1648
 
1495
1649
  # ........................optional - 13 - build plot panel..........................
1496
- if cfg["dont_show_plots"] is True:
1650
+ if dont_show_plots is True:
1497
1651
  pass # going on without building the plot window
1498
- elif cfg["dont_show_plots"] is False: # -> show plot panel
1652
+ elif dont_show_plots is False: # -> show plot panel
1499
1653
  # Destroy loading screen and build plot panel with all figures
1500
1654
  plot_panel_instance.destroy_plot_panel_loading_screen()
1501
1655
  plot_panel_instance.build_plot_panel()
@@ -1763,7 +1917,7 @@ def plot_hindlimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1763
1917
  ax.plot(this_xs, this_ys, color=this_color, label=this_label)
1764
1918
  else:
1765
1919
  ax.plot(this_xs, this_ys, color=this_color)
1766
- ax.set_title(name + " - Hindlimb Stick Diagram")
1920
+ ax.set_title(name + " - Primary Stick Diagram")
1767
1921
  # legend adjustments
1768
1922
  if legend_outside is True:
1769
1923
  ax.legend(
@@ -1779,7 +1933,7 @@ def plot_hindlimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1779
1933
  else:
1780
1934
  ax.set_xlabel("x (pixels)")
1781
1935
  ax.set_ylabel("y (pixels)")
1782
- figure_file_string = " - Hindlimb Stick Diagram"
1936
+ figure_file_string = " - Primary Stick Diagram"
1783
1937
  save_figures(f, results_dir, name, figure_file_string)
1784
1938
  if dont_show_plots:
1785
1939
  plt.close(f)
@@ -1827,7 +1981,7 @@ def plot_forelimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1827
1981
  ax.plot(this_xs, this_ys, color=this_color, label=this_label)
1828
1982
  else:
1829
1983
  ax.plot(this_xs, this_ys, color=this_color)
1830
- ax.set_title(name + " - Forelimb Stick Diagram")
1984
+ ax.set_title(name + " - Secondary Stick Diagram")
1831
1985
  if convert_to_mm:
1832
1986
  tickconvert_mm_to_cm(ax, "both")
1833
1987
  # legend adjustments
@@ -1845,7 +1999,7 @@ def plot_forelimb_stickdiagram(all_steps_data, sc_idxs, info, cfg, plot_panel_in
1845
1999
  else:
1846
2000
  ax.set_xlabel("x (pixels)")
1847
2001
  ax.set_ylabel("y (pixels)")
1848
- figure_file_string = " - Forelimb Stick Diagram"
2002
+ figure_file_string = " - Secondary Stick Diagram"
1849
2003
  save_figures(f, results_dir, name, figure_file_string)
1850
2004
  if dont_show_plots:
1851
2005
  plt.close(f)
@@ -2427,6 +2581,10 @@ class PlotPanel:
2427
2581
  f"AutoGaitA Plot Panel {self.current_fig_index+1}/{len(self.figures)}"
2428
2582
  )
2429
2583
 
2584
+ def destroy_plot_panel(self):
2585
+ # Needed if no SCs after checks
2586
+ self.loading_screen.destroy()
2587
+
2430
2588
 
2431
2589
  # %% local functions 5 - print finish
2432
2590