Skip to content

Commit 7668ab2

Browse files
nicer
1 parent 8d4681f commit 7668ab2

File tree

13 files changed

+924
-858
lines changed

13 files changed

+924
-858
lines changed
Lines changed: 2 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,3 @@
1-
# Calibration CLI UX Spec
1+
# Calibrations
22

3-
This spec defines the CLI UX conventions for calibration flows. Apply these patterns consistently across all calibration scripts.
4-
5-
Use pioreactor.calibrations.cli_helpers to assist.
6-
7-
## Goals
8-
9-
- Keep calibration outputs unchanged (same computed results, same data structures).
10-
- Make the CLI flow readable and consistent.
11-
- Separate user input, information, and actions visually.
12-
13-
## Colors
14-
15-
Use pioreactor.calibrations.cli_helpers to get functions for these.
16-
17-
- User input prompts: green.
18-
- Informational messages and headings: white.
19-
- User actions (physical steps): cyan.
20-
- Errors / aborts: red.
21-
22-
## Message Types
23-
24-
- **Info:** single-line status or explanation (white).
25-
- **Heading:** key sections, white + bold + underline.
26-
- **Action:** physical steps to perform, cyan.
27-
- **Prompt:** questions or inputs, green.
28-
- **Error:** aborts or invalid states, red.
29-
30-
## Spacing Rules
31-
32-
- Use an action block for physical steps, with a blank line before and after.
33-
- Avoid stacking info, prompt, and action on the same line.
34-
- When a prompt follows an action block, keep the blank line between them.
35-
36-
## Prompt Style
37-
38-
- Don't use a trailing colon for input prompts, e.g. `Enter OD600 measurement` .
39-
- Use a question mark for confirmations, e.g. `Continue?`.
40-
- Keep prompts short and specific.
41-
- If the information is just pausing to start something, add `...` at the end. Example: "Warming up OD...", "Waiting for X..."
42-
43-
## Consistency Notes
44-
45-
- Prefer simple, direct language and consistent verbs (Enter, Confirm, Record).
46-
- Avoid changing calibration behavior or output format.
47-
- Reuse shared helpers for styling and spacing where possible.
3+
Use `scratch/generate_calibration_flow_graphs.py` to generate directed graphs of the flows.

core/pioreactor/calibrations/protocols/od_reference_standard.py

Lines changed: 98 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import uuid
55
from time import sleep
66
from typing import cast
7+
from typing import ClassVar
78

89
from pioreactor import structs
910
from pioreactor import types as pt
@@ -12,10 +13,14 @@
1213
from pioreactor.background_jobs.od_reading import start_od_reading
1314
from pioreactor.calibrations import utils as calibration_utils
1415
from pioreactor.calibrations.registry import CalibrationProtocol
16+
from pioreactor.calibrations.session_flow import advance_session
17+
from pioreactor.calibrations.session_flow import CalibrationComplete
18+
from pioreactor.calibrations.session_flow import get_session_step
1519
from pioreactor.calibrations.session_flow import run_session_in_cli
1620
from pioreactor.calibrations.session_flow import SessionContext
17-
from pioreactor.calibrations.session_flow import SessionEngine
1821
from pioreactor.calibrations.session_flow import SessionExecutor
22+
from pioreactor.calibrations.session_flow import SessionStep
23+
from pioreactor.calibrations.session_flow import StepRegistry
1924
from pioreactor.calibrations.session_flow import steps
2025
from pioreactor.calibrations.structured_session import CalibrationSession
2126
from pioreactor.calibrations.structured_session import CalibrationStep
@@ -138,15 +143,10 @@ def start_reference_standard_session(
138143
)
139144

140145

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+
class Intro(SessionStep):
147+
step_id = "intro"
146148

147-
if ctx.step == "intro":
148-
if ctx.inputs.has_inputs:
149-
ctx.step = "record_readings"
149+
def render(self, ctx: SessionContext) -> CalibrationStep:
150150
return steps.info(
151151
"OD reference standard calibration",
152152
(
@@ -155,106 +155,121 @@ def reference_standard_flow(ctx: SessionContext) -> CalibrationStep:
155155
),
156156
)
157157

158-
if ctx.step == "record_readings":
158+
def advance(self, ctx: SessionContext) -> SessionStep | None:
159159
if ctx.inputs.has_inputs:
160-
if is_pio_job_running("od_reading"):
161-
raise ValueError("OD reading should be turned off.")
162-
163-
ir_led_intensity = get_ir_led_intensity()
164-
channel_angle_map = get_channel_angle_map(
165-
cast(pt.ODCalibrationDevices, ctx.session.target_device)
166-
)
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")
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}-optics-calibration-jig-{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}"))
197-
else:
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})
160+
return RecordReadings()
161+
return None
162+
163+
164+
class RecordReadings(SessionStep):
165+
step_id = "record_readings"
166+
167+
def render(self, ctx: SessionContext) -> CalibrationStep:
226168
return steps.action(
227169
"Record reference standard",
228170
"Continue to record OD readings against the optical reference standard.",
229171
)
230172

