Skip to content

Commit 715bffc

Browse files
Merge branch 'more-flows-to-huey' into develop
* more-flows-to-huey: more align on how we present errors ui details making stirring calibration like the others making stirring calibration like the others move flows to huey tasks instead of shelling out to python adding tests timeout and mypy timeout and mypy fix for stirring? increase timeouts fix for stirring wip on new cal arch wip on new cal arch wip on new cal arch wip on new cal arch wip on new cal arch wip on new cal arch
2 parents eaeda84 + bb28299 commit 715bffc

36 files changed

+3460
-1151
lines changed

core/pioreactor/actions/self_test.py

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -370,26 +370,6 @@ def test_detect_heating_pcb(managed_state, logger: CustomLogger, unit: str, expe
370370
assert is_heating_pcb_present(), "Heater PCB is not connected, or i2c is not working."
371371

372372

373-
def test_run_stirring_calibration(managed_state, logger: CustomLogger, unit: str, experiment: str) -> None:
374-
from pioreactor.calibrations.protocols.stirring_dc_based import run_stirring_calibration
375-
376-
cal = run_stirring_calibration()
377-
cal.save_to_disk_for_device("stirring")
378-
cal.set_as_active_calibration_for_device("stirring")
379-
return
380-
381-
382-
def test_create_od_calibrations_using_optical_reference_standard(
383-
managed_state, logger: CustomLogger, unit: str, experiment: str
384-
) -> None:
385-
from pioreactor.calibrations.protocols.od_reference_standard import run_od_calibration
386-
387-
calibrations = run_od_calibration("od")
388-
for calibration in calibrations:
389-
calibration_device = f"od{calibration.angle}"
390-
calibration.save_to_disk_for_device(calibration_device)
391-
calibration.set_as_active_calibration_for_device(calibration_device)
392-
393373

394374
def test_positive_correlation_between_temperature_and_heating(
395375
managed_state, logger: CustomLogger, unit: str, experiment: str
@@ -545,8 +525,6 @@ def click_self_test(k: Optional[str], retry_failed: bool) -> int:
545525
test_REF_is_in_correct_position,
546526
test_PD_is_near_0_volts_for_blank,
547527
test_positive_correlation_between_rpm_and_stirring,
548-
# test_run_stirring_calibration,
549-
# test_create_od_calibrations_using_optical_reference_standard,
550528
)
551529

552530
with managed_lifecycle(unit, testing_experiment, "self_test") as managed_state:
Lines changed: 153 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,28 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import annotations
33

4+
import uuid
45
from time import sleep
56
from typing import cast
67

7-
import click
8-
from click import echo
98
from pioreactor import structs
109
from pioreactor import types as pt
1110
from pioreactor.background_jobs.od_reading import average_over_od_readings
1211
from pioreactor.background_jobs.od_reading import REF_keyword
1312
from pioreactor.background_jobs.od_reading import start_od_reading
1413
from pioreactor.calibrations import utils as calibration_utils
15-
from pioreactor.calibrations.cli_helpers import info
16-
from pioreactor.calibrations.cli_helpers import info_heading
17-
from pioreactor.calibrations.cli_helpers import red
1814
from pioreactor.calibrations.registry import CalibrationProtocol
15+
from pioreactor.calibrations.session_flow import run_session_in_cli
16+
from pioreactor.calibrations.session_flow import SessionContext
17+
from pioreactor.calibrations.session_flow import SessionEngine
18+
from pioreactor.calibrations.session_flow import SessionExecutor
19+
from pioreactor.calibrations.session_flow import steps
20+
from pioreactor.calibrations.structured_session import CalibrationSession
21+
from pioreactor.calibrations.structured_session import CalibrationStep
22+
from pioreactor.calibrations.structured_session import utc_iso_timestamp
1923
from pioreactor.config import config
24+
from pioreactor.logging import create_logger
2025
from pioreactor.utils import is_pio_job_running
21-
from pioreactor.utils import managed_lifecycle
2226
from pioreactor.utils.timing import current_utc_datetime
2327
from pioreactor.whoami import get_testing_experiment_name
2428
from pioreactor.whoami import get_unit_name
@@ -29,21 +33,12 @@
2933
DEFAULT_TARGET_ANGLES = {"45", "90", "135"}
3034

3135

32-
def introduction() -> None:
33-
click.clear()
34-
info_heading("OD reference standard calibration")
35-
info("This routine creates OD calibrations using the optical reference standard.")
36-
37-
3836
def get_ir_led_intensity() -> float:
3937
ir_intensity_setting = config.get("od_reading.config", "ir_led_intensity")
4038
if ir_intensity_setting == "auto":
41-
echo(
42-
red(
43-
"ir_led_intensity must be numeric when creating OD calibrations from the optical reference standard. Try 80."
44-
)
39+
raise ValueError(
40+
"ir_led_intensity must be numeric when creating OD calibrations from the optical reference standard. Try 80."
4541
)
46-
raise click.Abort()
4742
return float(ir_intensity_setting)
4843

4944

@@ -69,14 +64,18 @@ def get_channel_angle_map(
6964
}
7065

7166
if not channel_angle_map:
72-
echo(red("No configured PD channels match the selected device."))
73-
raise click.Abort()
67+
raise ValueError("No configured PD channels match the selected device.")
7468

7569
return channel_angle_map
7670

7771

7872
def record_reference_standard(ir_led_intensity: float) -> structs.ODReadings:
79-
info("Recording OD readings...")
73+
logger = create_logger(
74+
"od_reference_standard",
75+
unit=get_unit_name(),
76+
experiment=get_testing_experiment_name(),
77+
)
78+
logger.info("Recording OD readings...")
8079
with start_od_reading(
8180
config["od_config.photodiode_channel"],
8281
interval=None,
@@ -102,69 +101,151 @@ def record_reference_standard(ir_led_intensity: float) -> structs.ODReadings:
102101
f"channel {pd_channel} ({od_reading.angle} deg)={od_reading.od:.6f}"
103102
for pd_channel, od_reading in sorted(averaged_od_readings.ods.items())
104103
)
105-
info(f"Averaged OD readings: {averaged_od_summary}")
104+
logger.info("Averaged OD readings: %s", averaged_od_summary)
106105
return averaged_od_readings
107106

108107

109-
def run_od_calibration(target_device: pt.ODCalibrationDevices) -> list[structs.ODCalibration]:
110-
unit = get_unit_name()
111-
experiment = get_testing_experiment_name()
112-
calibrations: list[structs.ODCalibration] = []
108+
def _record_reference_standard_for_session(
109+
ctx: SessionContext,
110+
ir_led_intensity: float,
111+
) -> dict[str, dict[str, float]] | structs.ODReadings:
112+
if ctx.executor and ctx.mode == "ui":
113+
payload = ctx.executor(
114+
"od_reference_standard_read",
115+
{"ir_led_intensity": ir_led_intensity},
116+
)
117+
raw = payload.get("od_readings", {})
118+
if not isinstance(raw, dict):
119+
raise ValueError("Invalid OD readings payload.")
120+
return raw
121+
return record_reference_standard(ir_led_intensity)
113122

114-
with managed_lifecycle(unit, experiment, "od_calibration"):
115-
introduction()
116123

117-
if is_pio_job_running("od_reading"):
118-
echo(red("OD reading should be turned off."))
119-
raise click.Abort()
124+
def start_reference_standard_session(
125+
target_device: pt.ODCalibrationDevices,
126+
) -> CalibrationSession:
127+
session_id = str(uuid.uuid4())
128+
now = utc_iso_timestamp()
129+
return CalibrationSession(
130+
session_id=session_id,
131+
protocol_name=ODReferenceStandardProtocol.protocol_name,
132+
target_device=target_device,
133+
status="in_progress",
134+
step_id="intro",
135+
data={},
136+
created_at=now,
137+
updated_at=now,
138+
)
120139

121-
ir_led_intensity = get_ir_led_intensity()
122-
channel_angle_map = get_channel_angle_map(target_device)
123140

124-
od_readings = record_reference_standard(ir_led_intensity)
125-
recorded_ods = [0.0, 1000 * STANDARD_OD]
126-
timestamp = current_utc_datetime().strftime("%Y-%m-%d")
141+
def reference_standard_flow(ctx: SessionContext) -> CalibrationStep:
142+
if ctx.session.status != "in_progress":
143+
if ctx.session.result is not None:
144+
return steps.result(ctx.session.result)
145+
return steps.info("Calibration ended", "This calibration session has ended.")
146+
147+
if ctx.step == "intro":
148+
if ctx.inputs.has_inputs:
149+
ctx.step = "record_readings"
150+
return steps.info(
151+
"OD reference standard calibration",
152+
(
153+
"This routine creates OD calibrations using the optical reference standard. "
154+
"Ensure the jig is installed, OD reading is stopped, and ir_led_intensity is numeric."
155+
),
156+
)
127157

128-
for pd_channel, od_reading in od_readings.ods.items():
129-
if pd_channel not in channel_angle_map:
130-
continue
131-
angle = channel_angle_map[pd_channel]
158+
if ctx.step == "record_readings":
159+
if ctx.inputs.has_inputs:
160+
if is_pio_job_running("od_reading"):
161+
raise ValueError("OD reading should be turned off.")
132162

133-
recorded_voltages = [0.0, 1000 * od_reading.od]
134-
curve_data_ = calibration_utils.calculate_poly_curve_of_best_fit(
135-
recorded_ods, recorded_voltages, degree=1
163+
ir_led_intensity = get_ir_led_intensity()
164+
channel_angle_map = get_channel_angle_map(
165+
cast(pt.ODCalibrationDevices, ctx.session.target_device)
136166
)
137-
if len(curve_data_) == 2:
138-
slope, intercept = curve_data_
139-
info(
140-
f"Fitted linear curve for od{angle} (channel {pd_channel}): "
141-
f"y = {slope:.6f}x + {intercept:.6f}"
142-
)
167+
168+
od_readings = _record_reference_standard_for_session(ctx, ir_led_intensity)
169+
recorded_ods = [0.0, 1000 * STANDARD_OD]
170+
timestamp = current_utc_datetime().strftime("%Y-%m-%d_%H-%M-%S")
171+
172+
calibration_links: list[dict[str, str | None]] = []
173+
if isinstance(od_readings, dict):
174+
for raw_channel, od_reading_payload in od_readings.items():
175+
pd_channel = cast(pt.PdChannel, raw_channel)
176+
if pd_channel not in channel_angle_map:
177+
continue
178+
angle = channel_angle_map[pd_channel]
179+
od_value = float(od_reading_payload["od"])
180+
181+
recorded_voltages = [0.0, 1000 * od_value]
182+
curve_data_ = calibration_utils.calculate_poly_curve_of_best_fit(
183+
recorded_ods, recorded_voltages, degree=1
184+
)
185+
calibration = structs.ODCalibration(
186+
created_at=current_utc_datetime(),
187+
calibrated_on_pioreactor_unit=get_unit_name(),
188+
calibration_name=f"od{angle}-optical-reference-standard-{timestamp}",
189+
angle=angle,
190+
curve_data_=curve_data_,
191+
curve_type="poly",
192+
recorded_data={"x": recorded_ods, "y": recorded_voltages},
193+
ir_led_intensity=ir_led_intensity,
194+
pd_channel=pd_channel,
195+
)
196+
calibration_links.append(ctx.store_calibration(calibration, f"od{angle}"))
143197
else:
144-
info(
145-
f"Fitted linear curve for od{angle} (channel {pd_channel}): "
146-
f"coefficients={curve_data_}"
147-
)
148-
149-
calibration = structs.ODCalibration(
150-
created_at=current_utc_datetime(),
151-
calibrated_on_pioreactor_unit=unit,
152-
calibration_name=f"od{angle}-optical-reference-standard-{timestamp}",
153-
angle=angle,
154-
curve_data_=curve_data_,
155-
curve_type="poly",
156-
recorded_data={"x": recorded_ods, "y": recorded_voltages},
157-
ir_led_intensity=ir_led_intensity,
158-
pd_channel=pd_channel,
159-
)
160-
calibrations.append(calibration)
198+
for raw_channel, od_reading_struct in od_readings.ods.items():
199+
pd_channel = cast(pt.PdChannel, raw_channel)
200+
if pd_channel not in channel_angle_map:
201+
continue
202+
angle = channel_angle_map[pd_channel]
203+
od_value = float(od_reading_struct.od)
204+
205+
recorded_voltages = [0.0, 1000 * od_value]
206+
curve_data_ = calibration_utils.calculate_poly_curve_of_best_fit(
207+
recorded_ods, recorded_voltages, degree=1
208+
)
209+
calibration = structs.ODCalibration(
210+
created_at=current_utc_datetime(),
211+
calibrated_on_pioreactor_unit=get_unit_name(),
212+
calibration_name=f"od{angle}-optical-reference-standard-{timestamp}",
213+
angle=angle,
214+
curve_data_=curve_data_,
215+
curve_type="poly",
216+
recorded_data={"x": recorded_ods, "y": recorded_voltages},
217+
ir_led_intensity=ir_led_intensity,
218+
pd_channel=pd_channel,
219+
)
220+
calibration_links.append(ctx.store_calibration(calibration, f"od{angle}"))
221+
222+
if not calibration_links:
223+
raise ValueError("No matching channels were recorded for this calibration.")
224+
225+
ctx.complete({"calibrations": calibration_links})
226+
return steps.action(
227+
"Record reference standard",
228+
"Continue to record OD readings against the optical reference standard.",
229+
)
230+
231+
return steps.info("Unknown step", "This step is not recognized.")
232+
233+
234+
def advance_reference_standard_session(
235+
session: CalibrationSession,
236+
inputs: dict[str, object],
237+
executor: SessionExecutor | None = None,
238+
) -> CalibrationSession:
239+
engine = SessionEngine(flow=reference_standard_flow, session=session, mode="ui", executor=executor)
240+
engine.advance(inputs)
241+
return engine.session
161242

162-
if not calibrations:
163-
echo(red("No matching channels were recorded for this calibration."))
164-
raise click.Abort()
165243

166-
info("Finished reference standard calibration.")
167-
return calibrations
244+
def get_reference_standard_step(
245+
session: CalibrationSession, executor: SessionExecutor | None = None
246+
) -> CalibrationStep | None:
247+
engine = SessionEngine(flow=reference_standard_flow, session=session, mode="ui", executor=executor)
248+
return engine.get_step()
168249

169250

170251
class ODReferenceStandardProtocol(CalibrationProtocol[pt.ODCalibrationDevices]):
@@ -175,4 +256,5 @@ class ODReferenceStandardProtocol(CalibrationProtocol[pt.ODCalibrationDevices]):
175256
def run( # type: ignore
176257
self, target_device: pt.ODCalibrationDevices, *args, **kwargs
177258
) -> list[structs.ODCalibration]:
178-
return run_od_calibration(target_device)
259+
session = start_reference_standard_session(target_device)
260+
return run_session_in_cli(reference_standard_flow, session)

0 commit comments

Comments
 (0)