Skip to content

Commit 8b8b8bd

Browse files
more ux for calibrations
1 parent 2f3b56e commit 8b8b8bd

File tree

8 files changed

+232
-57
lines changed

8 files changed

+232
-57
lines changed

core/pioreactor/calibrations/protocols/od_fusion_offset.py

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -210,11 +210,11 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
210210
return steps.info(
211211
"Fusion OD two-point offset",
212212
(
213-
"This protocol adjusts an existing fused OD estimator using two OD600 standards. "
213+
"This protocol adjusts an existing fused OD estimator using two OD standards. "
214214
"You will need:\n"
215215
"1. A Pioreactor XR.\n"
216216
"2. An existing od_fused estimator on any worker.\n"
217-
"3. Two OD600 standard vials with stir bars.\n\n"
217+
"3. Two standard vials of known OD with stir bars.\n\n"
218218
"For best results, choose standards that bracket the regime you care about and stay within "
219219
"a locally monotonic region (i.e. not beyond the saturation point)."
220220
),
@@ -346,8 +346,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
346346
)
347347

348348
def advance(self, ctx: SessionContext) -> SessionStep | None:
349-
default_rpm = float(ctx.data.get("rpm", 500.0))
350-
rpm = ctx.inputs.float("rpm", minimum=0.0, default=default_rpm)
349+
rpm = ctx.inputs.float("rpm")
351350
ctx.data["rpm"] = rpm
352351
ctx.data["standard_index"] = 1
353352
ctx.data["standards"] = []
@@ -384,13 +383,13 @@ class StandardOdInput(SessionStep):
384383
def render(self, ctx: SessionContext) -> CalibrationStep:
385384
standard_index = int(ctx.data.get("standard_index", 1))
386385
return steps.form(
387-
f"Enter OD600 for standard {standard_index} of {STANDARDS_REQUIRED}",
388-
"Enter the OD600 value for the standard vial.",
389-
[fields.float("standard_od", label="OD600", minimum=1e-6)],
386+
f"Enter OD for standard {standard_index} of {STANDARDS_REQUIRED}",
387+
"Enter the OD value for the standard vial.",
388+
[fields.float("standard_od", label="OD", minimum=1e-6)],
390389
)
391390

392391
def advance(self, ctx: SessionContext) -> SessionStep | None:
393-
ctx.data["standard_od"] = ctx.inputs.float("standard_od", minimum=1e-6)
392+
ctx.data["standard_od"] = ctx.inputs.float("standard_od")
394393
return RecordObservation()
395394

396395

@@ -554,11 +553,11 @@ class FusionOffsetODProtocol(CalibrationProtocol[pt.ODFusedCalibrationDevice]):
554553
protocol_name = "od_fusion_offset"
555554
target_device = [cast(pt.ODFusedCalibrationDevice, ESTIMATOR_DEVICE)]
556555
title = "Fusion OD two-point offset"
557-
description = "Adjust an existing fused OD estimator using two OD600 standards."
556+
description = "Adjust an existing fused OD estimator using two OD standards."
558557
requirements = (
559558
"Requires XR model with 45°, 90°, and 135° sensors.",
560559
"An existing od_fused estimator.",
561-
"Two OD600 standard vials with stir bars.",
560+
"Two standard vials of known OD with stir bars.",
562561
)
563562
step_registry = _FUSION_OFFSET_STEPS
564563
priority = 2

core/pioreactor/calibrations/protocols/od_fusion_standards.py

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def _measure_fusion_standard_samples(
9292
unit=get_unit_name(),
9393
experiment=get_testing_experiment_name(),
9494
) as st:
95-
st.block_until_rpm_is_close_to_target(abs_tolerance=120)
95+
st.block_until_rpm_is_close_to_target(abs_tolerance=60)
9696

