autogaita 1.2.0__tar.gz → 1.3.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 (90) hide show
  1. {autogaita-1.2.0 → autogaita-1.3.0}/PKG-INFO +1 -1
  2. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_2_sc_extraction.py +22 -10
  3. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_3_analysis.py +28 -29
  4. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_utils.py +79 -2
  5. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_1_preparation.py +137 -85
  6. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_main.py +3 -2
  7. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/utils.py +68 -3
  8. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_1_preparation.py +2 -35
  9. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_2_sc_extraction.py +96 -0
  10. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_3_analysis.py +25 -31
  11. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita.egg-info/PKG-INFO +1 -1
  12. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita.egg-info/SOURCES.txt +1 -0
  13. {autogaita-1.2.0 → autogaita-1.3.0}/setup.py +1 -1
  14. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_common2D_unit_1_preparation.py +5 -0
  15. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_common2D_unit_2_sc_extraction.py +55 -3
  16. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_common2D_unit_3_analysis.py +2 -31
  17. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_group_unit.py +25 -7
  18. autogaita-1.3.0/tests/test_universal3D_unit_2_sc_extraction.py +108 -0
  19. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_universal3D_unit_3_analysis.py +24 -11
  20. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_utils.py +97 -5
  21. {autogaita-1.2.0 → autogaita-1.3.0}/LICENSE +0 -0
  22. {autogaita-1.2.0 → autogaita-1.3.0}/README.md +0 -0
  23. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/__init__.py +0 -0
  24. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/__main__.py +0 -0
  25. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/__init__.py +0 -0
  26. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/dlc_multirun.py +0 -0
  27. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/dlc_singlerun.py +0 -0
  28. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_dlcrun.py +0 -0
  29. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_dlcrun_forpaper.py +0 -0
  30. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_universal3Drun.py +0 -0
  31. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/sleap_singlerun.py +0 -0
  32. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/universal3D_multirun.py +0 -0
  33. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/batchrun_scripts/universal3D_singlerun.py +0 -0
  34. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/__init__.py +0 -0
  35. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_1_preparation.py +0 -0
  36. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_4_plots.py +0 -0
  37. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/common2D/common2D_constants.py +0 -0
  38. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/dlc/__init__.py +0 -0
  39. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/dlc/dlc_main.py +0 -0
  40. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/__init__.py +0 -0
  41. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_2_data_processing.py +0 -0
  42. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_3_PCA.py +0 -0
  43. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_4_stats.py +0 -0
  44. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_5_plots.py +0 -0
  45. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_constants.py +0 -0
  46. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/group/group_utils.py +0 -0
  47. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/__init__.py +0 -0
  48. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_advanced_config_gui.py +0 -0
  49. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_columninfo_gui.py +0 -0
  50. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_gui_constants.py +0 -0
  51. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_gui_utils.py +0 -0
  52. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_main_gui.py +0 -0
  53. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/common2D_run_and_done_gui.py +0 -0
  54. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/dlc_gui.py +0 -0
  55. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/dlc_gui_config.json +0 -0
  56. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/first_level_gui_utils.py +0 -0
  57. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/gaita_widgets.py +0 -0
  58. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/group_gui.py +0 -0
  59. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/group_gui_config.json +0 -0
  60. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/gui_constants.py +0 -0
  61. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/gui_utils.py +0 -0
  62. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/main_gui.py +0 -0
  63. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/sleap_gui.py +0 -0
  64. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/sleap_gui_config.json +0 -0
  65. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/universal3D_gui.py +0 -0
  66. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/gui/universal3D_gui_config.json +0 -0
  67. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/__init__.py +0 -0
  68. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/constants.py +0 -0
  69. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/icon.icns +0 -0
  70. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/icon.ico +0 -0
  71. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/logo.png +0 -0
  72. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/resources/pic_to_demo_for_repo.png +0 -0
  73. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/sleap/__init__.py +0 -0
  74. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/sleap/sleap_main.py +0 -0
  75. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/__init__.py +0 -0
  76. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_4_plots.py +0 -0
  77. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_constants.py +0 -0
  78. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_datafile_preparation.py +0 -0
  79. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_main.py +0 -0
  80. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita/universal3D/universal3D_utils.py +0 -0
  81. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita.egg-info/dependency_links.txt +0 -0
  82. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita.egg-info/requires.txt +0 -0
  83. {autogaita-1.2.0 → autogaita-1.3.0}/autogaita.egg-info/top_level.txt +0 -0
  84. {autogaita-1.2.0 → autogaita-1.3.0}/setup.cfg +0 -0
  85. {autogaita-1.2.0 → autogaita-1.3.0}/tests/__init__.py +0 -0
  86. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_dlc_approval.py +0 -0
  87. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_dlc_unit_1_preparation.py +0 -0
  88. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_group_approval.py +0 -0
  89. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_universal3D_approval.py +0 -0
  90. {autogaita-1.2.0 → autogaita-1.3.0}/tests/test_universal3D_unit_1_preparation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: autogaita
