Skip to content

Commit 37ef48d

Browse files
new self test for REF position
1 parent 408672e commit 37ef48d

1 file changed

Lines changed: 50 additions & 33 deletions

File tree

core/pioreactor/actions/self_test.py

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@
4545
from pioreactor.utils import SummableDict
4646
from pioreactor.utils.math_helpers import correlation
4747
from pioreactor.utils.math_helpers import mean
48-
from pioreactor.utils.math_helpers import trimmed_mean
4948
from pioreactor.utils.math_helpers import variance
5049
from pioreactor.whoami import get_unit_name
5150
from pioreactor.whoami import is_testing_env
@@ -87,8 +86,9 @@ def test_pioreactor_HAT_present(managed_state, logger: CustomLogger, unit: str,
8786

8887

8988
def 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

157174
def test_all_positive_correlations_between_pds_and_leds(

0 commit comments

Comments
 (0)