99
1010from dlstbx .util import ChainMapWithReplacement
1111
12+ type Limits = tuple [float , float ]
13+
1214
1315def scale_parameter (
1416 value : float , scale_factor : float , limits : tuple [float , float ] | None = None
@@ -72,12 +74,46 @@ class AgamemnonParameters(BaseModel):
7274 wavelength : float = Field (gt = 0 )
7375
7476
77+ class AgamemnonLimits (BaseModel ):
78+ transmission : Limits
79+ exposure_time : Limits
80+ dose : Limits
81+
82+
7583def parse_agamemnon_recipe (recipe_path : Path ) -> list [AgamemnonParameters ]:
7684 with open (recipe_path , "r" ) as f :
7785 recipe = yaml .safe_load (f )
7886 return [AgamemnonParameters (** step ) for step in recipe ]
7987
8088
89+ def parse_agamemnon_config (recipe_config_path : Path ) -> dict [str , AgamemnonLimits ]:
90+ inf = float ("inf" )
91+ default_limits = AgamemnonLimits (
92+ transmission = (- inf , inf ), exposure_time = (- inf , inf ), dose = (- inf , inf )
93+ )
94+
95+ def parse_limits (limits ):
96+ limits = limits [0 ]
97+ params = (limits .get ("min" , - inf ), limits .get ("max" , inf ))
98+ return params
99+
100+ with open (recipe_config_path , "r" ) as f :
101+ recipe_conf = yaml .safe_load (f )
102+
103+ agamemnon_limits = {}
104+ for strategy , parameters in recipe_conf .items ():
105+ if strategy == "globals" :
106+ continue
107+ if not isinstance (parameters , dict ):
108+ continue
109+
110+ agamemnon_limits [strategy ] = default_limits .model_copy (
111+ update = {param : parse_limits (limits ) for param , limits in parameters .items ()}
112+ )
113+
114+ return agamemnon_limits
115+
116+
81117def parse_config_file (config_file : Path ) -> dict :
82118 config = {}
83119
@@ -152,9 +188,11 @@ def generate_strategy(
152188 """Generate a strategy from the results of an upstream pipeline"""
153189 self .log .info ("Received strategy request, generating strategy" )
154190
191+ recipe_params = rw .recipe_step ["parameters" ]
155192 parameters = ChainMapWithReplacement (
156193 message .get ("parameters" , {}) if isinstance (message , dict ) else {},
157- rw .recipe_step ["parameters" ].get ("ispyb_parameters" , {}),
194+ recipe_params .get ("ispyb_parameters" , {}),
195+ recipe_params ,
158196 substitutions = rw .environment ,
159197 )
160198 self .log .info (f"Received parameters for strategy generation:\n { parameters } " )
@@ -177,6 +215,7 @@ def generate_strategy(
177215 if isinstance (parameters ["resolution" ], list )
178216 else float (parameters ["resolution" ])
179217 )
218+ dc_transmission = float (parameters .get ("transmission" , 100 )) / 100
180219 resolution_offset = 0.5
181220 min_resolution = 0.9
182221 resolution = max ((resolution_estimate ) - resolution_offset , min_resolution )
@@ -193,31 +232,24 @@ def generate_strategy(
193232 )
194233 beamline_config = parse_config_file (beamline_config_file )
195234
196- scaled_transmission = parameters .get ("scaled_transmission" , 1.0 )
197-
198- transmission_limits = (
199- get_beamline_param (beamline_config , ("gda.mx.udc.minTransmission" ,), 0.0 ),
200- min (get_beamline_param (beamline_config , ("gda.mx.udc.maxTransmission" ,), 1.0 ),
201- scaled_transmission )
235+ recommended_max_transmission = parameters .get ("scaled_transmission" , 1.0 )
236+ base_recipe_home = Path (f"/dls_sw/{ beamline } /etc/agamemnon-recipes" )
237+ agamemnon_recipe_config = base_recipe_home / "recipe_config.yaml"
238+ agamemnon_limits : dict [str , AgamemnonLimits ] = parse_agamemnon_config (
239+ agamemnon_recipe_config
202240 )
203- exposure_time_limits = (
204- get_beamline_param (
205- beamline_config ,
206- ("gda.mx.udc.minExposureTime" , "gda.exptTableModel.minExposureTime" ),
207- 0.002 ,
208- ),
209- get_beamline_param (
210- beamline_config ,
211- ("gda.mx.udc.maxExposureTime" , "gda.exptTableModel.maxExposureTime" ),
212- float ("inf" ),
213- ),
241+ recipes = (
242+ ("OSC.yaml" , "OSC" , "Native" ),
243+ ("Ligand binding.yaml" , "Ligand binding" , "Ligand" ),
244+ ("SAD.yaml" , "SAD" , "Phasing" ),
214245 )
215-
216- recipes = {"OSC.yaml" : "Native" , "Ligand binding.yaml" : "Ligand" }
217246 ispyb_command_list = []
218247
219- for recipe , recipe_alias in recipes .items ():
220- recipe_path = Path (f"/dls_sw/{ beamline } /etc/agamemnon-recipes/{ recipe } " )
248+ beamline_wants_dose_displayed = beamline in [
249+ "i04" ,
250+ ]
251+ for recipe_file , recipe_name , recipe_alias in recipes :
252+ recipe_path = base_recipe_home / recipe_file
221253 if not recipe_path .is_file ():
222254 self .log .error (
223255 f"Recipe file { recipe_path } not found, terminating strategy generation"
@@ -250,41 +282,19 @@ def generate_strategy(
250282 }
251283 ispyb_command_list .append (d )
252284
285+ agamemnon_recipe_limits = agamemnon_limits [recipe_name ]
253286 for n_step , recipe_step in enumerate (recipe_steps , start = 1 ):
254287 scale = 1.0
255288 default_wavelength = recipe_step .wavelength
256- scale *= get_wavelength_scale (wavelength , default_wavelength )
257289 scale *= get_resolution_scale (resolution )
258-
259- dose , _ = scale_parameter ( recipe_step .dose , scale )
260-
290+ dose , _ = scale_parameter (
291+ recipe_step .dose , scale , limits = agamemnon_recipe_limits . dose
292+ )
261293 rotation_axis = recipe_step .scan_axis
262294 rotation_start = recipe_step .__getattribute__ (f"{ rotation_axis } _start" )
263295 rotation_increment = recipe_step .__getattribute__ (
264296 f"{ rotation_axis } _increment"
265297 )
266- transmission = recipe_step .transmission
267- exposure_time = recipe_step .exposure_time
268-
269- # Runs twice to ensure that limits are applied correctly to both parameters, as they are interdependent - is this necessary?
270- for _ in range (2 ):
271- if scale > 1.0 :
272- transmission , scale = scale_parameter (
273- transmission , scale , limits = transmission_limits
274- )
275- exposure_time , scale = scale_parameter (
276- exposure_time , scale , limits = exposure_time_limits
277- )
278- else :
279- exposure_time , scale = scale_parameter (
280- exposure_time , scale , limits = exposure_time_limits
281- )
282- transmission , scale = scale_parameter (
283- transmission , scale , limits = transmission_limits
284- )
285- self .log .debug (
286- f"Exposure time scaled to { exposure_time :.3f} s, transmission scaled to { transmission :.3f} , scale factor now { scale :.3f} "
287- )
288298
289299 # Step 3: Store screeningStrategyWedge results, linked to the screeningStrategyId
290300 # Keep the screeningStrategyWedgeId
@@ -303,31 +313,100 @@ def generate_strategy(
303313 }
304314 ispyb_command_list .append (d )
305315
306- # Convert transmission to percentage for ISPyB
307- transmission_pct = transmission * 100
308-
309316 axis_end = (
310317 rotation_start + rotation_increment * recipe_step .number_of_images
311318 )
312319
313- # Step 4: Store second screeningStrategySubWedge results, linked to the screeningStrategyWedgeId
314- # Keep the screeningStrategyWedgeId
315- d = {
320+ # Partial command for the ScreeningSubWedge table is updated depending on what fields the beamline wants to be shown in Synchweb
321+ screening_sub_wedge_command = {
316322 "subwedgenumber" : 1 ,
317323 "rotationaxis" : recipe_step .scan_axis ,
318324 "axisstart" : rotation_start ,
319325 "axisend" : axis_end ,
320- "exposuretime" : exposure_time ,
321- "transmission" : transmission_pct ,
322326 "oscillationrange" : rotation_increment ,
323327 "noimages" : recipe_step .number_of_images ,
324328 "resolution" : resolution ,
325329 "ispyb_command" : "insert_screening_strategy_sub_wedge" ,
326330 "screening_strategy_wedge_id" : f"$ispyb_screening_strategy_wedge_id_{ n_step } " ,
327331 "store_result" : f"ispyb_screening_strategy_sub_wedge_id_{ n_step } " ,
328332 }
329- ispyb_command_list .append (d )
333+ transmission_limits = (
334+ max (
335+ get_beamline_param (
336+ beamline_config , ("gda.mx.udc.minTransmission" ,), 0.0
337+ ),
338+ agamemnon_recipe_limits .transmission [0 ],
339+ ),
340+ min (
341+ get_beamline_param (
342+ beamline_config , ("gda.mx.udc.maxTransmission" ,), 1.0
343+ ),
344+ recommended_max_transmission ,
345+ agamemnon_recipe_limits .transmission [1 ],
346+ ),
347+ )
348+ if beamline_wants_dose_displayed :
349+ screening_sub_wedge_command .update (
350+ dosetotal = dose , transmission = (transmission_limits [1 ] * 100 )
351+ )
352+ ispyb_command_list .append (screening_sub_wedge_command )
353+ continue
354+
355+ scale *= get_wavelength_scale (wavelength , default_wavelength )
356+ transmission = recipe_step .transmission
357+ exposure_time = recipe_step .exposure_time
358+
359+ exposure_time_limits = (
360+ max (
361+ get_beamline_param (
362+ beamline_config ,
363+ (
364+ "gda.mx.udc.minExposureTime" ,
365+ "gda.exptTableModel.minExposureTime" ,
366+ ),
367+ 0.002 ,
368+ ),
369+ agamemnon_recipe_limits .exposure_time [0 ],
370+ ),
371+ min (
372+ get_beamline_param (
373+ beamline_config ,
374+ (
375+ "gda.mx.udc.maxExposureTime" ,
376+ "gda.exptTableModel.maxExposureTime" ,
377+ ),
378+ float ("inf" ),
379+ ),
380+ agamemnon_recipe_limits .exposure_time [1 ],
381+ ),
382+ )
383+ # Runs twice to ensure that limits are applied correctly to both parameters, as they are interdependent - is this necessary?
384+ for _ in range (2 ):
385+ if scale > 1.0 :
386+ transmission , scale = scale_parameter (
387+ transmission , scale , limits = transmission_limits
388+ )
389+ exposure_time , scale = scale_parameter (
390+ exposure_time , scale , limits = exposure_time_limits
391+ )
392+ else :
393+ exposure_time , scale = scale_parameter (
394+ exposure_time , scale , limits = exposure_time_limits
395+ )
396+ transmission , scale = scale_parameter (
397+ transmission , scale , limits = transmission_limits
398+ )
399+ self .log .debug (
400+ f"Exposure time scaled to { exposure_time :.3f} s, transmission scaled to { transmission :.3f} , scale factor now { scale :.3f} "
401+ )
402+
403+ # Convert transmission to percentage for ISPyB
404+ relative_transmission_pct = transmission / dc_transmission * 100
330405
406+ screening_sub_wedge_command .update (
407+ exposuretime = exposure_time , transmission = relative_transmission_pct
408+ )
409+ ispyb_command_list .append (screening_sub_wedge_command )
331410 # Send results onwards
332411 rw .set_default_channel ("ispyb" )
333412 rw .send_to ("ispyb" , {"ispyb_command_list" : ispyb_command_list }, transaction = txn )
0 commit comments