3
- Version: 1.2.0
3
+ Version: 1.3.0
4
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
@@ -4,6 +4,7 @@ from autogaita.common2D.common2D_utils import (
4
4
  check_cycle_out_of_bounds,
5
5
  check_cycle_duplicates,
6
6
  check_cycle_order,
7
+ check_differing_angle_joint_coords,
7
8
  check_tracking_xy_thresholds,
8
9
  check_tracking_SLEAP_nans,
9
10
  handle_issues,
@@ -208,14 +209,25 @@ def extract_stepcycles(tracking_software, data, info, folderinfo, cfg):
208
209
  # ............................ clean all_cycles ..................................
209
210
  # check if we skipped latencies because they were out of data-bounds
210
211
  all_cycles = check_cycle_out_of_bounds(all_cycles)
211
- if all_cycles: # can be None if all SCs were out of bounds
212
- # check if there are any duplicates (e.g., SC2's start-lat == SC1's end-lat)
213
- all_cycles = check_cycle_duplicates(all_cycles)
214
- # check if user input progressively later latencies
215
- all_cycles = check_cycle_order(all_cycles, info)
216
- # check if tracking broke for any SCs using user-provided x and y thresholds
217
- all_cycles = check_tracking_xy_thresholds(data, info, all_cycles, cfg)
218
- # for SLEAP - check if there were any NaNs in any joints/angle-joints in SCs
219
- if tracking_software == "SLEAP":
220
- all_cycles = check_tracking_SLEAP_nans(data, info, all_cycles, cfg)
212
+ if not all_cycles: # returns None if no clean cycles found
213
+ return None
214
+ # check if there are any duplicates (e.g., SC2's start-lat == SC1's end-lat)
215
+ all_cycles = check_cycle_duplicates(all_cycles) # doesnt return None!
216
+ # check if user input progressively later latencies
217
+ all_cycles = check_cycle_order(all_cycles, info)
218
+ if not all_cycles: # returns empty list if no clean cycles found
219
+ return None
220
+ # check that joints used in angle computations have different coords at all tps
221
+ all_cycles = check_differing_angle_joint_coords(all_cycles, data, info, cfg)
222
+ if not all_cycles:
223
+ return None
224
+ # check if tracking broke for any SCs using user-provided x and y thresholds
225
+ all_cycles = check_tracking_xy_thresholds(all_cycles, data, info, cfg)
226
+ if not all_cycles:
227
+ return None
228
+ # for SLEAP - check if there were any NaNs in any joints/angle-joints in SCs
229
+ if tracking_software == "SLEAP":
230
+ all_cycles = check_tracking_SLEAP_nans(all_cycles, data, info, cfg)
231
+ if not all_cycles:
232
+ return None
221
233
  return all_cycles
@@ -1,10 +1,13 @@
1
1
  # %% imports
2
- from autogaita.resources.utils import bin_num_to_percentages
2
+ from autogaita.resources.utils import (
3
+ bin_num_to_percentages,
4
+ compute_angle,
5
+ write_angle_warning,
6
+ )
3
7
  import os
4
8
  import warnings
5
9
  import pandas as pd
6
10
  import numpy as np
7
- import math
8
11
 
9
12
  # %% constants
10
13
  from autogaita.resources.constants import TIME_COL, SC_PERCENTAGE_COL
@@ -50,14 +53,14 @@ def analyse_and_export_stepcycles(data, all_cycles, info, cfg):
50
53
  this_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
51
54
  if standardise_x_coordinates:
52
55
  all_steps_data, x_standardised_steps_data = (
53
- standardise_x_y_and_add_features_to_one_step(this_step, cfg)
56
+ standardise_x_y_and_add_features_to_one_step(this_step, info, cfg)
54
57
  )
55
58
  normalised_steps_data = normalise_one_steps_data(
56
59
  x_standardised_steps_data, bin_num
57
60
  )
58
61
  else:
59
62
  all_steps_data = standardise_x_y_and_add_features_to_one_step(
60
- this_step, cfg
63
+ this_step, info, cfg
61
64
  )
62
65
  normalised_steps_data = normalise_one_steps_data(all_steps_data, bin_num)
63
66
  # 2 or more steps - build dataframe
@@ -69,14 +72,14 @@ def analyse_and_export_stepcycles(data, all_cycles, info, cfg):
69
72
  first_step = data_copy.loc[all_cycles[0][0] : all_cycles[0][1]]
70
73
  if standardise_x_coordinates:
71
74
  all_steps_data, x_standardised_steps_data = (
72
- standardise_x_y_and_add_features_to_one_step(first_step, cfg)
75
+ standardise_x_y_and_add_features_to_one_step(first_step, info, cfg)
73
76
  )
74
77
  normalised_steps_data = normalise_one_steps_data(
75
78
  x_standardised_steps_data, bin_num
76
79
  )
77
80
  else:
78
81
  all_steps_data = standardise_x_y_and_add_features_to_one_step(
79
- first_step, cfg
82
+ first_step, info, cfg
80
83
  )
81
84
  normalised_steps_data = normalise_one_steps_data(all_steps_data, bin_num)
82
85
  # some prep for addition of further steps
@@ -98,13 +101,15 @@ def analyse_and_export_stepcycles(data, all_cycles, info, cfg):
98
101
  this_step = data_copy.loc[all_cycles[s][0] : all_cycles[s][1]]
99
102
  if standardise_x_coordinates:
100
103
  this_step, this_x_standardised_step = (
101
- standardise_x_y_and_add_features_to_one_step(this_step, cfg)
104
+ standardise_x_y_and_add_features_to_one_step(this_step, info, cfg)
102
105
  )
103
106
  this_normalised_step = normalise_one_steps_data(
104
107
  this_x_standardised_step, bin_num
105
108
  )
106
109
  else:
107
- this_step = standardise_x_y_and_add_features_to_one_step(this_step, cfg)
110
+ this_step = standardise_x_y_and_add_features_to_one_step(
111
+ this_step, info, cfg
112
+ )
108
113
  this_normalised_step = normalise_one_steps_data(this_step, bin_num)
109
114
  # step separators & step-to-rest-concatenation
110
115
  # => note that normalised_step is already based on x-stand if required
@@ -170,7 +175,7 @@ def analyse_and_export_stepcycles(data, all_cycles, info, cfg):
170
175
  # ......................................................................................
171
176
 
172
177
 
173
- def standardise_x_y_and_add_features_to_one_step(step, cfg):
178
+ def standardise_x_y_and_add_features_to_one_step(step, info, cfg):
174
179
  """For a single step cycle's data, standardise x & y if wanted and add features"""
175
180
  # if user wanted this, standardise y (height) at step-cycle level
176
181
  step_copy = step.copy()
@@ -184,11 +189,11 @@ def standardise_x_y_and_add_features_to_one_step(step, cfg):
184
189
  step_copy[y_cols] -= this_y_min
185
190
  # if no x-standardisation, just add features & return non-(x-)normalised step
186
191
  if cfg["standardise_x_coordinates"] is False:
187
- non_stand_step = add_features(step_copy, cfg)
192
+ non_stand_step = add_features(step_copy, info, cfg)
188
193
  return non_stand_step
189
194
  # else standardise x (horizontal dimension) at step-cycle level too
190
195
  else:
191
- non_stand_step = add_features(step_copy, cfg)
196
+ non_stand_step = add_features(step_copy, info, cfg)
192
197
  x_stand_step = step_copy.copy()
193
198
  x_cols = [col for col in x_stand_step.columns if col.endswith("x")]
194
199
  # note the [0] here is important because it's still a list of len=1!!
@@ -196,11 +201,11 @@ def standardise_x_y_and_add_features_to_one_step(step, cfg):
196
201
  cfg["x_standardisation_joint"][0] + "x"
197
202
  ].min()
