biomechzoo 0.1.1__py3-none-any.whl

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.

Potentially problematic release.


This version of biomechzoo might be problematic. Click here for more details.

Files changed (44) hide show
  1. biomechzoo/__init__.py +5 -0
  2. biomechzoo/__main__.py +6 -0
  3. biomechzoo/biomech_ops/continuous_relative_phase_data.py +31 -0
  4. biomechzoo/biomech_ops/continuous_relative_phase_line.py +36 -0
  5. biomechzoo/biomech_ops/filter_data.py +56 -0
  6. biomechzoo/biomech_ops/filter_line.py +88 -0
  7. biomechzoo/biomech_ops/normalize_data.py +35 -0
  8. biomechzoo/biomech_ops/normalize_line.py +27 -0
  9. biomechzoo/biomech_ops/phase_angle_data.py +39 -0
  10. biomechzoo/biomech_ops/phase_angle_line.py +48 -0
  11. biomechzoo/biomechzoo.py +349 -0
  12. biomechzoo/conversion/__init__.py +0 -0
  13. biomechzoo/conversion/c3d2zoo_data.py +65 -0
  14. biomechzoo/conversion/csv2zoo_data.py +78 -0
  15. biomechzoo/conversion/mvnx2zoo_data.py +71 -0
  16. biomechzoo/conversion/opencap2zoo_data.py +23 -0
  17. biomechzoo/mvn/load_mvnx.py +514 -0
  18. biomechzoo/mvn/main_mvnx.py +75 -0
  19. biomechzoo/mvn/mvn.py +232 -0
  20. biomechzoo/mvn/mvnx_file_accessor.py +463 -0
  21. biomechzoo/processing/add_channel_data.py +71 -0
  22. biomechzoo/processing/addchannel_data.py +71 -0
  23. biomechzoo/processing/addevent_data.py +46 -0
  24. biomechzoo/processing/explodechannel_data.py +46 -0
  25. biomechzoo/processing/partition_data.py +51 -0
  26. biomechzoo/processing/removechannel_data.py +36 -0
  27. biomechzoo/processing/renamechannel_data.py +79 -0
  28. biomechzoo/processing/renameevent_data.py +68 -0
  29. biomechzoo/processing/split_trial_by_gait_cycle.py +52 -0
  30. biomechzoo/utils/batchdisp.py +21 -0
  31. biomechzoo/utils/compute_sampling_rate_from_time.py +25 -0
  32. biomechzoo/utils/engine.py +68 -0
  33. biomechzoo/utils/findfield.py +11 -0
  34. biomechzoo/utils/get_split_events.py +33 -0
  35. biomechzoo/utils/split_trial.py +23 -0
  36. biomechzoo/utils/zload.py +46 -0
  37. biomechzoo/utils/zplot.py +61 -0
  38. biomechzoo/utils/zsave.py +50 -0
  39. biomechzoo-0.1.1.dist-info/METADATA +48 -0
  40. biomechzoo-0.1.1.dist-info/RECORD +44 -0
  41. biomechzoo-0.1.1.dist-info/WHEEL +5 -0
  42. biomechzoo-0.1.1.dist-info/entry_points.txt +2 -0
  43. biomechzoo-0.1.1.dist-info/licenses/LICENSE +21 -0
  44. biomechzoo-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+
