From 100e5dcf7843f355f1e3e932f19b357c87fa97a4 Mon Sep 17 00:00:00 2001 From: Paul Naughton Date: Thu, 2 Apr 2026 14:12:28 +0100 Subject: [PATCH 1/3] xia2.overload wrapper --- setup.py | 1 + src/dlstbx/mimas/core.py | 7 ++ src/dlstbx/wrapper/xia2_overload.py | 133 ++++++++++++++++++++++++++++ 3 files changed, 141 insertions(+) create mode 100644 src/dlstbx/wrapper/xia2_overload.py diff --git a/setup.py b/setup.py index a4d26588e..f813428c9 100644 --- a/setup.py +++ b/setup.py @@ -93,6 +93,7 @@ "topaz3 = dlstbx.wrapper.topaz3_wrapper:Topaz3Wrapper", "xia2 = dlstbx.wrapper.xia2:Xia2Wrapper", "xia2.multiplex = dlstbx.wrapper.xia2_multiplex:Xia2MultiplexWrapper", + "xia2.overload = dlstbx.wrapper.xia2_overload:Xia2OverloadWrapper", "xia2.strategy = dlstbx.wrapper.xia2_strategy:Xia2StrategyWrapper", "xia2.to_shelxcde = dlstbx.wrapper.xia2_to_shelxcde:Xia2toShelxcdeWrapper", "xia2.ssx = dlstbx.wrapper.xia2_ssx:Xia2SsxWrapper", diff --git a/src/dlstbx/mimas/core.py b/src/dlstbx/mimas/core.py index 93e9648ad..387b4d317 100644 --- a/src/dlstbx/mimas/core.py +++ b/src/dlstbx/mimas/core.py @@ -200,6 +200,13 @@ def handle_characterization( source="automatic", displayname="align_crystal", ), + mimas.MimasISPyBJobInvocation( + DCID=scenario.DCID, + autostart=True, + recipe="strategy-xia2-overload", + source="automatic", + displayname="xia2-overload", + ), ] diff --git a/src/dlstbx/wrapper/xia2_overload.py b/src/dlstbx/wrapper/xia2_overload.py new file mode 100644 index 000000000..b51a04e66 --- /dev/null +++ b/src/dlstbx/wrapper/xia2_overload.py @@ -0,0 +1,133 @@ +from __future__ import annotations + +import json +import math +import subprocess + +import py + +from dlstbx.wrapper import Wrapper + + +class Xia2OverloadWrapper(Wrapper): + _logger_name = "dlstbx.wrap.xia2_overload" + + def send_to_ispyb(self, results): + ispyb_command_list = [] + + d = { + "program": "xia2.overload", + "ispyb_command": "insert_screening_output", + "screening_id": "$ispyb_screening_id", + "store_result": "ispyb_screening_output_id", + } + ispyb_command_list.append(d) + transmission = results["transmission"] + exposure_time = results["exposure_time"] + + d = { + "ispyb_command": "insert_screening_strategy", + "transmission": transmission, + "exposuretime": exposure_time, + "screening_output_id": "$ispyb_screening_output_id", + "store_result": "ispyb_screening_strategy_id", + } + ispyb_command_list.append(d) + + d = { + "ispyb_command": "update_processing_status", + "program_id": "$ispyb_autoprocprogram_id", + "message": "Processing successful", + "status": "success", + } + ispyb_command_list.append(d) + + self.recwrap.send_to("ispyb", {"ispyb_command_list": ispyb_command_list}) + self.log.info("Sent %d commands to ISPyB", len(ispyb_command_list)) + self.log.debug("Sending %s", json.dumps(ispyb_command_list, indent=2)) + + def run(self): + assert hasattr(self, "recwrap"), "No recipewrapper object found" + + params = self.recwrap.recipe_step["job_parameters"] + working_directory = py.path.local(params["working_directory"]) + results_directory = py.path.local(params["results_directory"]) + + target_saturation = float(params["target_saturation"]) + oscillation = float(params["oscillation"]) + transmission = float(params["transmission"]) + exposure_time = float(params["exposure_time"]) + + file = params["input_file"] + + command = [f"xia2.overload {file}"] + + result = subprocess.run( + command, shell=True, cwd=working_directory, capture_output=True + ) + + if result.returncode: + self.log.info(f"xia2.overload failed with return code {result.returncode}") + self.log.debug(f"Command output:\n{result.stdout}") + return False + + results_directory.ensure(dir=True) + output_file = "overload.json" + for file in working_directory.listdir(): + if file.basename != output_file: + continue + + destination = results_directory.join(file.basename) + self.log.debug(f"Copying {file.strpath} to {destination.strpath}") + file.copy(destination) + self.record_result_individual_file( + { + "file_path": destination.dirname, + "file_name": destination.basename, + "file_type": file.ext, + } + ) + + overload_file = working_directory.join(output_file) + with open(overload_file, "r") as f: + data = json.load(f) + counts = data["counts"] + overload_limit = float(data["overload_limit"]) + + max_count = float(list(counts)[-1]) + + mosaicity_corr = params.get("mosaicity_correction", False) + average_to_peak = ( + self.mosaicity_correction(mosaicity_corr, oscillation) + if mosaicity_corr + else 1 + ) + + saturation = (max_count / overload_limit) * average_to_peak + scale_factor = target_saturation / saturation + + scaled_transmission = transmission + scaled_exposure_time = exposure_time + if scale_factor < 1: + scaled_transmission *= scale_factor + scaled_exposure_time /= scale_factor + + results = { + "transmission": scaled_transmission, + "exposure_time": scaled_exposure_time, + } + + self.send_to_ispyb(results) + + self.log.info("Done.") + return True + + def mosaicity_correction(self, moscaicity_coefficent: float, oscillation: float): + delta_z = oscillation / (moscaicity_coefficent) * math.sqrt(2) + average_to_peak = ( + math.sqrt(math.pi) * delta_z * math.erf(delta_z) + + math.exp(-(delta_z * delta_z)) + - 1 + ) / (delta_z * delta_z) + self.log.info("Average-to-peak intensity ratio: %f", average_to_peak) + return average_to_peak From 41d238890c33d7a8b746288dc0cb306a99317ecf Mon Sep 17 00:00:00 2001 From: Paul Naughton Date: Wed, 8 Apr 2026 13:32:37 +0100 Subject: [PATCH 2/3] Removing exposure time logic and moved to pathlib --- src/dlstbx/wrapper/xia2_overload.py | 58 ++++++++++++++--------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/dlstbx/wrapper/xia2_overload.py b/src/dlstbx/wrapper/xia2_overload.py index b51a04e66..d8cceb842 100644 --- a/src/dlstbx/wrapper/xia2_overload.py +++ b/src/dlstbx/wrapper/xia2_overload.py @@ -4,7 +4,8 @@ import math import subprocess -import py +from pathlib import Path +import shutil from dlstbx.wrapper import Wrapper @@ -23,12 +24,10 @@ def send_to_ispyb(self, results): } ispyb_command_list.append(d) transmission = results["transmission"] - exposure_time = results["exposure_time"] d = { "ispyb_command": "insert_screening_strategy", "transmission": transmission, - "exposuretime": exposure_time, "screening_output_id": "$ispyb_screening_output_id", "store_result": "ispyb_screening_strategy_id", } @@ -50,13 +49,12 @@ def run(self): assert hasattr(self, "recwrap"), "No recipewrapper object found" params = self.recwrap.recipe_step["job_parameters"] - working_directory = py.path.local(params["working_directory"]) - results_directory = py.path.local(params["results_directory"]) + working_directory = Path(params["working_directory"]) + results_directory = Path(params["results_directory"]) - target_saturation = float(params["target_saturation"]) + target_countrate_pct = float(params["target_countrate_pct"]) oscillation = float(params["oscillation"]) transmission = float(params["transmission"]) - exposure_time = float(params["exposure_time"]) file = params["input_file"] @@ -68,28 +66,31 @@ def run(self): if result.returncode: self.log.info(f"xia2.overload failed with return code {result.returncode}") + self.log.info(result.stderr) self.log.debug(f"Command output:\n{result.stdout}") return False - results_directory.ensure(dir=True) - output_file = "overload.json" - for file in working_directory.listdir(): - if file.basename != output_file: - continue - - destination = results_directory.join(file.basename) - self.log.debug(f"Copying {file.strpath} to {destination.strpath}") - file.copy(destination) - self.record_result_individual_file( - { - "file_path": destination.dirname, - "file_name": destination.basename, - "file_type": file.ext, - } - ) - - overload_file = working_directory.join(output_file) - with open(overload_file, "r") as f: + results_directory.mkdir(parents=True, exist_ok=True) + output_file_name = "overload.json" + + source_file = working_directory / output_file_name + destination = results_directory / output_file_name + + if not source_file.exists(): + return False + + self.log.debug(f"Copying {str(source_file)} to {str(destination)}") + shutil.copy2(source_file, destination) + + self.record_result_individual_file( + { + "file_path": str(destination.parent), + "file_name": destination.name, + "file_type": "result", + } + ) + + with source_file.open("r") as f: data = json.load(f) counts = data["counts"] overload_limit = float(data["overload_limit"]) @@ -104,17 +105,14 @@ def run(self): ) saturation = (max_count / overload_limit) * average_to_peak - scale_factor = target_saturation / saturation + scale_factor = target_countrate_pct / saturation scaled_transmission = transmission - scaled_exposure_time = exposure_time if scale_factor < 1: scaled_transmission *= scale_factor - scaled_exposure_time /= scale_factor results = { "transmission": scaled_transmission, - "exposure_time": scaled_exposure_time, } self.send_to_ispyb(results) From 856f6294820ac47deb3b7aae9fe0e171cf88fe59 Mon Sep 17 00:00:00 2001 From: Paul Naughton Date: Thu, 9 Apr 2026 15:28:31 +0100 Subject: [PATCH 3/3] changing result to only transmission --- src/dlstbx/wrapper/xia2_overload.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/dlstbx/wrapper/xia2_overload.py b/src/dlstbx/wrapper/xia2_overload.py index d8cceb842..a05512551 100644 --- a/src/dlstbx/wrapper/xia2_overload.py +++ b/src/dlstbx/wrapper/xia2_overload.py @@ -13,7 +13,7 @@ class Xia2OverloadWrapper(Wrapper): _logger_name = "dlstbx.wrap.xia2_overload" - def send_to_ispyb(self, results): + def send_to_ispyb(self, transmission): ispyb_command_list = [] d = { @@ -23,7 +23,6 @@ def send_to_ispyb(self, results): "store_result": "ispyb_screening_output_id", } ispyb_command_list.append(d) - transmission = results["transmission"] d = { "ispyb_command": "insert_screening_strategy", @@ -32,7 +31,7 @@ def send_to_ispyb(self, results): "store_result": "ispyb_screening_strategy_id", } ispyb_command_list.append(d) - + d = { "ispyb_command": "update_processing_status", "program_id": "$ispyb_autoprocprogram_id", @@ -111,11 +110,7 @@ def run(self): if scale_factor < 1: scaled_transmission *= scale_factor - results = { - "transmission": scaled_transmission, - } - - self.send_to_ispyb(results) + self.send_to_ispyb(scaled_transmission) self.log.info("Done.") return True