Skip to content

Commit 7e2b338

Browse files
updates
1 parent 2377618 commit 7e2b338

File tree

7 files changed

+69
-25
lines changed

7 files changed

+69
-25
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
- new events in the dosing_automation_events table & export detailing when dosing starts and stops.
66
- new query pattern for faster Experiment Overview chart loading. However, there is a random element to what data is displayed in a time series. Let me know if this is too distracting.
7+
- OD calibrations now support multiple photodiode angles; `pio calibrations run --device od` can emit per-angle calibrations for 45/90/135.
8+
- Calibration CLI now saves and can activate multiple OD calibration outputs in one run.
9+
- Added an update helper to migrate legacy OD calibrations into per-angle devices.
710

811
#### Breaking changes
912

1013
- Removed `/api/workers/<pioreactor_unit>/configuration`; use `/api/units/<pioreactor_unit>/configuration`.
14+
- OD calibration devices are now per-angle (`od45`, `od90`, `od135`). Existing `od` calibration files and active calibration keys must be migrated.
1115

1216
### 25.12.10
1317

config.dev.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ Kd=0.0
5252
1=REF
5353
2=90
5454

55+
5556
[od_reading.config]
5657
# how many samples should the ADC publish per second?
5758
samples_per_second=0.2

core/pioreactor/calibrations/__init__.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,25 @@
1313
from msgspec.yaml import decode as yaml_decode
1414
from msgspec.yaml import encode as yaml_encode
1515
from pioreactor import structs
16-
from pioreactor.types import PumpCalibrationDevices
16+
from pioreactor import types as pt
1717
from pioreactor.utils import local_persistent_storage
1818
from pioreactor.whoami import is_testing_env
19+
from typing import Generic, TypeVar, Literal
20+
1921

2022
if not is_testing_env():
2123
CALIBRATION_PATH = Path("/home/pioreactor/.pioreactor/storage/calibrations/")
2224
else:
2325
CALIBRATION_PATH = Path(os.environ["DOT_PIOREACTOR"]) / "storage" / "calibrations"
2426

2527
# Lookup table for different calibration protocols
26-
Device = str
28+
Device = TypeVar("Device", bound=str)
2729
ProtocolName = str
2830

2931
calibration_protocols: dict[Device, dict[ProtocolName, Type[CalibrationProtocol]]] = defaultdict(dict)
3032

3133

