11# -*- coding: utf-8 -*-
22from __future__ import annotations
33
4+ import uuid
45from time import sleep
56from typing import cast
67
7- import click
8- from click import echo
98from pioreactor import structs
109from pioreactor import types as pt
1110from pioreactor .background_jobs .od_reading import average_over_od_readings
1211from pioreactor .background_jobs .od_reading import REF_keyword
1312from pioreactor .background_jobs .od_reading import start_od_reading
1413from 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
1814from 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
1923from pioreactor .config import config
24+ from pioreactor .logging import create_logger
2025from pioreactor .utils import is_pio_job_running
21- from pioreactor .utils import managed_lifecycle
2226from pioreactor .utils .timing import current_utc_datetime
2327from pioreactor .whoami import get_testing_experiment_name
2428from pioreactor .whoami import get_unit_name
2933DEFAULT_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-
3836def 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
7872def 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
170251class 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