9797
with start_od_reading(
9898
config["od_config.photodiode_channel"],
@@ -207,6 +207,7 @@ def start_fusion_session() -> CalibrationSession:
207207
data={
208208
"channel_angle_map": to_builtins(channel_angle_map),
209209
"records": [],
210+
"standards": [],
210211
},
211212
created_at=utc_iso_timestamp(),
212213
updated_at=utc_iso_timestamp(),
@@ -218,12 +219,12 @@ class Intro(SessionStep):
218219

219220
def render(self, ctx: SessionContext) -> CalibrationStep:
220221
return steps.info(
221-
"Fusion OD calibration",
222+
"Introduction",
222223
(
223-
"This protocol fits a fused OD model using the 45°, 90°, and 135° sensors. "
224-
"You will need:\n"
224+
"This protocol fits a OD model using fusing together 45°, 90°, and 135° signals into a single measurement."
225+
"You will need:\n\n"
225226
"1. A Pioreactor XR.\n"
226-
"2. At least four OD600 standards in Pioreactor vials, with stir bars. It helps to enumerate them 1..N.\n"
227+
"2. At least four OD600 standards in Pioreactor vials, with stir bars.\n\n\u00a0\u00a0\u00a0\u00a0- It helps to number them 1, 2, ... N.\n\u00a0\u00a0\u00a0\u00a0- You don't need one of the standard vials to be a blank.\n"
227228
),
228229
)
229230

@@ -287,7 +288,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
287288
)
288289

289290
def advance(self, ctx: SessionContext) -> SessionStep | None:
290-
ctx.data["rpm"] = ctx.inputs.float("rpm", minimum=0.0)
291+
ctx.data["rpm"] = ctx.inputs.float("rpm")
291292
return PlaceStandard()
292293

293294

