1818
1919import numpy as np
2020import warnings
21+ import threading
2122from collections import deque
2223from 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
2830class 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