198
203
  x_stand_step[x_cols] -= min_x_standardisation_joint
199
- x_stand_step = add_features(x_stand_step, cfg)
204
+ x_stand_step = add_features(x_stand_step, info, cfg)
200
205
  return non_stand_step, x_stand_step
201
206
 
202
207
 
203
- def add_features(step, cfg):
208
+ def add_features(step, info, cfg):
204
209
  """Add Features, i.e. Angles & Velocities"""
205
210
  # unpack
206
211
  hind_joints = cfg["hind_joints"]
@@ -208,12 +213,12 @@ def add_features(step, cfg):
208
213
  if hind_joints:
209
214
  step = add_x_velocities(step, cfg)
210
215
  if angles["name"]: # if there is at least 1 string in the list
211
- step = add_angles(step, cfg)
216
+ step = add_angles(step, info, cfg)
212
217
  step = add_angular_velocities(step, cfg)
213
218
  return step
214
219
 
215
220
 
216
- def add_angles(step, cfg):
221
+ def add_angles(step, info, cfg):
217
222
  """Feature #1: Joint Angles"""
218
223
  # unpack
219
224
  angles = cfg["angles"]
@@ -234,26 +239,20 @@ def add_angles(step, cfg):
234
239
  joint3[:, 1] = step[upper_joint + "y"]