@@ -298,10 +299,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
298299
standard_index = int(ctx.data.get("standard_index", 1))
299300
step = steps.action(
300301
f"Place standard vial {standard_index}",
301-
(
302-
f"Place standard vial {standard_index} with a stir bar into the Pioreactor. "
303-
"We will take OD readings, then you will remove it."
304-
),
302+
(f"Place standard vial {standard_index} with a stir bar into the Pioreactor. "),
305303
)
306304
step.metadata = {
307305
"image": {
@@ -326,12 +324,37 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
326324
step = steps.form(
327325
f"Record standard vial {standard_index}",
328326
f"Enter the OD600 measurement for standard vial {standard_index}.",
329-
[fields.float("od_value", label="OD600", minimum=0.0001)],
327+
[
328+
fields.float(
329+
"od_value",
330+
label="OD600",
331+
minimum=0.0001,
332+
min_error_msg="Don't use a blank in this protocol — we fit in log-space.",
333+
)
334+
],
330335
)
336+
standards = ctx.data.get("standards", [])
337+
if isinstance(standards, list):
338+
rows = []
339+
for item in standards:
340+
if not isinstance(item, dict):
341+
continue
342+
index = item.get("index")
343+
value = item.get("od_value")
344+
if isinstance(index, int) and isinstance(value, (int, float)):
345+
rows.append([index, value])
346+
if rows:
347+
step.metadata = {
348+
"table": {
349+
"title": "Standards recorded so far",
350+
"columns": ["#", "OD600"],
351+
"rows": rows,
352+
}
353+
}
331354
return step
332355

333356
def advance(self, ctx: SessionContext) -> SessionStep | None:
334-
od_value = ctx.inputs.float("od_value", minimum=0.0001)
357+
od_value = ctx.inputs.float("od_value")
335358
rpm = float(ctx.data["rpm"])
336359

337360
ctx.data.setdefault("standard_index", 1)
@@ -347,12 +370,13 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
347370
standard_index = int(ctx.data.get("standard_index", 1))
348371
step = steps.action(
349372
f"Recording standard vial {standard_index}",
350-
"Press Continue to take OD readings for this standard.",
373+
"Press Continue to start stirring and take OD readings for this standard.",
351374
)
352375
return step
353376

354377
def advance(self, ctx: SessionContext) -> SessionStep | None:
355378
od_value = float(ctx.data["current_standard_od"])
379+
standard_index = int(ctx.data.get("standard_index", 1))
356380
rpm = float(ctx.data["rpm"])
357381

358382
samples = _measure_fusion_standard_for_session(
@@ -367,6 +391,11 @@ def advance(self, ctx: SessionContext) -> SessionStep | None:
367391
records.append([angle, log10(od_value), log(max(reading, 1e-12))])
368392

369393
ctx.data["records"] = records
394+
standards = ctx.data.get("standards", [])
395+
if not isinstance(standards, list):
396+
standards = []
397+
standards.append({"index": standard_index, "od_value": od_value})
398+
ctx.data["standards"] = standards
370399
return RemoveObservation()
371400

372401

@@ -454,10 +483,10 @@ class FusionStandardsODProtocol(CalibrationProtocol[pt.ODFusedCalibrationDevice]
454483
protocol_name = "od_fusion_standards"
455484
target_device = [cast(pt.ODFusedCalibrationDevice, pt.OD_FUSED_DEVICE)]
456485
title = "Fusion OD using standards"
457-
description = "Fit a fused OD model using standards measured at 45°, 90°, and 135° sensors."
486+
description = "Fit an OD model by fusing the 45°, 90°, and 135° sensor readings into a single measurement"
458487
requirements = (
459488
"Requires XR model with 45°, 90°, and 135° sensors.",
460-
"At least four vials containing standards with known OD600 value",
489+
"At least four vials containing standards with known OD value",
461490
"Stir bars",
462491
)
463492
step_registry = _FUSION_STEPS

core/pioreactor/calibrations/protocols/od_standards.py

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -365,8 +365,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
365365
)
366366

367367
def advance(self, ctx: SessionContext) -> SessionStep | None:
368-
default_rpm = ctx.data["rpm"]
369-
rpm = ctx.inputs.float("rpm", minimum=0, maximum=10000, default=default_rpm)
368+
rpm = ctx.inputs.float("rpm")
370369
ctx.data["rpm"] = rpm
371370
return PlaceStandard()
372371

@@ -410,21 +409,35 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
410409
standard_index = int(ctx.data.get("standard_index", 1))
411410
step = steps.form(
412411
f"Record standard {standard_index}",
413-
f"Enter the OD600 measurement for standard vial {standard_index}. Then, press Continue to take a reading from the Pioreactor.",
412+
f"Enter the OD600 measurement for standard vial {standard_index}. Then, press Continue to take an OD reading for this standard.",
414413
[fields.float("od600_value", label="OD600 value", minimum=0)],
415414
)
415+
od600_values = ctx.data.get("od600_values", [])
416+
if isinstance(od600_values, list) and od600_values:
417+
rows = []
418+
for index, value in enumerate(od600_values, start=1):
419+
if isinstance(value, (int, float)):
420+
rows.append([index, value])
421+
if rows:
422+
step.metadata = {
423+
"table": {
424+
"title": "Standards recorded so far",
425+
"columns": ["#", "OD600"],
426+
"rows": rows,
427+
}
428+
}
416429
chart = _build_standards_chart_metadata(
417430
ctx.data["od600_values"],
418431
ctx.data["voltages_by_channel"],
419432
_get_channel_angle_map(ctx),
420433
)
421434
if chart:
422-
step.metadata = {"chart": chart}
435+
step.metadata = {**step.metadata, "chart": chart} if step.metadata else {"chart": chart}
423436
return step
424437

425438
def advance(self, ctx: SessionContext) -> SessionStep | None:
426439
channel_angle_map = _get_channel_angle_map(ctx)
427-
od600_value = ctx.inputs.float("od600_value", minimum=0)
440+
od600_value = ctx.inputs.float("od600_value")
428441
voltages = _measure_standard_for_session(ctx, ctx.data["rpm"], channel_angle_map)
429442
ctx.data.setdefault("standard_index", 1)
430443
ctx.data["od600_values"].append(od600_value)
@@ -519,18 +532,32 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
519532
"Enter the OD600 measurement for the blank.",
520533
[fields.float("od600_blank", label="Blank OD600 value", minimum=0)],
521534
)
535+
od600_values = ctx.data.get("od600_values", [])
536+
if isinstance(od600_values, list) and od600_values:
537+
rows = []
538+
for index, value in enumerate(od600_values, start=1):
539+
if isinstance(value, (int, float)):
540+
rows.append([index, value])
541+
if rows:
542+
step.metadata = {
543+
"table": {
544+
"title": "Standards recorded so far",
545+
"columns": ["#", "OD600"],
546+
"rows": rows,
547+
}
548+
}
522549
chart = _build_standards_chart_metadata(
523550
ctx.data["od600_values"],
524551
ctx.data["voltages_by_channel"],
525552
_get_channel_angle_map(ctx),
526553
)
527554
if chart:
528-
step.metadata = {"chart": chart}
555+
step.metadata = {**step.metadata, "chart": chart} if step.metadata else {"chart": chart}
529556
return step
530557

531558
def advance(self, ctx: SessionContext) -> SessionStep | None:
532559
channel_angle_map = _get_channel_angle_map(ctx)
533-
od600_blank = ctx.inputs.float("od600_blank", minimum=0)
560+
od600_blank = ctx.inputs.float("od600_blank")
534561
voltages = _measure_standard_for_session(ctx, ctx.data["rpm"], channel_angle_map)
535562
ctx.data["od600_values"].append(od600_blank)
536563
for channel, voltage in voltages.items():

core/pioreactor/calibrations/protocols/pump_duration_based.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
306306
)
307307

308308
def advance(self, ctx: SessionContext) -> SessionStep | None:
309-
hz = ctx.inputs.float("hz", minimum=0.1, maximum=10000, default=250.0)
310-
dc = ctx.inputs.float("dc", minimum=0, maximum=100, default=100.0)
309+
hz = ctx.inputs.float("hz")
310+
dc = ctx.inputs.float("dc")
311311
ctx.data["hz"] = hz
312312
ctx.data["dc"] = dc
313313
return TubingIntoWater()
@@ -348,7 +348,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
348348
)
349349