231-
return steps.info("Unknown step", "This step is not recognized.")
173+
def advance(self, ctx: SessionContext) -> SessionStep | None:
174+
if is_pio_job_running("od_reading"):
175+
raise ValueError("OD reading should be turned off.")
176+
177+
ir_led_intensity = get_ir_led_intensity()
178+
channel_angle_map = get_channel_angle_map(cast(pt.ODCalibrationDevices, ctx.session.target_device))
179+
180+
od_readings = _record_reference_standard_for_session(ctx, ir_led_intensity)
181+
recorded_ods = [0.0, 1000 * STANDARD_OD]
182+
timestamp = current_utc_datetime().strftime("%Y-%m-%d_%H-%M")
183+
184+
calibration_links: list[dict[str, str | None]] = []
185+
if isinstance(od_readings, dict):
186+
for raw_channel, od_reading_payload in od_readings.items():
187+
pd_channel = cast(pt.PdChannel, raw_channel)
188+
if pd_channel not in channel_angle_map:
189+
continue
190+
angle = channel_angle_map[pd_channel]
191+
od_value = float(od_reading_payload["od"])
192+
193+
recorded_voltages = [0.0, 1000 * od_value]
194+
curve_data_ = calibration_utils.calculate_poly_curve_of_best_fit(
195+
recorded_ods, recorded_voltages, degree=1
196+
)
197+
calibration = structs.ODCalibration(
198+
created_at=current_utc_datetime(),
199+
calibrated_on_pioreactor_unit=get_unit_name(),
200+
calibration_name=f"od{angle}-optics-calibration-jig-{timestamp}",
201+
angle=angle,
202+
curve_data_=curve_data_,
203+
curve_type="poly",
204+
recorded_data={"x": recorded_ods, "y": recorded_voltages},
205+
ir_led_intensity=ir_led_intensity,
206+
pd_channel=pd_channel,
207+
)
208+
calibration_links.append(ctx.store_calibration(calibration, f"od{angle}"))
209+
else:
210+
for raw_channel, od_reading_struct in od_readings.ods.items():
211+
pd_channel = cast(pt.PdChannel, raw_channel)
212+
if pd_channel not in channel_angle_map:
213+
continue
214+
angle = channel_angle_map[pd_channel]
215+
od_value = float(od_reading_struct.od)
216+
217+
recorded_voltages = [0.0, 1000 * od_value]
218+
curve_data_ = calibration_utils.calculate_poly_curve_of_best_fit(
219+
recorded_ods, recorded_voltages, degree=1
220+
)
221+
calibration = structs.ODCalibration(
222+
created_at=current_utc_datetime(),
223+
calibrated_on_pioreactor_unit=get_unit_name(),
224+
calibration_name=f"od{angle}-optical-reference-standard-{timestamp}",
225+
angle=angle,
226+
curve_data_=curve_data_,
227+
curve_type="poly",
228+
recorded_data={"x": recorded_ods, "y": recorded_voltages},
229+
ir_led_intensity=ir_led_intensity,
230+
pd_channel=pd_channel,
231+
)
232+
calibration_links.append(ctx.store_calibration(calibration, f"od{angle}"))
233+
234+
if not calibration_links:
235+
raise ValueError("No matching channels were recorded for this calibration.")
236+
237+
ctx.complete({"calibrations": calibration_links})
238+
return CalibrationComplete()
239+
240+
241+
_REFERENCE_STANDARD_STEPS: StepRegistry = {
242+
Intro.step_id: Intro,
243+
RecordReadings.step_id: RecordReadings,
244+
}
232245

233246

234247
def advance_reference_standard_session(
235248
session: CalibrationSession,
236249
inputs: dict[str, object],
237250
executor: SessionExecutor | None = None,
238251
) -> CalibrationSession:
239-
engine = SessionEngine(flow=reference_standard_flow, session=session, mode="ui", executor=executor)
240-
engine.advance(inputs)
241-
return engine.session
252+
return advance_session(_REFERENCE_STANDARD_STEPS, session, inputs, executor)
242253

243254

244255
def get_reference_standard_step(
245256
session: CalibrationSession, executor: SessionExecutor | None = None
246257
) -> CalibrationStep | None:
247-
engine = SessionEngine(flow=reference_standard_flow, session=session, mode="ui", executor=executor)
248-
return engine.get_step()
258+
return get_session_step(_REFERENCE_STANDARD_STEPS, session, executor)
249259

250260

251261
class ODReferenceStandardProtocol(CalibrationProtocol[pt.ODCalibrationDevices]):
252262
target_device = pt.OD_DEVICES
253263
protocol_name = "od_reference_standard"
254264
description = "Calibrate OD using the Pioreactor Optical Reference Standard."
265+
step_registry: ClassVar[StepRegistry] = _REFERENCE_STANDARD_STEPS
266+
267+
@classmethod
268+
def start_session(cls, target_device: pt.ODCalibrationDevices) -> CalibrationSession:
269+
return start_reference_standard_session(target_device)
255270

256271
def run( # type: ignore
257272
self, target_device: pt.ODCalibrationDevices, *args, **kwargs
258273
) -> list[structs.ODCalibration]:
259274
session = start_reference_standard_session(target_device)
260-
return run_session_in_cli(reference_standard_flow, session)
275+
return run_session_in_cli(_REFERENCE_STANDARD_STEPS, session)

0 commit comments

Comments
 (0)