32-
OD_DEVICES = ["od", "od45", "od90", "od135"]
33-
34-
35-
class CalibrationProtocol:
34+
class CalibrationProtocol(Generic[Device]):
3635
protocol_name: ProtocolName
3736
target_device: Device | list[Device]
3837
description = ""
@@ -53,46 +52,46 @@ def run(
5352
raise NotImplementedError("Subclasses must implement this method.")
5453

5554

56-
class SingleVialODProtocol(CalibrationProtocol):
57-
target_device = OD_DEVICES
55+
class SingleVialODProtocol(CalibrationProtocol[pt.ODCalibrationDevices]):
56+
target_device = pt.OD_DEVICES
5857
protocol_name = "single_vial"
5958
description = "Calibrate OD using a single vial"
6059

61-
def run(self, target_device: str, **kwargs) -> structs.OD600Calibration:
60+
def run(self, target_device: pt.ODCalibrationDevices, **kwargs) -> structs.OD600Calibration:
6261
from pioreactor.calibrations.od_calibration_single_vial import run_od_calibration
6362

6463
return run_od_calibration(target_device)
6564

6665

67-
class StandardsODProtocol(CalibrationProtocol):
68-
target_device = OD_DEVICES
66+
class StandardsODProtocol(CalibrationProtocol[pt.ODCalibrationDevices]):
67+
target_device = pt.OD_DEVICES
6968
protocol_name = "standards"
7069
description = "Calibrate OD using standards. Requires multiple vials"
7170

7271
def run( # type: ignore
73-
self, target_device: str, *args, **kwargs
72+
self, target_device: pt.ODCalibrationDevices, *args, **kwargs
7473
) -> structs.OD600Calibration | list[structs.OD600Calibration]:
7574
from pioreactor.calibrations.od_calibration_using_standards import run_od_calibration
7675

7776
return run_od_calibration(target_device)
7877

7978

80-
class DurationBasedPumpProtocol(CalibrationProtocol):
81-
target_device = ["media_pump", "alt_media_pump", "waste_pump"]
79+
class DurationBasedPumpProtocol(CalibrationProtocol[pt.PumpCalibrationDevices]):
80+
target_device = pt.PUMP_DEVICES
8281
protocol_name = "duration_based"
8382

84-
def run(self, target_device: str, **kwargs) -> structs.SimplePeristalticPumpCalibration:
83+
def run(self, target_device: pt.PumpCalibrationDevices, **kwargs) -> structs.SimplePeristalticPumpCalibration:
8584
from pioreactor.calibrations.pump_calibration import run_pump_calibration
8685

8786
return run_pump_calibration(target_device)
8887

8988

90-
class DCBasedStirringProtocol(CalibrationProtocol):
89+
class DCBasedStirringProtocol(CalibrationProtocol[Device]):
9190
target_device = "stirring"
9291
protocol_name = "dc_based"
9392

9493
def run(
95-
self, target_device: str, min_dc: str | None = None, max_dc: str | None = None
94+
self, target_device: Device, min_dc: str | None = None, max_dc: str | None = None
9695
) -> structs.SimpleStirringCalibration:
9796
from pioreactor.calibrations.stirring_calibration import run_stirring_calibration
9897

@@ -102,13 +101,13 @@ def run(
102101

103102

104103
@overload
105-
def load_active_calibration(device: Literal["od", "od45", "od90", "od135"]) -> structs.ODCalibration | None:
104+
def load_active_calibration(device: pt.ODCalibrationDevices) -> structs.ODCalibration | None:
106105
pass
107106

108107

109108
@overload
110109
def load_active_calibration(
111-
device: PumpCalibrationDevices,
110+
device: pt.PumpCalibrationDevices,
112111
) -> structs.SimplePeristalticPumpCalibration | None:
113112
pass
114113

core/pioreactor/calibrations/od_calibration_single_vial.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def get_name_from_user() -> str:
8282

8383

8484
def get_metadata_from_user(
85-
target_device: str,
85+
target_device: pt.ODCalibrationDevices,
8686
) -> tuple[pt.CalibratedOD, pt.CalibratedOD, pt.mL, pt.PdAngle, pt.PdChannel]:
8787
if config.get("od_reading.config", "ir_led_intensity") == "auto":
8888
echo(
@@ -421,7 +421,7 @@ def to_struct(
421421
return data_blob
422422

423423

424-
def run_od_calibration(target_device: str) -> structs.OD600Calibration:
424+
def run_od_calibration(target_device: pt.ODCalibrationDevices) -> structs.OD600Calibration:
425425
unit = get_unit_name()
426426
experiment = get_testing_experiment_name()
427427
curve_data_: list[float] = []

core/pioreactor/calibrations/od_calibration_using_standards.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ def get_name_from_user() -> str:
8585
return name
8686

8787

88-
def get_metadata_from_user(target_device: str) -> dict[pt.PdChannel, pt.PdAngle]:
88+
def get_metadata_from_user(target_device: pt.ODCalibrationDevices) -> dict[pt.PdChannel, pt.PdAngle]:
8989
if config.get("od_reading.config", "ir_led_intensity") == "auto":
9090
echo(
9191
red(
@@ -317,7 +317,7 @@ def get_voltages_from_adc() -> dict[pt.PdChannel, pt.Voltage]:
317317
return od600_values, voltages_by_channel
318318

319319

320-
def run_od_calibration(target_device: str) -> list[structs.OD600Calibration]:
320+
def run_od_calibration(target_device: pt.ODCalibrationDevices) -> list[structs.OD600Calibration]:
321321
unit = get_unit_name()
322322
experiment = get_testing_experiment_name()
323323
curve_type = "poly"

core/pioreactor/types.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,5 +158,9 @@ class PublishableSetting(t.TypedDict, total=False):
158158
type mL = float
159159
type Seconds = float
160160

161-
# calibration stuff
162-
type PumpCalibrationDevices = t.Literal["waste_pump", "media_pump", "alt_media_pump"]
161+
# calibration data
162+
OD_DEVICES = ["od", "od45", "od90", "od135"]
163+
PUMP_DEVICES = ["media_pump", "alt_media_pump", "waste_pump"]
164+
165+
type ODCalibrationDevices = t.Literal["od", "od45", "od90", "od135"]
166+
type PumpCalibrationDevices = t.Literal["media_pump", "alt_media_pump", "waste_pump"]

core/tests/test_od_reading.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -856,6 +856,42 @@ def test_calibration_not_present() -> None:
856856
assert len(od.calibration_transformer.models) == 0, od.calibration_transformer.models
857857

858858

859+
def test_calibration_duplicate_channel_raises_value_error() -> None:
860+
cal_1 = structs.OD600Calibration(
861+
created_at=current_utc_datetime(),
862+
curve_type="poly",
863+
curve_data_=[2.0, 0.0],
864+
calibration_name="linear_a",
865+
ir_led_intensity=90.0,
866+
angle="90",
867+
recorded_data={"x": [0, 2], "y": [0, 4]},
868+
pd_channel="2",
869+
calibrated_on_pioreactor_unit=get_unit_name(),
870+
)
871+
cal_2 = structs.OD600Calibration(
872+
created_at=current_utc_datetime(),
873+
curve_type="poly",
874+
curve_data_=[1.0, 0.0],
875+
calibration_name="linear_b",
876+
ir_led_intensity=90.0,
877+
angle="90",
878+
recorded_data={"x": [0, 1], "y": [0, 1]},
879+
pd_channel="2",
880+
calibrated_on_pioreactor_unit=get_unit_name(),
881+
)
882+
883+
with pytest.raises(ValueError, match="already hydrated"):
884+
start_od_reading(
885+
make_channels("REF", "90"),
886+
interval=None,
887+
fake_data=True,
888+
experiment="test_calibration_duplicate_channel_raises_value_error",
889+
unit=get_unit_name(),
890+
calibration=[cal_1, cal_2],
891+
ir_led_intensity=90.0,
892+
)
893+
894+
859895
def test_calibration_multi_angle_active_calibrations() -> None:
860896
experiment = "test_calibration_multi_angle_active_calibrations"
861897

0 commit comments

Comments
 (0)