350350
def advance(self, ctx: SessionContext) -> SessionStep | None:
351-
duration_s = ctx.inputs.float("prime_duration_s", minimum=0.1, default=20.0)
351+
duration_s = ctx.inputs.float("prime_duration_s")
352352
_execute_pump_for_calibration(ctx, _get_pump_device(ctx), duration_s)
353353
ctx.data["prime_duration_s"] = duration_s
354354
ctx.data["tracer_duration_s"] = ctx.data.get("tracer_duration_s", 1.0)
@@ -394,7 +394,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
394394
)
395395

396396
def advance(self, ctx: SessionContext) -> SessionStep | None:
397-
tracer_ml = ctx.inputs.float("volume_ml", minimum=0.0001)
397+
tracer_ml = ctx.inputs.float("volume_ml")
398398
mls_to_calibrate_for = ctx.data["mls_to_calibrate_for"]
399399
tracer_duration = float(ctx.data.get("tracer_duration_s", 1.0))
400400
min_duration = min(mls_to_calibrate_for) * 0.8 / tracer_ml * tracer_duration
@@ -469,7 +469,7 @@ def render(self, ctx: SessionContext) -> CalibrationStep:
469469
return step
470470

471471
def advance(self, ctx: SessionContext) -> SessionStep | None:
472-
volume_ml = ctx.inputs.float("volume_ml", minimum=0.0001)
472+
volume_ml = ctx.inputs.float("volume_ml")
473473
results = ctx.data["results"]
474474
results.append(volume_ml)
475475
ctx.data["results"] = results

0 commit comments

Comments
 (0)