235
240
  # initialise the angle vector and assign looping over timepoints
236
241
  this_angle = np.zeros(len(joint_angle))
242
+ broken_angle_idxs = [] # initialise broken idxs-list for each angle anew
237
243
  for t in range(len(joint_angle)):
238
- this_angle[t] = compute_angle(joint_angle[t, :], joint2[t, :], joint3[t, :])
244
+ this_angle[t], broken = compute_angle(
245
+ joint_angle[t, :], joint2[t, :], joint3[t, :]
246
+ )
247
+ if broken:
248
+ broken_angle_idxs.append(t)
249
+ if broken_angle_idxs:
250
+ write_angle_warning(step, a, angles, broken_angle_idxs, info)
239
251
  this_colname = angle + "Angle"
240
252
  step[this_colname] = this_angle
241
253
  return step
242
254
 
243
255
 
244
- def compute_angle(joint_angle, joint2, joint3):
245
- """Compute a given angle at a joint & a given timepoint"""
246
- # Get vectors between the joints
247
- v1 = (joint_angle[0] - joint2[0], joint_angle[1] - joint2[1])
248
- v2 = (joint_angle[0] - joint3[0], joint_angle[1] - joint3[1])
249
- # dot product, magnitude of vectors, angle in radians & convert 2 degrees
250
- dot_product = v1[0] * v2[0] + v1[1] * v2[1]
251
- mag_v1 = math.sqrt(v1[0] ** 2 + v1[1] ** 2)
252
- mag_v2 = math.sqrt(v2[0] ** 2 + v2[1] ** 2)
253
- angle = math.acos(dot_product / (mag_v1 * mag_v2))
254
- return math.degrees(angle)
255
-
256
-
257
256
  def add_x_velocities(step, cfg):
258
257
  """Feature #2: Joint x Velocities & Accelerations"""
259
258
  # unpack
@@ -6,6 +6,9 @@ import copy
6
6
  import numpy as np
7
7
  import tkinter as tk
8
8
 
9
+ # %% constants
10
+ from autogaita.resources.constants import TIME_COL
11
+
9
12
 
10
13
  def run_singlerun_in_multirun(tracking_software, idx, info, folderinfo, cfg):
11
14
  """When performing a multirun, either via Batch Analysis in GUI or batchrun scripts, run the analysis for a given dataset"""
@@ -210,7 +213,81 @@ def check_cycle_order(all_cycles, info):
210
213
  return clean_cycles
211
214
 
212
215
 
