Skip to content

Commit efbca98

Browse files
Add thread lock, filter_params support, and centralized phase computation utilities to BilateralSymmetryAnalyzer
1 parent 6f501e3 commit efbca98

1 file changed

Lines changed: 40 additions & 23 deletions

File tree

pyeyesweb/analysis_primitives/bilateral_symmetry.py

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@
1818

1919
import numpy as np
2020
import warnings
21+
import threading
2122
from collections import deque
2223
from sklearn.cross_decomposition import CCA
2324

24-
from pyeyesweb.utils.signal_processing import compute_hilbert_phases
25-
from pyeyesweb.utils.math_utils import compute_phase_locking_value
25+
from pyeyesweb.utils.signal_processing import compute_hilbert_phases, bandpass_filter, validate_filter_params
26+
from pyeyesweb.utils.math_utils import compute_phase_locking_value, center_signals
27+
from pyeyesweb.utils.validators import validate_integer, validate_filter_params_tuple
2628

2729

2830
class BilateralSymmetryAnalyzer:
@@ -35,29 +37,40 @@ class BilateralSymmetryAnalyzer:
3537
Read more in the [User Guide](/PyEyesWeb/user_guide/theoretical_framework/analysis_primitives/bilateral_symmetry/).
3638
"""
3739

38-
def __init__(self, window_size=100, joint_pairs=None):
40+
def __init__(self, window_size=100, joint_pairs=None, filter_params=None):
3941
"""
4042
Initialize bilateral symmetry analyzer.
41-
43+
4244
Args:
4345
window_size: Number of frames for sliding window analysis
4446
joint_pairs: List of tuples defining bilateral joint pairs
47+
filter_params: Optional tuple of (lowcut_hz, highcut_hz, sampling_rate_hz)
48+
for band-pass filtering. If None, no filtering is applied.
4549
"""
46-
self.window_size = window_size
47-
self.history = deque(maxlen=window_size)
50+
self.window_size = validate_integer(window_size, 'window_size', min_val=1, max_val=10000)
51+
self.history = deque(maxlen=self.window_size)
52+
self._history_lock = threading.Lock()
4853

4954
# Default joint pairs for standard MoCap setup
5055
if joint_pairs is None:
5156
self.joint_pairs = [
5257
(4, 5), # left_shoulder, right_shoulder
53-
(6, 7), # left_elbow, right_elbow
58+
(6, 7), # left_elbow, right_elbow
5459
(8, 9), # left_wrist, right_wrist
5560
(10, 11), # left_hip, right_hip
5661
(12, 13), # left_knee, right_knee
5762
(14, 15), # left_ankle, right_ankle
5863
]
5964
else:
6065
self.joint_pairs = joint_pairs
66+
67+
# Validate and store filter_params
68+
if filter_params is not None:
69+
filter_params = validate_filter_params_tuple(filter_params)
70+
lowcut, highcut, fs = validate_filter_params(*filter_params)
71+
self.filter_params = (lowcut, highcut, fs)
72+
else:
73+
self.filter_params = None
6174

6275
def _compute_bilateral_symmetry_index(self, left_data, right_data):
6376
"""
@@ -95,24 +108,24 @@ def _compute_bilateral_symmetry_index(self, left_data, right_data):
95108
def _compute_phase_synchronization(self, left_signal, right_signal):
96109
"""
97110
Compute phase synchronization using Hilbert Transform.
98-
111+
99112
Based on general biomechanics research for bilateral coordination.
100-
113+
Reuses centralized utilities from Synchronization class pattern.
114+
101115
Args:
102116
left_signal: 1D array of left limb signal (e.g., vertical displacement)
103117
right_signal: 1D array of right limb signal
104-
118+
105119
Returns:
106120
float: Phase locking value (0-1, where 1 is perfect synchronization)
107121
"""
108122
if len(left_signal) < 10: # Need minimum samples for Hilbert Transform
109123
return np.nan
110-
111-
try:
112-
left_centered = left_signal - np.mean(left_signal)
113-
right_centered = right_signal - np.mean(right_signal)
114-
sig = np.column_stack([left_centered, right_centered])
115124

125+
try:
126+
# Stack signals for processing
127+
sig = np.column_stack([left_signal, right_signal])
128+
sig = center_signals(sig)
116129
phase1, phase2 = compute_hilbert_phases(sig)
117130
plv = compute_phase_locking_value(phase1, phase2)
118131

@@ -165,25 +178,29 @@ def _compute_cca_correlation(self, left_data, right_data):
165178
def analyze_frame(self, mocap_frame):
166179
"""
167180
Analyze single frame of MoCap data for bilateral symmetry.
168-
181+
169182
Args:
170183
mocap_frame: (n_joints, 3) array of joint positions for one frame
171-
184+
172185
Returns:
173186
dict: Symmetry metrics for current frame
174187
"""
175-
self.history.append(mocap_frame)
176-
177-
if len(self.history) < 10: # Need minimum history for analysis
188+
# Thread-safe history append (reusing pattern from Synchronization)
189+
with self._history_lock:
190+
self.history.append(mocap_frame)
191+
history_length = len(self.history)
192+
193+
if history_length < 10: # Need minimum history for analysis
178194
return {
179195
'overall_symmetry': 0.0,
180196
'phase_sync': 0.0,
181197
'cca_correlation': 0.0,
182198
'joint_symmetries': {}
183199
}
184-
185-
# Convert history to array for analysis
186-
history_array = np.array(list(self.history)) # (n_frames, n_joints, 3)
200+
201+
# Thread-safe history conversion to array for analysis
202+
with self._history_lock:
203+
history_array = np.array(list(self.history)) # (n_frames, n_joints, 3)
187204

188205
joint_symmetries = {}
189206
symmetry_scores = []

0 commit comments

Comments
 (0)