3
+
4
+ def add_channel_data(data, ch_new_name, ch_new_data, section='Video'):
5
+ """
6
+ Add a new channel to zoo data.
7
+
8
+ Parameters
9
+ ----------
10
+ data : dict
11
+ Zoo file data.
12
+ ch_new_name : str
13
+ Name of the new channel.
14
+ ch_new_data : array-like
15
+ New data to be added to the channel (should be n x 1 or n x 3).
16
+ section : str
17
+ Section of zoo data ('Video' or 'Analog').
18
+
19
+ Returns
20
+ -------
21
+ dict
22
+ Updated zoo data with new channel added.
23
+
24
+ Notes
25
+ -----
26
+ - If the channel already exists, it will be overwritten.
27
+ - Adds channel name to the list in data['zoosystem'][section]['Channels'].
28
+ """
29
+
30
+ # Warn if overwriting
31
+ if ch_new_name in data:
32
+ print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
33
+
34
+ # Assign channel data
35
+ data[ch_new_name] = {
36
+ 'line': ch_new_data,
37
+ 'event': {}
38
+ }
39
+
40
+ # Update channel list
41
+ ch_list = data['zoosystem'][section].get('Channels', [])
42
+
43
+ # If the channel list is a NumPy array, convert it to a list
44
+ if isinstance(ch_list, np.ndarray):
45
+ ch_list = ch_list.tolist()
46
+
47
+ # Ensure it's a flat list of strings
48
+ if isinstance(ch_list, list) and ch_new_name not in ch_list:
49
+ ch_list.append(ch_new_name)
50
+ data['zoosystem'][section]['Channels'] = ch_list
51
+
52
+ return data
53
+
54
+
55
+ if __name__ == '__main__':
56
+ # -------TESTING--------
57
+ import os
58
+ from src.biomechzoo.utils.zload import zload
59
+ from src.biomechzoo.utils.zplot import zplot
60
+ # get path to sample zoo file
61
+ current_dir = os.path.dirname(os.path.abspath(__file__))
62
+ project_root = os.path.dirname(current_dir)
63
+ fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
64
+
65
+ # load zoo file
66
+ data = zload(fl)
67
+ data = data['data']
68
+ r = data['RKneeAngles']['line']*3
69
+ data= add_channel_data(data, ch_new_name='blah', ch_new_data=r)
70
+ zplot(data, 'blah')
71
+
@@ -0,0 +1,71 @@
1
+ import numpy as np
2
+
3
+
4
+ def addchannel_data(data, ch_new_name, ch_new_data, section='Video'):
5
+ """
6
+ Add a new channel to zoo data.
7
+
8
+ Parameters
9
+ ----------
10
+ data : dict
11
+ Zoo file data.
12
+ ch_new_name : str
13
+ Name of the new channel.
14
+ ch_new_data : array-like
15
+ New data to be added to the channel (should be n x 1 or n x 3).
16
+ section : str
17
+ Section of zoo data ('Video' or 'Analog').
18
+
19
+ Returns
20
+ -------
21
+ dict
22
+ Updated zoo data with new channel added.
23
+
24
+ Notes
25
+ -----
26
+ - If the channel already exists, it will be overwritten.
27
+ - Adds channel name to the list in data['zoosystem'][section]['Channels'].
28
+ """
29
+
30
+ # Warn if overwriting
31
+ if ch_new_name in data:
32
+ print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
33
+
34
+ # Assign channel data
35
+ data[ch_new_name] = {
36
+ 'line': ch_new_data,
37
+ 'event': {}
38
+ }
39
+
40
+ # Update channel list
41
+ ch_list = data['zoosystem'][section].get('Channels', [])
42
+
43
+ # If the channel list is a NumPy array, convert it to a list
44
+ if isinstance(ch_list, np.ndarray):
45
+ ch_list = ch_list.tolist()
46
+
47
+ # Ensure it's a flat list of strings
48
+ if isinstance(ch_list, list) and ch_new_name not in ch_list:
49
+ ch_list.append(ch_new_name)
50
+ data['zoosystem'][section]['Channels'] = ch_list
51
+
52
+ return data
53
+
54
+
55
+ if __name__ == '__main__':
56
+ # -------TESTING--------
57
+ import os
58
+ from src.biomechzoo.utils.zload import zload
59
+ from src.biomechzoo.utils.zplot import zplot
60
+ # get path to sample zoo file
61
+ current_dir = os.path.dirname(os.path.abspath(__file__))
62
+ project_root = os.path.dirname(current_dir)
63
+ fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
64
+
65
+ # load zoo file
66
+ data = zload(fl)
67
+ data = data['data']
68
+ r = data['RKneeAngles']['line']*3
69
+ data= addchannel_data(data, ch_new_name='blah', ch_new_data=r)
70
+ zplot(data, 'blah')
71
+
@@ -0,0 +1,46 @@
1
+ import numpy as np
2
+
3
+
4
+ def addevent_data(data, ch, ename, etype):
5
+ if isinstance(ch, str):
6
+ ch = [ch]
7
+
8
+ if len(ch) == 1 and ch[0].lower() == 'all':
9
+ ch = [key for key in data if key != 'zoosystem']
10
+
11
+ for channel in ch:
12
+ if ename == '':
13
+ data[channel]['event'] = {}
14
+ continue
15
+
16
+ if channel not in data:
17
+ print(f'Channel {channel} does not exist')
18
+ continue
19
+
20
+ yd = data[channel]['line'] # 1D array
21
+ etype = etype.lower()
22
+ if etype == 'absmax':
23
+ exd = int(np.argmax(np.abs(yd)))
24
+ eyd = float(yd[exd])
25
+ elif etype == 'first':
26
+ exd = 0
27
+ eyd = float(yd[exd])
28
+ elif etype == 'last':
29
+ exd = len(yd) - 1
30
+ eyd = float(yd[exd])
31
+ elif etype == 'max':
32
+ exd = int(np.argmax(yd))
33
+ eyd = float(yd[exd])
34
+ elif etype == 'min':
35
+ exd = int(np.argmin(yd))
36
+ eyd = float(yd[exd])
37
+ elif etype == 'rom':
38
+ eyd = float(np.max(yd) - np.min(yd))
39
+ exd = 0 # dummy index (like MATLAB version)
40
+ else:
41
+ raise ValueError(f'Unknown event type: {etype}')
42
+
43
+ # Add event to the channel's event dict
44
+ data[channel]['event'][ename] = [exd, eyd, 0]
45
+
46
+ return data
@@ -0,0 +1,46 @@
1
+ def explodechannel_data(data, channels=None):
2
+ """ Explodes 3D channels (n x 3 arrays) into separate X, Y, Z channels.
3
+
4
+ Arguments:
5
+ data (dict): Zoo data loaded from a file
6
+ channels (list of str or None): Channels to explode.
7
+ If None, explode all channels with 'line' shaped (n x 3).
8
+
9
+ Returns:
10
+ data_new (dict): Modified zoo dictionary with exploded channels.
11
+ """
12
+
13
+ data_new = data.copy()
14
+
15
+ # Find default channels if none provided
16
+ if channels is None:
17
+ channels = []
18
+ for ch in data_new:
19
+ if ch == 'zoosystem':
20
+ continue
21
+ ch_data = data_new[ch]['line']
22
+ if ch_data.ndim == 2 and ch_data.shape[1] == 3:
23
+ channels.append(ch)
24
+
25
+ # Explode each channel
26
+ for ch in channels:
27
+ if ch not in data_new:
28
+ print('Warning: channel {} not found, skipping.'.format(ch))
29
+ continue
30
+
31
+ ch_data = data_new[ch]['line']
32
+ if ch_data.ndim != 2 or ch_data.shape[1] != 3:
33
+ print(f"Warning: channel '{ch}' 'line' is not n x 3 shape, skipping.")
34
+ continue
35
+
36
+ x, y, z = ch_data[:, 0], ch_data[:, 1], ch_data[:, 2]
37
+ for axis, line in zip(['_x', '_y', '_z'], [x, y, z]):
38
+ key = ch + axis
39
+ data_new[key] = {
40
+ 'line': line,
41
+ 'event': data[ch]['event']}
42
+
43
+ # Remove original channel
44
+ del data_new[ch]
45
+
46
+ return data_new
@@ -0,0 +1,51 @@
1
+ from biomechzoo.utils.findfield import findfield
2
+ import warnings
3
+
4
+
5
+ def partition_data(data, evt_start, evt_end):
6
+ """ partition data for all channels between events evt_start and evt_end"""
7
+
8
+ # extract event values
9
+ e1, _ = findfield(data,evt_start)
10
+ e2, _ = findfield(data,evt_end)
11
+
12
+ data_new = data.copy()
13
+ for ch_name, ch_data in data_new.items():
14
+ if ch_name != 'zoosystem':
15
+ try:
16
+ data_new[ch_name]['line'] = ch_data[e1[0]:e2[0], :]
17
+ except (IndexError, ValueError) as e:
18
+ # IndexError: if e1[0]:e2[0] goes beyond the available indices
19
+ # ValueError: less likely, but may arise with shape mismatches
20
+ warnings.warn(f"Skipping {ch_name} due to error: {e}")
21
+
22
+ # partition events
23
+ events = ch_data['event']
24
+ for event_name, value in events.items():
25
+ original_frame = value[0]
26
+ if original_frame == 1:
27
+ continue # leave index 1 as is (per MATLAB version)
28
+ elif original_frame == 999:
29
+ continue # do not change outlier markers
30
+ else:
31
+ new_frame = original_frame - e1[0] + 1
32
+ data_new[ch_name]['event'][event_name][0] = new_frame
33
+
34
+ return data_new
35
+
36
+
37
+ def _partition_line(arr, evt_start, evt_end):
38
+ arr_new = arr[evt_start:evt_end, :]
39
+ return arr_new
40
+
41
+
42
+ def _partition_event(event_dict, evt_start, evt_end, arr_len):
43
+ raise NotImplementedError
44
+ # event_dict_new = {}
45
+ # for event, event_val in event_dict:
46
+ # event_val_new =
47
+ # event_dict_new[event] =
48
+ #
49
+ #
50
+ #
51
+ # return event_dict_new
@@ -0,0 +1,36 @@
1
+ def removechannel_data(data, channels, mode='remove'):
2
+ """
3
+ File-level processing: Remove or keep specified channels in a single zoo dictionary.
4
+
5
+ Parameters:
6
+ - data (dict): Zoo data loaded from a file
7
+ - channels (list of str): List of channels to remove or keep
8
+ - mode (str): 'remove' or 'keep'
9
+
10
+ Returns:
11
+ - dict: Modified zoo dictionary with updated channels
12
+ """
13
+ if mode not in ['remove', 'keep']:
14
+ raise ValueError("mode must be 'remove' or 'keep'.")
15
+
16
+ zoosystem = data.get('zoosystem', {})
17
+ all_channels = [ch for ch in data if ch != 'zoosystem']
18
+
19
+ # Check for missing channels
20
+ missing = [ch for ch in channels if ch not in all_channels]
21
+ if missing:
22
+ print('Warning: the following channels were not found {}'.format(missing))
23
+
24
+ if mode == 'remove':
25
+ keep_channels = [ch for ch in all_channels if ch not in channels]
26
+ elif mode == 'keep':
27
+ keep_channels = [ch for ch in all_channels if ch in channels]
28
+ else:
29
+ raise ValueError("Mode must be 'remove' or 'keep'.")
30
+
31
+ # Build new zoo dictionary
32
+ data_new = {'zoosystem': zoosystem}
33
+ for ch in keep_channels:
34
+ data_new[ch] = data[ch]
35
+
36
+ return data_new
@@ -0,0 +1,79 @@
1
+ import numpy as np
2
+
3
+
4
+ def renamechannel_data(data, ch_old_names, ch_new_names, section='Video'):
5
+ """
6
+ Rename channels in a zoo data.
7
+
8
+ Parameters
9
+ ----------
10
+ data : dict
11
+ Zoo file data.
12
+ ch_old_names : str or list
13
+ Name of the old channels.
14
+ ch_new_names : str or list
15
+ Name of the new channels.
16
+
17
+ section : str
18
+ Section of zoo data ('Video' or 'Analog').
19
+
20
+ Returns
21
+ -------
22
+ dict
23
+ Updated zoo data with new channel added.
24
+
25
+ Notes
26
+ -----
27
+ - If the channel already exists, it will be overwritten.
28
+ - Adds channel name to the list in data['zoosystem'][section]['Channels'].
29
+ """
30
+
31
+ for i, ch_old_name in enumerate(ch_old_names):
32
+ ch_new_name = ch_new_names[i]
33
+ # Warn if overwriting
34
+ if ch_new_name in data:
35
+ print('Warning: channel {} already exists, overwriting...'.format(ch_new_name))
36
+
37
+ # Assign new channel
38
+ data[ch_new_name] = {
39
+ 'line': data[ch_old_name]['line'],
40
+ 'event': data[ch_old_name]['event']
41
+ }
42
+
43
+ # remove old channel
44
+ data.pop(ch_old_name)
45
+
46
+ # Update channel list
47
+ ch_list = data['zoosystem'][section].get('Channels', [])
48
+
49
+ # If the channel list is a NumPy array, convert it to a list
50
+ if isinstance(ch_list, np.ndarray):
51
+ ch_list = ch_list.tolist()
52
+
53
+ # Ensure it's a flat list of strings
54
+ if isinstance(ch_list, list) and ch_new_name not in ch_list:
55
+ ch_list.append(ch_new_name)
56
+ data['zoosystem'][section]['Channels'] = ch_list
57
+
58
+ return data
59
+
60
+
61
+ if __name__ == '__main__':
62
+ # -------TESTING--------
63
+ import os
64
+ from src.biomechzoo.utils.zload import zload
65
+ current_dir = os.path.dirname(os.path.abspath(__file__))
66
+ project_root = os.path.dirname(current_dir)
67
+ fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
68
+
69
+ # load zoo file
70
+ data = zload(fl)
71
+ ch_old_name = 'RKneeAngles'
72
+ ch_new_name = 'RightKneeAngles'
73
+ data = renamechannel_data(data, ch_old_name=ch_old_name, ch_new_name=ch_new_name)
74
+
75
+ if ch_old_name not in data:
76
+ print('RkneeAngles removed')
77
+ if ch_new_name in data:
78
+ print('RightKneeAngles added')
79
+
@@ -0,0 +1,68 @@
1
+ def renameevent_data(data, evt, nevt):
2
+ """
3
+ Rename events in the Zoo data structure.
4
+
5
+ Parameters
6
+ ----------
7
+ data : dict
8
+ The Zoo-formatted dictionary.
9
+ evt : str or list of str
10
+ Names of existing events to rename.
11
+ nevt : str or list of str
12
+ Names of new events to apply.
13
+
14
+ Returns
15
+ -------
16
+ data : dict
17
+ Updated Zoo data with renamed events.
18
+ """
19
+ # Convert to list if passed as single string
20
+ if isinstance(evt, str):
21
+ evt = [evt]
22
+ if isinstance(nevt, str):
23
+ nevt = [nevt]
24
+
25
+ if len(evt) != len(nevt):
26
+ raise ValueError("`evt` and `nevt` must have the same length.")
27
+
28
+ # Get all data channels except 'zoosystem'
29
+ channels = [ch for ch in data if ch != 'zoosystem']
30
+ for old_name, new_name in zip(evt, nevt):
31
+ eventsRenamed = False
32
+ for ch in channels:
33
+ events = data[ch].get('event', {})
34
+ if old_name in events:
35
+ print('Renaming event {} in channel {} to {}'.format(old_name, ch, new_name))
36
+ data[ch]['event'][new_name] = events[old_name]
37
+ del data[ch]['event'][old_name]
38
+ eventsRenamed = True
39
+
40
+ if not eventsRenamed:
41
+ print('no event {} found in any channel'.format(old_name))
42
+
43
+ return data
44
+
45
+
46
+ if __name__ == '__main__':
47
+ # -------TESTING--------
48
+ import os
49
+ from src.biomechzoo.utils.zload import zload
50
+ # get path to sample zoo file
51
+ current_dir = os.path.dirname(os.path.abspath(__file__))
52
+ project_root = os.path.dirname(current_dir)
53
+ fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
54
+
55
+ # load zoo file
56
+ data = zload(fl)
57
+ evt = ['Left_FootStrike1', 'Left_FootStrike2', 'NonExistingEvent']
58
+
59
+ # see existing keys
60
+ missing = [k for k in evt if k not in data['SACR']['event']]
61
+ print("Missing keys:", missing) # expected behavior is missing 'NonExistingEvent'
62
+
63
+ nevt = ['LFS_1', 'LFS2', 'NE_1']
64
+ data = renameevent_data(data, evt=evt, nevt=nevt)
65
+
66
+ # after applying renameevent_data
67
+ missing = [k for k in nevt if k not in data['SACR']['event']]
68
+ print("Missing keys:", missing) # expected behavior is missing 'NE_1'
@@ -0,0 +1,52 @@
1
+ import numpy as np
2
+ import os
3
+ import scipy.io as sio
4
+ from biomechzoo.utils.findfield import findfield
5
+
6
+
7
+ def split_trial_by_gait_cycle(fl, event_name):
8
+ """ splits lengthy trials containing n cycles into n trials based on side"""
9
+
10
+ # extract folder and file name
11
+ fld = os.path.dirname(fl)
12
+ fl_name = os.path.splitext(os.path.basename(fl))[0]
13
+
14
+ # load file
15
+ data = zload(fl)
16
+
17
+ # find all events, events should follow style name1, name2, etc..
18
+ split_events = []
19
+ i = 1
20
+ _, channel_name = findfield(data, event_name)
21
+ event_name_root = event_name[0:-1]
22
+ while True:
23
+ key = f"{event_name_root}{i}"
24
+ if key in data[channel_name]['event']:
25
+ split_events.append(data[channel_name]['event'][key])
26
+ i += 1
27
+ else:
28
+ break
29
+
30
+ n_segments = len(split_events) - 1
31
+ if n_segments < 1:
32
+ print("Not enough {} events to split.".format(event_name_root))
33
+ return
34
+
35
+ for i in range(n_segments):
36
+
37
+ start = split_events[i][0]
38
+ end = split_events[i + 1][0]
39
+ fl_new = os.path.join(fld, fl_name + '_' + str(i+1) + '.zoo')
40
+ data_new = _split_trial(fld, fl_new, start, end)
41
+
42
+ def _split_trial(fld, fl_new, start, end):
43
+ raise NotImplementedError
44
+
45
+ if __name__ == '__main__':
46
+ """ testing: load a single zoo file from the other subfolder in data"""
47
+ # -------TESTING--------
48
+ from src.biomechzoo.utils.zload import zload
49
+ current_dir = os.path.dirname(os.path.abspath(__file__))
50
+ project_root = os.path.dirname(current_dir)
51
+ fl = os.path.join(project_root, 'data', 'other', 'HC030A05.zoo')
52
+ split_trial_by_gait_cycle(fl, event_name='Right_FootStrike1')
@@ -0,0 +1,21 @@
1
+ def batchdisp(msg, level=1, verbose='none'):
2
+ """ utility to control verbosity level during batch processing"""
3
+ level = _normalize_verbose(level)
4
+ verbose = _normalize_verbose(verbose)
5
+ if verbose >= level:
6
+ print(msg)
7
+
8
+ def _normalize_verbose(verbose):
9
+ if isinstance(verbose, int):
10
+ if verbose not in (0, 1, 2):
11
+ raise ValueError("Integer verbose level must be 0 (none), 1 (minimal), or 2 (all)")
12
+ return verbose
13
+ elif isinstance(verbose, str):
14
+ verbose_map = {'none': 0, 'minimal': 1, 'all': 2}
15
+ if verbose.lower() not in verbose_map:
16
+ raise ValueError("String verbose level must be 'none', 'minimal', or 'all'")
17
+ return verbose_map[verbose.lower()]
18
+ else:
19
+ raise TypeError("Verbose must be an int (0–2) or str ('none', 'minimal', 'all')")
20
+
21
+
@@ -0,0 +1,25 @@
1
+ import numpy as np
2
+
3
+
4
+ def compute_sampling_rate_from_time(t, verbose=False):
5
+ """ computes sampling rate from time column
6
+ Arguments
7
+ t, 1D numpy array, recorded times
8
+
9
+ Returns
10
+ fsamp, int: sampling rate of capture
11
+ """
12
+ # Calculate differences between consecutive time points
13
+ dt = np.diff(t)
14
+
15
+ # Average time difference (seconds)
16
+ avg_dt = np.mean(dt)
17
+
18
+ # Sampling frequency (Hz)
19
+ # Sampling frequency (Hz)
20
+ fsamp = int(np.round(1 / avg_dt))
21
+
22
+ if verbose:
23
+ print('Inferred sampling rate: {} Hz'.format(fsamp))
24
+
25
+ return fsamp
@@ -0,0 +1,68 @@
1
+ import os
2
+
3
+
4
+ def engine(root_folder, extension='.zoo', subfolders=None, name_contains=None, verbose=False):
5
+ """
6
+ Recursively search for files with a given extension, optionally filtering by
7
+ specific subfolders and substrings in filenames.
8
+
9
+ This function walks through every directory and subdirectory starting at
10
+ 'root_folder'. If 'subfolders' is provided, it limits the search only to those
11
+ folders (or any of their subfolders) whose names appear in 'subfolders'. For
12
+ example, if subfolders=['Straight'], it will only consider files inside any
13
+ folder named 'Straight' at any depth within the root folder.
14
+
15
+ For each file found, it checks whether the file's extension matches the given
16
+ 'extension' (case-insensitive). If 'name_contains' is specified, it also
17
+ requires the filename to contain that substring (case-insensitive).
18
+
19
+ Arguments:
20
+ root_folder (str): The root directory path where the search begins.
21
+ extension (str): File extension to search for (e.g., '.zoo', '.c3d'). Default .zoo
22
+ subfolders (list of str, optional): List of folder names to restrict the search to.
23
+ Only files inside these folders (or their subfolders) are included.
24
+ If None, search all subfolders.
25
+ name_contains (str, optional): Substring that must be present in the filename
26
+ (case-insensitive). If None, no substring filtering is applied.
27
+ verbose (bool, optional): If true, displays additional information to user
28
+ Returns:
29
+ list of str: List of full file paths matching the criteria.
30
+ """
31
+ matched_files = []
32
+
33
+ subfolders_set = set(subfolders) if subfolders else None
34
+
35
+ for dirpath, _, filenames in os.walk(root_folder):
36
+ if subfolders_set is not None:
37
+ rel_path = os.path.relpath(dirpath, root_folder)
38
+ if rel_path == '.':
39
+ continue
40
+ # Split the relative path into all folder parts
41
+ parts = rel_path.split(os.sep)
42
+ # Check if any folder in the path matches one in subfolders_set
43
+ if not any(part in subfolders_set for part in parts):
44
+ continue
45
+
46
+ for file in filenames:
47
+ if not file.lower().endswith(extension.lower()):
48
+ continue
49
+ if name_contains and name_contains.lower() not in file.lower():
50
+ continue
51
+ matched_files.append(os.path.join(dirpath, file))
52
+
53
+ if verbose:
54
+ print("Found {} {} files in subfolders named {} with substring {}:".format(len(matched_files), extension, subfolders, name_contains))
55
+ for f in matched_files:
56
+ print(" - {}".format(f))
57
+
58
+ return matched_files
59
+
60
+
61
+ if __name__ == '__main__':
62
+ """ testing: use engine to search for files in any subfolder called 'Straight' for files with the substring 'HC03'
63
+ with extension .c3d in the sample study folder (data)"""
64
+ # -------TESTING--------
65
+ current_dir = os.path.dirname(os.path.abspath(__file__))
66
+ project_root = os.path.dirname(current_dir)
67
+ sample_dir = os.path.join(project_root, 'data', 'sample_study', 'raw c3d files')
68
+ c3d_files = engine(sample_dir, '.c3d', subfolders=['Straight'], name_contains='HC03', verbose=True)
@@ -0,0 +1,11 @@
1
+ def findfield(data, target_event):
2
+ """ searches in zoo data for the event value and channel name associated with target_event"""
3
+ for channel, content in data.items():
4
+ if channel == 'zoosystem':
5
+ continue
6
+ events = content.get('event', {})
7
+ if target_event in events:
8
+ return events[target_event], channel
9
+ return None, None
10
+
11
+
@@ -0,0 +1,33 @@
1
+ from biomechzoo.utils.findfield import findfield
2
+
3
+
4
+ def get_split_events(data, first_event_name):
5
+ """ splits lengthy trials containing n cycles into n trials based on side"""
6
+
7
+ # find all events, events should follow style name1, name2, etc..
8
+ split_events = []
9
+ _, channel_name = findfield(data, first_event_name)
10
+ if channel_name is None:
11
+ return None
12
+
13
+ event_name_root = first_event_name[0:-1]
14
+ first_event_number = int(first_event_name[-1])
15
+ i = 1
16
+ if first_event_number > 1:
17
+ i = first_event_number
18
+
19
+ while True:
20
+ key = f"{event_name_root}{i}"
21
+ if key in data[channel_name]['event']:
22
+ split_events.append(key)
23
+ i += 1
24
+ else:
25
+ break
26
+
27
+ n_segments = len(split_events) - 1
28
+ if n_segments < 1:
29
+ print("Not enough {} events to split.".format(event_name_root))
30
+ return
31
+
32
+ return split_events
33
+