autogaita 1.1.1__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.
- {autogaita-1.1.1 → autogaita-1.3.0}/PKG-INFO +1 -1
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_2_sc_extraction.py +22 -10
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_3_analysis.py +28 -29
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_utils.py +79 -2
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_1_preparation.py +137 -85
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_main.py +3 -2
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/utils.py +68 -3
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_1_preparation.py +2 -35
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_2_sc_extraction.py +96 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_3_analysis.py +25 -31
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita.egg-info/PKG-INFO +1 -1
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita.egg-info/SOURCES.txt +1 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/setup.py +1 -1
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_common2D_unit_1_preparation.py +5 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_common2D_unit_2_sc_extraction.py +55 -3
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_common2D_unit_3_analysis.py +2 -31
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_group_unit.py +25 -7
- autogaita-1.3.0/tests/test_universal3D_unit_2_sc_extraction.py +108 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_universal3D_unit_3_analysis.py +24 -11
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_utils.py +97 -5
- {autogaita-1.1.1 → autogaita-1.3.0}/LICENSE +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/README.md +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/__main__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/dlc_multirun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/dlc_singlerun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_dlcrun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_dlcrun_forpaper.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/group_universal3Drun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/sleap_singlerun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/universal3D_multirun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/batchrun_scripts/universal3D_singlerun.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_1_preparation.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_4_plots.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/common2D/common2D_constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/dlc/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/dlc/dlc_main.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_2_data_processing.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_3_PCA.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_4_stats.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_5_plots.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/group/group_utils.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_advanced_config_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_columninfo_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_gui_constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_gui_utils.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_main_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/common2D_run_and_done_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/dlc_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/dlc_gui_config.json +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/first_level_gui_utils.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/gaita_widgets.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/group_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/group_gui_config.json +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/gui_constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/gui_utils.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/main_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/sleap_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/sleap_gui_config.json +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/universal3D_gui.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/gui/universal3D_gui_config.json +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/icon.icns +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/icon.ico +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/logo.png +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/resources/pic_to_demo_for_repo.png +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/sleap/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/sleap/sleap_main.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_4_plots.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_constants.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_datafile_preparation.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_main.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita/universal3D/universal3D_utils.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita.egg-info/dependency_links.txt +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita.egg-info/requires.txt +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/autogaita.egg-info/top_level.txt +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/setup.cfg +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/__init__.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_dlc_approval.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_dlc_unit_1_preparation.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_group_approval.py +0 -0
- {autogaita-1.1.1 → autogaita-1.3.0}/tests/test_universal3D_approval.py +0 -0
- {autogaita-1.1.1 → 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.
|
|
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: #
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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,
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
#
|
|
97
|
-
# =>
|
|
98
|
-
#
|
|
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):
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
161
|
+
that they match between groups. Also some tests for users' PCA config!
|
|
124
162
|
"""
|
|
125
163
|
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
#
|
|
72
|
-
# =>
|
|
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)
|