Skip to content

Commit dc93124

Browse files
pjnaughtonpblowey
andauthored
Add estimate transmission to udc strategy pipeline (#367)
Estimate transmission wrapper was added in #366. This PR updates the strategy service to take the suggested maximum transmission from the estimate transmission wrapper and apply it to the UDC strategy. Some other fixes/features added to the UDC strategy pipeline are also applied to the strategy service in this PR: * Include limits from recipe_config.yaml, which are limits that beamline staff can set for certain recipes * Only apply resolution based scale to dose (wavelength scale was erroneously being applied to dose). * Populate the database differently for beamlines that work with dose: * For beamlines that work with dose, populate dosetotal and transmission columns but not exposure time column in ScreeningStrategySubWedge table. Transmission column is populated with recommended maximum transmission (from estimate transmission). * For beamlines that don't work with dose, populate transmission with scaled value along with scaled exposure time but do not populate dosetotal column in ScreeningStrategySubWedge table. This PR also adds a check in the trigger service to ensure that the strategy pipeline is only triggered once for each dcid, running on whichever upstream pipeline finishes first. The trigger function is also updated to only allow the pipeline to run on I04 for initial rollout of the pipeline. Also removes the now obsolete xia2-overload wrapper. --------- Co-authored-by: Phil Blowey <philip.blowey@diamond.ac.uk>
1 parent b6601e8 commit dc93124

3 files changed

Lines changed: 154 additions & 150 deletions

File tree

src/dlstbx/services/strategy.py

Lines changed: 136 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
from dlstbx.util import ChainMapWithReplacement
1111

12+
type Limits = tuple[float, float]
13+
1214

1315
def 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+
7583
def 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+
81117
def 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)

src/dlstbx/services/trigger.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2905,12 +2905,29 @@ def trigger_strategy(
29052905
)
29062906
return {"success": True}
29072907

2908-
if parameters.beamline not in ["i03", "i04", "i04-1"]:
2908+
if parameters.beamline not in ["i04"]:
29092909
self.log.info(
29102910
f"Skipping strategy trigger: beamline {parameters.beamline} not supported"
29112911
)
29122912
return {"success": True}
29132913

2914+
udc_strategy_previously_triggered = (
2915+
session.query(AutoProcProgram.processingPrograms)
2916+
.join(
2917+
ProcessingJob,
2918+
AutoProcProgram.processingJobId == ProcessingJob.processingJobId,
2919+
)
2920+
.filter(ProcessingJob.dataCollectionId == parameters.dcid)
2921+
.filter(AutoProcProgram.processingPrograms == "UDC strategy")
2922+
.all()
2923+
)
2924+
2925+
if udc_strategy_previously_triggered:
2926+
self.log.info(
2927+
f"Skipping strategy trigger: UDC Strategy has already been triggered for dcid={parameters.dcid}."
2928+
)
2929+
return {"success": True}
2930+
29142931
# Get resolution estimate from ispyb records for upstream pipeline - returns None if not found.
29152932
resolution = (
29162933
session.query(AutoProcScalingStatistics.resolutionLimitHigh)

0 commit comments

Comments
 (0)