@@ -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
0 commit comments