213
- def check_tracking_xy_thresholds(data, info, all_cycles, cfg):
216
+ def check_differing_angle_joint_coords(all_cycles, data, info, cfg):
217
+ """Check if none of the joints used for angle computations later have equal values (since this would lead to math.domain errors due to floating point precision)"""
218
+
219
+ # Note
220
+ # ----
221
+ # In theory, I could fix this programatically in the add_angle function, but I feel
222
+ # like joint-coords should not often be exactly equal like this in a meaningful way
223
+ # We can still change it in the future.
224
+
225
+ # unpack
226
+ angles = cfg["angles"]
227
+
228
+ clean_cycles = None
229
+ for c, cycle in enumerate(all_cycles): # for each SC
230
+ cycle = check_a_single_cycle_for_joint_coords(cycle, angles, data, c, info)
231
+ if cycle: # if cycle was not valid (equal-joint-coords) this returns None
232
+ if clean_cycles == None:
233
+ clean_cycles = [cycle] # also makes a 2xscs list of lists
234
+ else:
235
+ clean_cycles.append(cycle)
236
+ return clean_cycles
237
+
238
+
239
+ def check_a_single_cycle_for_joint_coords(cycle, angles, data, c, info):
240
+ for a in range(len(angles["name"])): # for each angle configuration
241
+ # prepare a dict that has only the data of this angle config's joints
242
+ this_angle_data = {"name": [], "lower_joint": [], "upper_joint": []}
243
+ for key in this_angle_data.keys():
244
+ this_joint = angles[key][a]
245
+ this_angle_data[key] = np.array(
246
+ [data[this_joint + "x"], data[this_joint + "y"]]
247
+ )
248
+ # now check if any of the joints have the same coord at any idx
249
+ for idx in range(cycle[0], cycle[1]):
250
+ if (
251
+ np.array_equal(
252
+ this_angle_data["name"][:, idx],
253
+ this_angle_data["lower_joint"][:, idx],
254
+ )
255
+ or np.array_equal(
256
+ this_angle_data["name"][:, idx],
257
+ this_angle_data["upper_joint"][:, idx],
258
+ )
259
+ or np.array_equal(
260
+ this_angle_data["lower_joint"][:, idx],
261
+ this_angle_data["upper_joint"][:, idx],
262
+ )
263
+ ):
264
+ this_message = (
265
+ "\n***********\n! WARNING !\n***********\n"
266
+ + f"SC #{c + 1} has equal joint coordinates at "
267
+ + f"{round(data[TIME_COL][idx],4)}s:"
268
+ + "\n\nAngle - [x y]:\n"
269
+ + angles["name"][a]
270
+ + " - "
271
+ + str(this_angle_data["name"][:, idx])
272
+ + "\nLower joint: "
273
+ + angles["lower_joint"][a]
274
+ + " - "
275
+ + str(this_angle_data["lower_joint"][:, idx])
276
+ + "\nUpper joint: "
277
+ + angles["upper_joint"][a]
278
+ + " - "
279
+ + str(this_angle_data["upper_joint"][:, idx])
280
+ + "\nRemoving the SC from "
281
+ + f"{round(data[TIME_COL][cycle[0]], 4)}-"
282
+ + f"{round(data[TIME_COL][cycle[1]], 4)}s"
283
+ )
284
+ print(this_message)
285
+ write_issues_to_textfile(this_message, info)
286
+ return None # removes this SC
287
+ return cycle # if we never returned None, this SC is valid
288
+
289
+
290
+ def check_tracking_xy_thresholds(all_cycles, data, info, cfg):
214
291
  """Check if any x/y column of any joint has broken datapoints"""
215
292
  # unpack
216
293
  convert_to_mm = cfg["convert_to_mm"]
@@ -255,7 +332,7 @@ def check_tracking_xy_thresholds(data, info, all_cycles, cfg):
255
332
  return clean_cycles
256
333
 
257
334
 
258
- def check_tracking_SLEAP_nans(data, info, all_cycles, cfg):
335
+ def check_tracking_SLEAP_nans(all_cycles, data, info, cfg):
259
336
  """In SLEAP if tracking fails it generates NaNs - make sure we don't have those in any SC in any joint or angle-joint"""
260
337
  # unpack
261
338
  hind_joints = cfg["hind_joints"]
