4545from pioreactor .utils import SummableDict
4646from pioreactor .utils .math_helpers import correlation
4747from pioreactor .utils .math_helpers import mean
48- from pioreactor .utils .math_helpers import trimmed_mean
4948from pioreactor .utils .math_helpers import variance
5049from pioreactor .whoami import get_unit_name
5150from pioreactor .whoami import is_testing_env
@@ -87,8 +86,9 @@ def test_pioreactor_HAT_present(managed_state, logger: CustomLogger, unit: str,
8786
8887
8988def test_REF_is_in_correct_position (managed_state , logger : CustomLogger , unit : str , experiment : str ) -> None :
90- # this _also_ uses stirring to increase the variance in the non-REF.
91- # The idea is to trigger stirring on and off and the REF should not see a change in signal / variance, but the other PD should.
89+ # Toggle stirring between SLEEPING and READY. The REF channel should have a much smaller
90+ # ON/OFF effect than signal channels if it is physically in the REF position.
91+ from statistics import median
9292
9393 assert is_HAT_present (), "Hat is not detected."
9494
@@ -100,7 +100,8 @@ def test_REF_is_in_correct_position(managed_state, logger: CustomLogger, unit: s
100100
101101 test_channels = {str (channel ): "90" for channel , angle in pd_channels .items ()}
102102
103- signals : dict [PdChannel , list [float ]] = {cast (PdChannel , channel ): [] for channel in test_channels }
103+ all_channels = [cast (PdChannel , channel ) for channel in test_channels ]
104+ delta_per_channel : dict [PdChannel , list [float ]] = {channel : [] for channel in all_channels }
104105
105106 with stirring .start_stirring (
106107 target_rpm = 1250 , unit = unit , experiment = experiment , enable_dodging_od = False
@@ -116,42 +117,58 @@ def test_REF_is_in_correct_position(managed_state, logger: CustomLogger, unit: s
116117 st .block_until_rpm_is_close_to_target (abs_tolerance = 150 , timeout = 10 )
117118
118119 warmup_samples = 5
119- total_samples = 50
120+ n_cycles = 8
121+ settle_samples = 1
122+ window_samples = 2
120123
121124 # Warm-up: discard a few initial readings to allow signals to stabilize.
122125 for _ in range (warmup_samples ):
123- try :
124- next (od_stream )
125- except StopIteration :
126- break
127-
128- for i , reading in enumerate (od_stream , start = 1 ):
129- for channel in signals :
130- signals [channel ].append (reading .ods [channel ].od )
131-
132- if i % 5 == 0 and i % 2 == 0 :
133- st .set_state (JobState .READY )
134- elif i % 5 == 0 :
135- st .set_state (JobState .SLEEPING )
136-
137- if i == total_samples :
138- break
139-
140- logger .debug (f"{ signals = } " )
141-
142- norm_variance_per_channel = {
143- channel : variance (readings ) / trimmed_mean (readings ) ** 2 for channel , readings in signals .items ()
126+ next (od_stream )
127+
128+ def collect_phase_medians (target_state : JobState ) -> dict [PdChannel , float ]:
129+ st .set_state (target_state )
130+ per_channel_window : dict [PdChannel , list [float ]] = {channel : [] for channel in all_channels }
131+
132+ for sample_i in range (settle_samples + window_samples ):
133+ reading = next (od_stream )
134+ if sample_i < settle_samples :
135+ continue
136+ for channel in all_channels :
137+ per_channel_window [channel ].append (reading .ods [channel ].od )
138+
139+ return {channel : float (median (values )) for channel , values in per_channel_window .items ()}
140+
141+ for _ in range (n_cycles ):
142+ off_medians = collect_phase_medians (JobState .SLEEPING )
143+ on_medians = collect_phase_medians (JobState .READY )
144+ for channel in all_channels :
145+ delta_per_channel [channel ].append (on_medians [channel ] - off_medians [channel ])
146+
147+ effect_per_channel = {
148+ channel : float (median ([abs (delta ) for delta in deltas ]))
149+ for channel , deltas in delta_per_channel .items ()
144150 }
151+ ref_effect = effect_per_channel [reference_channel ]
152+ signal_effects = {channel : effect_per_channel [channel ] for channel in signal_channels }
153+ highest_signal_channel = max (signal_effects , key = signal_effects .__getitem__ )
154+ highest_signal_effect = signal_effects [highest_signal_channel ]
155+
156+ logger .debug (f"{ delta_per_channel = } " )
157+ logger .debug (f"{ effect_per_channel = } " )
145158
146- THRESHOLD = 1.0
147- ref_variance = norm_variance_per_channel [reference_channel ]
148- signal_variances = {channel : norm_variance_per_channel [channel ] for channel in signal_channels }
149- lowest_signal_channel = min (signal_variances , key = signal_variances .__getitem__ )
150- lowest_signal_variance = signal_variances [lowest_signal_channel ]
159+ MIN_SIGNAL_EFFECT = 0.002
160+ MAX_REF_EFFECT = 0.010
161+ MAX_REF_TO_SIGNAL_RATIO = 0.5
151162
152163 assert (
153- THRESHOLD * ref_variance < lowest_signal_variance
154- ), f"REF measured higher variance than SIGNAL. { reference_channel = } , { norm_variance_per_channel = } "
164+ highest_signal_effect >= MIN_SIGNAL_EFFECT
165+ ), f"Stirring perturbation too small to evaluate REF placement. { effect_per_channel = } "
166+ assert (
167+ ref_effect <= MAX_REF_EFFECT
168+ ), f"REF is too disturbed by stirring. { reference_channel = } , { effect_per_channel = } "
169+ assert (
170+ ref_effect <= MAX_REF_TO_SIGNAL_RATIO * highest_signal_effect
171+ ), f"REF disturbance is too similar to SIGNAL disturbance. { reference_channel = } , { highest_signal_channel = } , { effect_per_channel = } "
155172
156173
157174def test_all_positive_correlations_between_pds_and_leds (
0 commit comments