@@ -23,6 +23,28 @@ from autogaita.group.group_constants import (
23
23
  def some_prep(folderinfo, cfg):
24
24
  """Add some folderinfo & cfg variables to the dictionaries for further processes"""
25
25
 
26
+ # AN IMPORTANT NOTE ABOUT LOAD_DIR
27
+ # --------------------------------
28
+ # Alright so the group pipeline's cfg (and thus, of course, config.json) is a bit
29
+ # special because it includes:
30
+ # 1) first-level config-keys, such as "joints" or "angles", that reflect what has
31
+ # been analysed at the first level. These are also checked for equivalence
32
+ # across groups when running this without load_dir (see the for g_idx loop
33
+ # below) to ensure we are not comparing different sampling rates or so with a
34
+ # group analysis
35
+ # 2) group-level config-keys, such as "do_permtest" or "PCA_variables" that define
36
+ # how group analysis should be done
37
+ # Now:
38
+ # When loading previously generated group dfs (i.e., using load_dir), the vars in
39
+ # (2) should naturally be changing so the user can change "PCA_variables" or
40
+ # "do_anova". The config.json is coded to reflect the group-keys of the most
41
+ # recent analysis. The "first-level" config keys are, however, just checked for
42
+ # equivalence once and then never changed by group gaita. So if users should
43
+ # repeatedly run analyses in the same results_dir, the config.json file includes
44
+ # the first-level keys of the first run and the group-level keys of the most recent
45
+ # run. This is not an issue per se but very likely something that I might forget in
46
+ # a year thus here is a note.
47
+
26
48
  # unpack
27
49
  group_names = folderinfo["group_names"]
28
50
  group_dirs = folderinfo["group_dirs"]
@@ -49,23 +71,16 @@ def some_prep(folderinfo, cfg):
49
71
  if os.path.exists(info_file_path):
50
72
  os.remove(info_file_path)
51
73
 
52
- # *********** IMPORTANT ***********
53
- # if load_dir, we have already saved a group config.json (see below before return)
54
- # => use this and just return the cfg
55
- # => make sure to write folderinfo["contrast"] manually above as is and then return
56
- # that folderinfo plus the cfg you load from the file
74
+ # load a couple necessary first-level cfg vars from previous run's group config.json
57
75
  if len(folderinfo["load_dir"]) > 0:
58
- with open(
59
- os.path.join(folderinfo["load_dir"], CONFIG_JSON_FILENAME), "r"
60
- ) as config_json_file:
61
- cfg = json.load(config_json_file)
62
- cfg["loaded"] = True # used in a unit test in test_group_unit.py
76
+ cfg = load_previous_runs_first_level_cfg_vars(folderinfo, cfg)
63
77
 
64
- else: # if not, do the below things based on group dirs' configs
65
- # 1. extracted_cfg_vars: save_to_xls, PCA stuff & dont show plots
66
- cfg = extract_cfg_vars(folderinfo, cfg)
78
+ # define save_to_xls and test PCA
79
+ cfg = extract_save_to_xls_and_test_PCA_config(folderinfo, cfg)
67
80
 
68
- # 2. ennsure each key's across-group-equivalence and then add to cfg dict
81
+ # if not loading previous results, ensure cfg-keys are equivalent across groups
82
+ # then add them to cfg dict
83
+ if len(folderinfo["load_dir"]) == 0:
69
84
  for g_idx, group_dir in enumerate(group_dirs):
70
85
  with open(
71
86
  os.path.join(group_dir, CONFIG_JSON_FILENAME), "r"
@@ -84,24 +99,31 @@ def some_prep(folderinfo, cfg):
84
99
  "config.json variables differ between groups!"
85
100
  + "\nPlease make sure that all cfg variables between "
86
101
  + "groups match & try again!"
102
+ + f"\nMismatch at {key} in group {group_names[g_idx]}"
87
103
  )
88
104
  raise ValueError(error_message)
89
105
  else:
90
106
  cfg[key] = config_vars_from_json[key]
91
107
 
92
- # 3. rename hind_joints is to joints if DLC or SLEAP
93
- if "hind_joints" in cfg.keys():
94
- cfg["joints"] = cfg["hind_joints"]
108
+ # rename hind_joints to joints (if DLC or SLEAP)
109
+ if "hind_joints" in cfg.keys():
110
+ cfg["joints"] = cfg["hind_joints"]
95
111
 
96
- # ******** IMPORTANT *******
97
- # => Do the following two things regardless of load_dir:
98
- # 1. save cfg to json file in results_dir for load_dir capability
112
+ # update cfg keys in json file in results_dir
113
+ # => i.e. if there's already a config.json @ results_dir (happens if load_dir was
114
+ # True) the if-condition below updates (only) the group-config keys according to
115
+ # this run's cfg dict
116
+ # => this also means that first-level cfg keys are never changed (which is intended)
99
117
  config_json_path = os.path.join(results_dir, CONFIG_JSON_FILENAME)
100
- if os.path.exists(config_json_path): # overwrite if exists
101
- os.remove(config_json_path)
118
+ if os.path.exists(config_json_path):
119
+ with open(config_json_path, "r") as config_json_file:
120
+ existing_cfg = json.load(config_json_file)
121
+ existing_cfg.update({key: cfg[key] for key in cfg if key in existing_cfg})
122
+ cfg = existing_cfg # update cfg with existing keys
102
123
  with open(config_json_path, "w") as config_json_file:
103
124
  json.dump(cfg, config_json_file)
104
- # 2. create this plot stuff manually (cycler objects cannot be written to json)
125
+
126
+ # create this plot stuff manually (cycler objects cannot be written to json)
105
127
  cfg["group_color_cycler"] = plt.cycler(
106
128
  "color", sns.color_palette(cfg["color_palette"], len(group_names))
107
129
  )
@@ -115,76 +137,40 @@ def some_prep(folderinfo, cfg):
115
137
  "color", sns.color_palette(cfg["color_palette"], len(cfg["angles"]["name"]))
116
138
  )
117
139
 
140
+ # have this key for a unit test - make sure it's never written to json
141
+ if len(folderinfo["load_dir"]) > 0:
142
+ cfg["loaded"] = True
143
+
118
144
  return folderinfo, cfg
119
145
 
120
146
 
121
- def extract_cfg_vars(folderinfo, cfg):
147
+ def load_previous_runs_first_level_cfg_vars(folderinfo, cfg):
148
+ """There are only a few "first-level" cfg vars (like "joints") we require for group gaita's workflow - load them here"""
149
+ with open(
150
+ os.path.join(folderinfo["load_dir"], CONFIG_JSON_FILENAME), "r"
151
+ ) as config_json_file:
152
+ old_cfg = json.load(config_json_file)
153
+ cfg["save_to_xls"] = old_cfg["save_to_xls"]
154
+ cfg["joints"] = old_cfg["joints"]
155
+ cfg["angles"] = old_cfg["angles"]
156
+ return cfg
157
+
158
+
159
+ def extract_save_to_xls_and_test_PCA_config(folderinfo, cfg):
122
160
  """Extract save_to_xls from example Normalised dfs and sanity check
123
- that they match between groups. Also some stuff for PCA!
161
+ that they match between groups. Also some tests for users' PCA config!
124
162
  """
125
163
 
126
- group_names = folderinfo["group_names"]
127
- group_dirs = folderinfo["group_dirs"]
164
+ # NOTE
165
+ # ----
166
+ # save_to_xls is a list of bools that is infered from file type of group's sheet
167
+ # files - only when not using load_dir. if we use load_dir, save_to_xls is loaded
168
+ # by load_previous_runs_first_level_cfg_vars
128
169
 
129
170
  # ................................ save_to_xls ...................................
130
- save_to_xls = [None] * len(group_dirs)
131
- for g, group_dir in enumerate(group_dirs):
132
- all_results_folders = os.listdir(
133
- group_dir
134
- ) # remove no-results valid_results_folders
135
- valid_results_folders = []
136
- # => Note if there's ambiguity / mixed filetypes, we set save_to_xls to True!
137
- sheet_type_mismatch_message = (
138
- "\n***********\n! WARNING !\n***********\n"
139
- + "Mismatch in sheet file types for group "
140
- + group_names[g]
141
- + "!\nSaving all output sheets to"
142
- + ".xlsx!\nRe-run first level & only save .csvs if "
143
- + "you want .csv files of group results!"
144
- )
145
- for folder in all_results_folders:
146
- # create save_to_xls here, there are two cases we have to deal with:
147
- # case 1: we found a csv file
148
- if os.path.exists(
149
- os.path.join(
150
- group_dir,
151
- folder,
152
- folder + " - " + ORIG_SHEET_NAME + ".csv",
153
- )
154
- ):
155
- valid_results_folders.append(folder)
156
- if save_to_xls[g] is None:
157
- save_to_xls[g] = False
158
- if save_to_xls[g] is True:
159
- print(sheet_type_mismatch_message)
160
- write_issues_to_textfile(sheet_type_mismatch_message, folderinfo)
161
- # case 2: we found a xlsx file
162
- elif os.path.exists(
163
- os.path.join(
164
- group_dir,
165
- folder,
166
- folder + " - " + ORIG_SHEET_NAME + ".xlsx",
167
- )
168
- ):
169
- valid_results_folders.append(folder)
170
- if save_to_xls[g] is None:
171
- save_to_xls[g] = True
172
- if save_to_xls[g] is False:
173
- save_to_xls[g] = True
174
- print(sheet_type_mismatch_message)
175
- write_issues_to_textfile(sheet_type_mismatch_message, folderinfo)
176
- # test that at least 1 folder has valid results for all groups
177
- if not valid_results_folders:
178
- no_valid_results_error = (
179
- "\n*********\n! ERROR !\n*********\n"
180
- + "No valid results folder found for "
181
- + group_names[g]
182
- + "\nFix & re-run!"
183
- )
184
- print(no_valid_results_error)
185
- write_issues_to_textfile(no_valid_results_error, folderinfo)
186
- # assign to our cfg dict after group loop
187
- cfg["save_to_xls"] = save_to_xls
171
+ if len(folderinfo["load_dir"]) == 0:
172
+ # infer save_to_xls from sheet files
173
+ cfg["save_to_xls"] = infer_save_to_xls_from_group_dirs_sheetfiles(folderinfo)
188
174
 
189
175
  # ......................... test if PCA config is valid ..........................
190
176
  # only test if user wants PCA (ie. selected any features) and is not using the
@@ -272,3 +258,69 @@ def extract_cfg_vars(folderinfo, cfg):
272
258
  cfg["PCA_bins"] = cfg["PCA_bins"][:-1]
273
259
 
274
260
  return cfg
261
+
262
+
263
+ def infer_save_to_xls_from_group_dirs_sheetfiles(folderinfo):
264
+ """Generate a list of save_to_xls bools that is automatically inferred from sheet file in group dir"""
265
+
266
+ # unpack
267
+ group_names = folderinfo["group_names"]
268
+ group_dirs = folderinfo["group_dirs"]
269
+
270
+ save_to_xls = [None] * len(group_dirs)
271
+ for g, group_dir in enumerate(group_dirs):
272
+ all_results_folders = os.listdir(
273
+ group_dir
274
+ ) # remove no-results valid_results_folders
275
+ valid_results_folders = []
276
+ # => Note if there are mixed filetypes, we set save_to_xls to True!
277
+ sheet_type_mismatch_message = (
278
+ "\n***********\n! WARNING !\n***********\n"
279
+ + "Mismatch in sheet file types for group "
280
+ + group_names[g]
281
+ + "!\nSaving all output sheets to"
282
+ + ".xlsx!\nRe-run first level & only save .csvs if "
283
+ + "you want .csv files of group results!"
284
+ )
285
+ for folder in all_results_folders:
286
+ # create save_to_xls here, there are two cases we have to deal with:
287
+ # case 1: we found a csv file
288
+ if os.path.exists(
289
+ os.path.join(
290
+ group_dir,
291
+ folder,
292
+ folder + " - " + ORIG_SHEET_NAME + ".csv",
293
+ )
294
+ ):
295
+ valid_results_folders.append(folder)
296
+ if save_to_xls[g] is None:
297
+ save_to_xls[g] = False
298
+ if save_to_xls[g] is True:
299
+ print(sheet_type_mismatch_message)
300
+ write_issues_to_textfile(sheet_type_mismatch_message, folderinfo)
301
+ # case 2: we found a xlsx file
302
+ elif os.path.exists(
303
+ os.path.join(
304
+ group_dir,
305
+ folder,
306
+ folder + " - " + ORIG_SHEET_NAME + ".xlsx",
307
+ )
308
+ ):
309
+ valid_results_folders.append(folder)
310
+ if save_to_xls[g] is None:
311
+ save_to_xls[g] = True
312
+ if save_to_xls[g] is False:
313
+ save_to_xls[g] = True
314
+ print(sheet_type_mismatch_message)
315
+ write_issues_to_textfile(sheet_type_mismatch_message, folderinfo)
316
+ # test that at least 1 folder has valid results for all groups
317
+ if not valid_results_folders:
318
+ no_valid_results_error = (
319
+ "\n*********\n! ERROR !\n*********\n"
320
+ + "No valid results folder found for "
321
+ + group_names[g]
322
+ + "\nFix & re-run!"
323
+ )
324
+ print(no_valid_results_error)
325
+ write_issues_to_textfile(no_valid_results_error, folderinfo)
326
+ return save_to_xls
@@ -58,6 +58,7 @@ def group(folderinfo, cfg):
58
58
  # ................................ preparation ...................................
59
59
  # => either creates and sanity-checks folderinfo & cfg or loads it from a previous
60
60
  # run's config.json file (if load_dir)
61
+ # => there is an IMPORTANT NOTE about this in some_prep!
61
62
  folderinfo, cfg = some_prep(folderinfo, cfg)
62
63
 
63
64
  # .............................. print start ....................................
@@ -68,8 +69,8 @@ def group(folderinfo, cfg):
68
69
 
69
70
  # approach a - import & transform (i.e. no previous results to load from)
70
71
  if not folderinfo["load_dir"]:
71
- # in dlc/sleap, dfs is x-standardised automatically if 1st-level standardised x
72
- # => As a result all average & std dfs are x-standardised as well
72
+ # "dfs" are x-/Y-standardised automatically if 1st-level standardised x/Y
73
+ # => if this is the case, it translates to all average & std dfs as well
73
74
  dfs, raw_dfs, cfg = import_data(folderinfo, cfg)
74
75
  avg_dfs, std_dfs = avg_and_std(dfs, folderinfo, cfg)
75
76
  g_avg_dfs, g_std_dfs = grand_avg_and_std(avg_dfs, folderinfo, cfg)