diff --git a/deploy/deploy_hyperion.py b/deploy/deploy_hyperion.py index 1e75f70df..ffda7c5ed 100644 --- a/deploy/deploy_hyperion.py +++ b/deploy/deploy_hyperion.py @@ -5,7 +5,7 @@ from git import Repo from packaging.version import Version -recognised_beamlines = ["dev", "i03", "i04"] +recognised_beamlines = ["dev", "i03", "i02-1", "i04"] class repo: @@ -32,7 +32,7 @@ def deploy(self, url): deploy_repo.git.checkout(self.latest_version_str) print("Setting permissions") - groups_to_give_permission = ["i03_staff", "gda2", "dls_dasc"] + groups_to_give_permission = ["i02-1_staff", "gda2", "dls_dasc"] setfacl_params = ",".join( [f"g:{group}:rwx" for group in groups_to_give_permission] ) @@ -67,6 +67,10 @@ def get_hyperion_release_dir_from_args(repo: repo) -> str: if args.beamline == "dev": print("Running as dev") return "/tmp/hyperion_release_test/bluesky" + elif args.beamline == "i03": + return f"/dls_sw/{args.beamline}/software/bluesky" + elif args.beamline == "i02-1": + return f"/dls_sw/{args.beamline}/software/bluesky" else: return f"/dls_sw/{args.beamline}/software/bluesky" diff --git a/run_hyperion.sh b/run_hyperion.sh index 46f9944dd..879315c81 100755 --- a/run_hyperion.sh +++ b/run_hyperion.sh @@ -92,7 +92,7 @@ if [[ $START == 1 ]]; then exit 1 fi - ISPYB_CONFIG_PATH="/dls_sw/dasc/mariadb/credentials/ispyb-artemis-${BEAMLINE}.cfg" + ISPYB_CONFIG_PATH="/dls_sw/dasc/mariadb/credentials/ispyb-artemis-i03.cfg" export ISPYB_CONFIG_PATH fi diff --git a/setup.cfg b/setup.cfg index 1c87b8622..041c41cca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -36,7 +36,7 @@ install_requires = xarray doct databroker - dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@b58451b5902db75b9d7cf1c40740bdeac3e53348 + dls-dodal @ git+https://github.com/DiamondLightSource/dodal.git@39084723b73adec1df9199b9fe601d68242eeb3d pydantic<2.0 # See https://github.com/DiamondLightSource/hyperion/issues/774 scipy diff --git a/src/hyperion/experiment_plans/__init__.py b/src/hyperion/experiment_plans/__init__.py index a2e9f07fe..1008726b9 100644 --- a/src/hyperion/experiment_plans/__init__.py +++ b/src/hyperion/experiment_plans/__init__.py @@ -10,10 +10,14 @@ pin_tip_centre_then_xray_centre, ) from hyperion.experiment_plans.rotation_scan_plan import rotation_scan +from hyperion.experiment_plans.vmxm_flyscan_xray_centre_plan import ( + vmxm_flyscan_xray_centre, +) __all__ = [ "flyscan_xray_centre", "grid_detect_then_xray_centre", "rotation_scan", "pin_tip_centre_then_xray_centre", + "vmxm_flyscan_xray_centre", ] diff --git a/src/hyperion/experiment_plans/experiment_registry.py b/src/hyperion/experiment_plans/experiment_registry.py index c0fb7e543..ecb7ac986 100644 --- a/src/hyperion/experiment_plans/experiment_registry.py +++ b/src/hyperion/experiment_plans/experiment_registry.py @@ -4,12 +4,13 @@ from dodal.devices.fast_grid_scan import GridScanParams -import hyperion.experiment_plans.flyscan_xray_centre_plan as flyscan_xray_centre_plan import hyperion.experiment_plans.rotation_scan_plan as rotation_scan_plan from hyperion.experiment_plans import ( + flyscan_xray_centre_plan, grid_detect_then_xray_centre_plan, pin_centre_then_xray_centre_plan, stepped_grid_scan_plan, + vmxm_flyscan_xray_centre_plan, ) from hyperion.external_interaction.callbacks.abstract_plan_callback_collection import ( NullPlanCallbackCollection, @@ -18,6 +19,7 @@ RotationCallbackCollection, ) from hyperion.external_interaction.callbacks.xray_centre.callback_collection import ( + VmxmFastGridScanCallbackCollection, XrayCentreCallbackCollection, ) from hyperion.parameters.plan_specific.grid_scan_with_edge_detect_params import ( @@ -51,36 +53,47 @@ def do_nothing(): EXPERIMENT_TYPES = Union[GridScanParams, RotationScanParams, SteppedGridScanParams] PLAN_REGISTRY: dict[str, dict[str, Callable]] = { - "flyscan_xray_centre": { - "setup": flyscan_xray_centre_plan.create_devices, + # FIXME - HACK - plans commented out here because VMXm complains about no + # aperture_scatterguard which is needed for some plans. + # Need a way to either mark plans as being relevant only on some beamlines, or + # generally allow hyperion to *start* even if some plans reference nonexistent + # devices (and then fail at runtime if that plan is actually used). + # "flyscan_xray_centre": { + # "setup": flyscan_xray_centre_plan.create_devices, + # "internal_param_type": GridscanInternalParameters, + # "experiment_param_type": GridScanParams, + # "callback_collection_type": XrayCentreCallbackCollection, + # }, + "vmxm_flyscan_xray_centre": { + "setup": vmxm_flyscan_xray_centre_plan.create_devices, "internal_param_type": GridscanInternalParameters, "experiment_param_type": GridScanParams, - "callback_collection_type": XrayCentreCallbackCollection, - }, - "grid_detect_then_xray_centre": { - "setup": grid_detect_then_xray_centre_plan.create_devices, - "internal_param_type": GridScanWithEdgeDetectInternalParameters, - "experiment_param_type": GridScanWithEdgeDetectParams, - "callback_collection_type": NullPlanCallbackCollection, - }, - "rotation_scan": { - "setup": rotation_scan_plan.create_devices, - "internal_param_type": RotationInternalParameters, - "experiment_param_type": RotationScanParams, - "callback_collection_type": RotationCallbackCollection, - }, - "pin_tip_centre_then_xray_centre": { - "setup": pin_centre_then_xray_centre_plan.create_devices, - "internal_param_type": PinCentreThenXrayCentreInternalParameters, - "experiment_param_type": PinCentreThenXrayCentreParams, - "callback_collection_type": NullPlanCallbackCollection, - }, - "stepped_grid_scan": { - "setup": stepped_grid_scan_plan.create_devices, - "internal_param_type": SteppedGridScanInternalParameters, - "experiment_param_type": SteppedGridScanParams, - "callback_collection_type": NullPlanCallbackCollection, + "callback_collection_type": VmxmFastGridScanCallbackCollection, }, + # "grid_detect_then_xray_centre": { + # "setup": grid_detect_then_xray_centre_plan.create_devices, + # "internal_param_type": GridScanWithEdgeDetectInternalParameters, + # "experiment_param_type": GridScanWithEdgeDetectParams, + # "callback_collection_type": NullPlanCallbackCollection, + # }, + # "rotation_scan": { + # "setup": rotation_scan_plan.create_devices, + # "internal_param_type": RotationInternalParameters, + # "experiment_param_type": RotationScanParams, + # "callback_collection_type": RotationCallbackCollection, + # }, + # "pin_tip_centre_then_xray_centre": { + # "setup": pin_centre_then_xray_centre_plan.create_devices, + # "internal_param_type": PinCentreThenXrayCentreInternalParameters, + # "experiment_param_type": PinCentreThenXrayCentreParams, + # "callback_collection_type": NullPlanCallbackCollection, + # }, + # "stepped_grid_scan": { + # "setup": stepped_grid_scan_plan.create_devices, + # "internal_param_type": SteppedGridScanInternalParameters, + # "experiment_param_type": SteppedGridScanParams, + # "callback_collection_type": NullPlanCallbackCollection, + # }, } EXPERIMENT_NAMES = list(PLAN_REGISTRY.keys()) EXPERIMENT_TYPE_LIST = [p["experiment_param_type"] for p in PLAN_REGISTRY.values()] diff --git a/src/hyperion/experiment_plans/tests/test_grid_detection_plan.py b/src/hyperion/experiment_plans/tests/test_grid_detection_plan.py index c7d5fc73f..f95d6ca77 100644 --- a/src/hyperion/experiment_plans/tests/test_grid_detection_plan.py +++ b/src/hyperion/experiment_plans/tests/test_grid_detection_plan.py @@ -4,7 +4,7 @@ from bluesky.run_engine import RunEngine from dodal.beamlines import i03 from dodal.devices.backlight import Backlight -from dodal.devices.fast_grid_scan import GridAxis +from dodal.devices.fast_grid_scan_common import GridAxis from dodal.devices.oav.oav_detector import OAV from dodal.devices.oav.oav_parameters import OAVParameters from dodal.devices.smargon import Smargon diff --git a/src/hyperion/experiment_plans/vmxm_flyscan_xray_centre_plan.py b/src/hyperion/experiment_plans/vmxm_flyscan_xray_centre_plan.py new file mode 100755 index 000000000..8d33e0335 --- /dev/null +++ b/src/hyperion/experiment_plans/vmxm_flyscan_xray_centre_plan.py @@ -0,0 +1,225 @@ +from __future__ import annotations + +import argparse +import dataclasses +from typing import TYPE_CHECKING, Any + +import bluesky.plan_stubs as bps +import bluesky.preprocessors as bpp +from blueapi.core import BlueskyContext, MsgGenerator +from bluesky.run_engine import RunEngine +from bluesky.utils import ProgressBarManager +from dodal.devices.backlight import VmxmBacklight +from dodal.devices.eiger import EigerDetector +from dodal.devices.fast_grid_scan_2d import FastGridScan2D +from dodal.devices.fast_grid_scan_2d import ( + set_fast_grid_scan_params as set_flyscan_params, +) +from dodal.devices.synchrotron import Synchrotron +from dodal.devices.vmxm.vmxm_attenuator import VmxmAttenuator +from dodal.devices.vmxm.vmxm_sample_motors import VmxmSampleMotors +from dodal.devices.zebra import ( + Zebra, +) + +import hyperion.log +from hyperion.exceptions import WarningException +from hyperion.external_interaction.callbacks.xray_centre.callback_collection import ( + VmxmFastGridScanCallbackCollection, +) +from hyperion.parameters import external_parameters +from hyperion.parameters.constants import ISPYB_HARDWARE_READ_PLAN, SIM_BEAMLINE +from hyperion.tracing import TRACER +from hyperion.utils.context import device_composite_from_context, setup_context + +if TYPE_CHECKING: + from hyperion.parameters.plan_specific.gridscan_internal_params import ( + GridscanInternalParameters, + ) + + +@dataclasses.dataclass +class VmxmFlyScanXRayCentreComposite: + """All devices which are directly or indirectly required by this plan""" + + attenuator: VmxmAttenuator + backlight: VmxmBacklight + eiger: EigerDetector + fast_grid_scan: FastGridScan2D + sample_motors: VmxmSampleMotors + synchrotron: Synchrotron + zebra: Zebra + + +def create_devices(context: BlueskyContext) -> VmxmFlyScanXRayCentreComposite: + """Creates the devices required for the plan and connect to them""" + return device_composite_from_context(context, VmxmFlyScanXRayCentreComposite) + + +def wait_for_gridscan_valid(fgs_motors: FastGridScan2D, timeout=0.5): + hyperion.log.LOGGER.info("Waiting for valid fgs_params") + SLEEP_PER_CHECK = 0.1 + times_to_check = int(timeout / SLEEP_PER_CHECK) + for _ in range(times_to_check): + scan_invalid = yield from bps.rd(fgs_motors.scan_invalid) + pos_counter = yield from bps.rd(fgs_motors.position_counter) + hyperion.log.LOGGER.debug( + f"Scan invalid: {scan_invalid} and position counter: {pos_counter}" + ) + if not scan_invalid and pos_counter == 0: + hyperion.log.LOGGER.info("Gridscan scan valid and position counter reset") + return + yield from bps.sleep(SLEEP_PER_CHECK) + raise WarningException("Scan invalid") + + +def tidy_up_plans(fgs_composite: VmxmFlyScanXRayCentreComposite): + hyperion.log.LOGGER.info("Tidying up Zebra") + yield from tidyup_vmxm_zebra_after_gridscan(fgs_composite.zebra) + + +@bpp.set_run_key_decorator("run_gridscan") +@bpp.run_decorator(md={"subplan_name": "run_gridscan"}) +def run_gridscan( + fgs_composite: VmxmFlyScanXRayCentreComposite, + parameters: GridscanInternalParameters, + md={ + "plan_name": "run_gridscan", + }, +): + fgs_motors = fgs_composite.fast_grid_scan + + yield from bps.mv( + fgs_composite.attenuator, + parameters.hyperion_params.ispyb_params.transmission_fraction, + ) + + yield from bps.create(name=ISPYB_HARDWARE_READ_PLAN) + yield from bps.read(fgs_composite.synchrotron.machine_status.synchrotron_mode) + yield from bps.save() + + # TODO: Check topup gate + yield from set_flyscan_params(fgs_motors, parameters.experiment_params) + yield from bps.mv(fgs_composite.sample_motors.omega, parameters.get_omega_start(1)) + yield from wait_for_gridscan_valid(fgs_motors) + + @bpp.set_run_key_decorator("do_fgs") + @bpp.run_decorator(md={"subplan_name": "do_fgs"}) + @bpp.contingency_decorator( + except_plan=lambda e: (yield from bps.stop(fgs_composite.eiger)), + else_plan=lambda: (yield from bps.unstage(fgs_composite.eiger)), + ) + def do_fgs(): + yield from bps.wait() # Wait for all moves to complete + hyperion.log.LOGGER.info("Kicking off") + yield from bps.kickoff(fgs_motors) + + yield from bps.complete(fgs_motors, wait=True) + + hyperion.log.LOGGER.info("Waiting for arming to finish") + yield from bps.wait("ready_for_data_collection") + yield from bps.stage(fgs_composite.eiger) + + with TRACER.start_span("do_fgs"): + yield from do_fgs() + + +def setup_vmxm_zebra_for_gridscan( + zebra: Zebra, group="setup_zebra_for_gridscan", wait=False +): + # note: VMXm-specific + vmxm_zebra_input = 4 + vmxm_zebra_output = 1 + yield from bps.abs_set( + zebra.output.out_pvs[vmxm_zebra_input], vmxm_zebra_output, group=group + ) + + if wait: + yield from bps.wait(group) + + +def tidyup_vmxm_zebra_after_gridscan( + zebra: Zebra, group="tidyup_vmxm_zebra_after_gridscan", wait=False +): # note: VMXm-specific + vmxm_zebra_input = 4 + vmxm_zebra_output = 36 + yield from bps.abs_set( + zebra.output.out_pvs[vmxm_zebra_input], vmxm_zebra_output, group=group + ) + + if wait: + yield from bps.wait(group) + + +@bpp.set_run_key_decorator("run_gridscan_and_move") +@bpp.run_decorator(md={"subplan_name": "run_gridscan_and_move"}) +def run_gridscan_and_move( + fgs_composite: VmxmFlyScanXRayCentreComposite, + parameters: GridscanInternalParameters, + subscriptions: VmxmFastGridScanCallbackCollection, +): + yield from setup_vmxm_zebra_for_gridscan(fgs_composite.zebra) + + hyperion.log.LOGGER.info("Starting grid scan") + + yield from run_gridscan(fgs_composite, parameters) + + +def vmxm_flyscan_xray_centre( + composite: VmxmFlyScanXRayCentreComposite, + parameters: Any, +) -> MsgGenerator: + """Create the plan to run the grid scan based on provided parameters. + + The ispyb handler should be added to the whole gridscan as we want to capture errors + at any point in it. + + Args: + parameters (FGSInternalParameters): The parameters to run the scan. + + Returns: + Generator: The plan for the gridscan + """ + composite.eiger.set_detector_parameters(parameters.hyperion_params.detector_params) + + subscriptions = VmxmFastGridScanCallbackCollection.from_params(parameters) + + @bpp.subs_decorator( # subscribe the RE to nexus, ispyb callbacks + list(subscriptions) # must be the outermost decorator to receive the metadata + ) + @bpp.set_run_key_decorator("run_gridscan_move_and_tidy") + @bpp.run_decorator( # attach experiment metadata to the start document + md={ + "subplan_name": "run_gridscan_move_and_tidy", + "hyperion_internal_parameters": parameters.json(), + } + ) + @bpp.finalize_decorator(lambda: tidy_up_plans(composite)) + def run_gridscan_and_move_and_tidy(fgs_composite, params, comms): + yield from run_gridscan_and_move(fgs_composite, params, comms) + + return run_gridscan_and_move_and_tidy(composite, parameters, subscriptions) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--beamline", + help="The beamline prefix this is being run on", + default=SIM_BEAMLINE, + ) + args = parser.parse_args() + + RE = RunEngine({}) + RE.waiting_hook = ProgressBarManager() + from hyperion.parameters.plan_specific.gridscan_internal_params import ( + GridscanInternalParameters, + ) + + parameters = GridscanInternalParameters(**external_parameters.from_file()) + subscriptions = VmxmFastGridScanCallbackCollection.from_params(parameters) + + context = setup_context(wait_for_connection=True) + composite = create_devices(context) + + RE(vmxm_flyscan_xray_centre(composite, parameters)) diff --git a/src/hyperion/external_interaction/callbacks/ispyb_callback_base.py b/src/hyperion/external_interaction/callbacks/ispyb_callback_base.py index 753239531..134247f75 100644 --- a/src/hyperion/external_interaction/callbacks/ispyb_callback_base.py +++ b/src/hyperion/external_interaction/callbacks/ispyb_callback_base.py @@ -42,6 +42,7 @@ def descriptor(self, doc: dict): self.descriptors[doc["uid"]] = doc def start(self, doc: dict): + LOGGER.info(f"ISPyB handler received start document {doc}.") if self.uid_to_finalize_on is None: self.uid_to_finalize_on = doc.get("uid") @@ -49,37 +50,37 @@ def event(self, doc: dict): """Subclasses should extend this to add a call to set_dcig_tag from hyperion.log""" - LOGGER.debug("ISPyB handler received event document.") + LOGGER.debug(f"ISPyB handler received event document {doc}.") assert isinstance( self.ispyb, StoreInIspyb ), "ISPyB deposition can't be initialised!" event_descriptor = self.descriptors[doc["descriptor"]] if event_descriptor.get("name") == ISPYB_HARDWARE_READ_PLAN: - self.params.hyperion_params.ispyb_params.undulator_gap = doc["data"][ - "undulator_gap" - ] - self.params.hyperion_params.ispyb_params.synchrotron_mode = doc["data"][ - "synchrotron_machine_status_synchrotron_mode" - ] - self.params.hyperion_params.ispyb_params.slit_gap_size_x = doc["data"][ - "s4_slit_gaps_xgap" - ] - self.params.hyperion_params.ispyb_params.slit_gap_size_y = doc["data"][ - "s4_slit_gaps_ygap" - ] + self.params.hyperion_params.ispyb_params.undulator_gap = doc["data"].get( + "undulator_gap", 0.0 + ) + self.params.hyperion_params.ispyb_params.synchrotron_mode = doc["data"].get( + "synchrotron_machine_status_synchrotron_mode", None + ) + self.params.hyperion_params.ispyb_params.slit_gap_size_x = doc["data"].get( + "s4_slit_gaps_xgap", 0.0 + ) + self.params.hyperion_params.ispyb_params.slit_gap_size_y = doc["data"].get( + "s4_slit_gaps_ygap", 0.0 + ) if event_descriptor.get("name") == ISPYB_TRANSMISSION_FLUX_READ_PLAN: self.params.hyperion_params.ispyb_params.transmission_fraction = doc[ "data" - ]["attenuator_actual_transmission"] - self.params.hyperion_params.ispyb_params.flux = doc["data"][ - "flux_flux_reading" - ] + ].get("attenuator_actual_transmission", 0.0) + self.params.hyperion_params.ispyb_params.flux = doc["data"].get( + "flux_flux_reading", 0.0 + ) - LOGGER.info("Creating ispyb entry.") - self.ispyb_ids = self.ispyb.begin_deposition() - LOGGER.info(f"Recieved ISPYB IDs: {self.ispyb_ids}") + LOGGER.info("Creating ispyb entry.") + self.ispyb_ids = self.ispyb.begin_deposition() + LOGGER.info(f"Recieved ISPYB IDs: {self.ispyb_ids}") def stop(self, doc: dict): """Subclasses must check that they are recieving a stop document for the correct diff --git a/src/hyperion/external_interaction/callbacks/rotation/callback_collection.py b/src/hyperion/external_interaction/callbacks/rotation/callback_collection.py index d23be80de..1a8b3a713 100644 --- a/src/hyperion/external_interaction/callbacks/rotation/callback_collection.py +++ b/src/hyperion/external_interaction/callbacks/rotation/callback_collection.py @@ -15,6 +15,7 @@ from hyperion.external_interaction.callbacks.rotation.zocalo_callback import ( RotationZocaloCallback, ) +from hyperion.external_interaction.nexus.nexus_utils import create_i03_goniometer_axes if TYPE_CHECKING: from hyperion.parameters.plan_specific.rotation_scan_internal_params import ( @@ -25,7 +26,9 @@ @dataclass(frozen=True, order=True) class RotationCallbackCollection(AbstractPlanCallbackCollection): """Groups the callbacks for external interactions for a rotation scan. - Cast to a list to pass it to Bluesky.preprocessors.subs_decorator().""" + Cast to a list to pass it to Bluesky.preprocessors.subs_decorator(). + + Note: specific to i03.""" nexus_handler: RotationNexusFileCallback ispyb_handler: RotationISPyBCallback @@ -33,7 +36,9 @@ class RotationCallbackCollection(AbstractPlanCallbackCollection): @classmethod def from_params(cls, parameters: RotationInternalParameters): - nexus_handler = RotationNexusFileCallback() + nexus_handler = RotationNexusFileCallback( + create_goniometer_axes=create_i03_goniometer_axes + ) ispyb_handler = RotationISPyBCallback(parameters) zocalo_handler = RotationZocaloCallback( parameters.hyperion_params.zocalo_environment, ispyb_handler diff --git a/src/hyperion/external_interaction/callbacks/rotation/nexus_callback.py b/src/hyperion/external_interaction/callbacks/rotation/nexus_callback.py index 3135da361..d3858c610 100644 --- a/src/hyperion/external_interaction/callbacks/rotation/nexus_callback.py +++ b/src/hyperion/external_interaction/callbacks/rotation/nexus_callback.py @@ -2,7 +2,10 @@ from bluesky.callbacks import CallbackBase -from hyperion.external_interaction.nexus.write_nexus import NexusWriter +from hyperion.external_interaction.nexus.write_nexus import ( + CreateGoniometerProtocol, + NexusWriter, +) from hyperion.log import LOGGER from hyperion.parameters.plan_specific.rotation_scan_internal_params import ( RotationInternalParameters, @@ -24,10 +27,11 @@ class RotationNexusFileCallback(CallbackBase): Usually used as part of a RotationCallbackCollection. """ - def __init__(self): + def __init__(self, create_goniometer_axes: CreateGoniometerProtocol): self.run_uid: str | None = None self.parameters: RotationInternalParameters | None = None self.writer: NexusWriter | None = None + self.create_goniometer_axes = create_goniometer_axes def start(self, doc: dict): if doc.get("subplan_name") == "rotation_scan_with_cleanup": @@ -42,5 +46,6 @@ def start(self, doc: dict): self.parameters, self.parameters.get_scan_points(), self.parameters.get_data_shape(), + create_goniometer_func=self.create_goniometer_axes, ) self.writer.create_nexus_file() diff --git a/src/hyperion/external_interaction/callbacks/xray_centre/callback_collection.py b/src/hyperion/external_interaction/callbacks/xray_centre/callback_collection.py index 65b1eb4b1..a92eb38eb 100644 --- a/src/hyperion/external_interaction/callbacks/xray_centre/callback_collection.py +++ b/src/hyperion/external_interaction/callbacks/xray_centre/callback_collection.py @@ -10,11 +10,16 @@ GridscanISPyBCallback, ) from hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( + Gridscan2DNexusFileCallback, GridscanNexusFileCallback, ) from hyperion.external_interaction.callbacks.xray_centre.zocalo_callback import ( XrayCentreZocaloCallback, ) +from hyperion.external_interaction.nexus.nexus_utils import ( + create_i03_goniometer_axes, + create_vmxm_goniometer_axes, +) if TYPE_CHECKING: from hyperion.parameters.internal_parameters import InternalParameters @@ -24,7 +29,9 @@ class XrayCentreCallbackCollection(AbstractPlanCallbackCollection): """Groups the callbacks for external interactions in the fast grid scan, and connects the Zocalo and ISPyB handlers. Cast to a list to pass it to - Bluesky.preprocessors.subs_decorator().""" + Bluesky.preprocessors.subs_decorator(). + + Note: currently specific to i03 as it creates i03's goniometer axes.""" nexus_handler: GridscanNexusFileCallback ispyb_handler: GridscanISPyBCallback @@ -32,7 +39,7 @@ class XrayCentreCallbackCollection(AbstractPlanCallbackCollection): @classmethod def from_params(cls, parameters: InternalParameters): - nexus_handler = GridscanNexusFileCallback() + nexus_handler = GridscanNexusFileCallback(create_i03_goniometer_axes) ispyb_handler = GridscanISPyBCallback(parameters) zocalo_handler = XrayCentreZocaloCallback(parameters, ispyb_handler) callback_collection = cls( @@ -41,3 +48,23 @@ def from_params(cls, parameters: InternalParameters): zocalo_handler=zocalo_handler, ) return callback_collection + + +@dataclass(frozen=True, order=True) +class VmxmFastGridScanCallbackCollection(AbstractPlanCallbackCollection): + """Like XRayCentreCallbackCollection, but without zocalo. + + Note: currently specific to VMXm as it creates VMXm's goniometer axes.""" + + nexus_handler: Gridscan2DNexusFileCallback + ispyb_handler: GridscanISPyBCallback + + @classmethod + def from_params(cls, parameters: InternalParameters): + nexus_handler = Gridscan2DNexusFileCallback(create_vmxm_goniometer_axes) + ispyb_handler = GridscanISPyBCallback(parameters) + callback_collection = cls( + nexus_handler=nexus_handler, + ispyb_handler=ispyb_handler, + ) + return callback_collection diff --git a/src/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py b/src/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py index d68ee31e9..8d49c97a1 100644 --- a/src/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py +++ b/src/hyperion/external_interaction/callbacks/xray_centre/nexus_callback.py @@ -2,7 +2,10 @@ from bluesky.callbacks import CallbackBase -from hyperion.external_interaction.nexus.write_nexus import NexusWriter +from hyperion.external_interaction.nexus.write_nexus import ( + CreateGoniometerProtocol, + NexusWriter, +) from hyperion.log import LOGGER from hyperion.parameters.constants import ISPYB_HARDWARE_READ_PLAN from hyperion.parameters.plan_specific.gridscan_internal_params import ( @@ -29,11 +32,12 @@ class GridscanNexusFileCallback(CallbackBase): Usually used as part of an FGSCallbackCollection. """ - def __init__(self) -> None: + def __init__(self, create_goniometer_func: CreateGoniometerProtocol) -> None: self.parameters: GridscanInternalParameters | None = None self.run_start_uid: str | None = None self.nexus_writer_1: NexusWriter | None = None self.nexus_writer_2: NexusWriter | None = None + self.create_goniometer_func = create_goniometer_func def start(self, doc: dict): if doc.get("subplan_name") == "run_gridscan_move_and_tidy": @@ -55,12 +59,56 @@ def descriptor(self, doc): LOGGER.info("Initialising nexus writers") nexus_data_1 = self.parameters.get_nexus_info(1) + LOGGER.info(f"Nexus data 1: {nexus_data_1}") nexus_data_2 = self.parameters.get_nexus_info(2) - self.nexus_writer_1 = NexusWriter(self.parameters, **nexus_data_1) + self.nexus_writer_1 = NexusWriter( + self.parameters, + create_goniometer_func=self.create_goniometer_func, + **nexus_data_1, + ) self.nexus_writer_2 = NexusWriter( self.parameters, + create_goniometer_func=self.create_goniometer_func, **nexus_data_2, vds_start_index=nexus_data_1["data_shape"][0], ) self.nexus_writer_1.create_nexus_file() self.nexus_writer_2.create_nexus_file() + + +class Gridscan2DNexusFileCallback(CallbackBase): + """Similar to above, but for a 2D gridscan""" + + def __init__(self, create_goniometer_func: CreateGoniometerProtocol) -> None: + self.parameters: GridscanInternalParameters | None = None + self.run_start_uid: str | None = None + self.nexus_writer: NexusWriter | None = None + self.create_goniometer_func = create_goniometer_func + + def start(self, doc: dict): + if doc.get("subplan_name") == "run_gridscan_move_and_tidy": + LOGGER.info( + "Nexus writer recieved start document with experiment parameters." + ) + json_params = doc.get("hyperion_internal_parameters") + self.parameters = GridscanInternalParameters.from_json(json_params) + self.run_start_uid = doc.get("uid") + + def descriptor(self, doc): + if doc.get("name") == ISPYB_HARDWARE_READ_PLAN: + assert ( + self.parameters is not None + ), "Nexus callback did not receive parameters before being asked to write!" + # TODO instead of ispyb wait for detector parameter reading in plan + # https://github.com/DiamondLightSource/python-hyperion/issues/629 + # and update parameters before creating writers + + LOGGER.info("Initialising nexus writer") + nexus_data = self.parameters.get_nexus_info(1) + LOGGER.info(f"Nexus data: {nexus_data}") + self.nexus_writer = NexusWriter( + self.parameters, + create_goniometer_func=self.create_goniometer_func, + **nexus_data, + ) + self.nexus_writer.create_nexus_file() diff --git a/src/hyperion/external_interaction/callbacks/xray_centre/tests/test_nexus_handler.py b/src/hyperion/external_interaction/callbacks/xray_centre/tests/test_nexus_handler.py index 376ee401d..4f6380dc1 100644 --- a/src/hyperion/external_interaction/callbacks/xray_centre/tests/test_nexus_handler.py +++ b/src/hyperion/external_interaction/callbacks/xray_centre/tests/test_nexus_handler.py @@ -5,6 +5,7 @@ from hyperion.external_interaction.callbacks.xray_centre.nexus_callback import ( GridscanNexusFileCallback, ) +from hyperion.external_interaction.nexus.nexus_utils import create_i03_goniometer_axes from hyperion.parameters.constants import ISPYB_HARDWARE_READ_PLAN from hyperion.parameters.external_parameters import from_file as default_raw_params from hyperion.parameters.plan_specific.gridscan_internal_params import ( @@ -36,7 +37,7 @@ def test_writers_not_setup_on_plan_start_doc( nexus_writer: MagicMock, dummy_params: GridscanInternalParameters, ): - nexus_handler = GridscanNexusFileCallback() + nexus_handler = GridscanNexusFileCallback(create_i03_goniometer_axes) nexus_writer.assert_not_called() nexus_handler.start( { @@ -51,7 +52,7 @@ def test_writers_dont_create_on_init_but_do_on_ispyb_event( nexus_writer: MagicMock, dummy_params: GridscanInternalParameters, ): - nexus_handler = GridscanNexusFileCallback() + nexus_handler = GridscanNexusFileCallback(create_i03_goniometer_axes) assert nexus_handler.nexus_writer_1 is None assert nexus_handler.nexus_writer_2 is None @@ -85,7 +86,7 @@ def test_writers_do_create_one_file_each_on_start_doc_for_run_gridscan( ): nexus_writer.side_effect = [MagicMock(), MagicMock()] - nexus_handler = GridscanNexusFileCallback() + nexus_handler = GridscanNexusFileCallback(create_i03_goniometer_axes) nexus_handler.start( { "subplan_name": "run_gridscan_move_and_tidy", @@ -113,7 +114,7 @@ def test_writers_do_create_one_file_each_on_start_doc_for_run_gridscan( def test_sensible_error_if_writing_triggered_before_params_received( nexus_writer: MagicMock, dummy_params ): - nexus_handler = GridscanNexusFileCallback() + nexus_handler = GridscanNexusFileCallback(create_i03_goniometer_axes) with pytest.raises(AssertionError) as excinfo: nexus_handler.descriptor( { diff --git a/src/hyperion/external_interaction/nexus/nexus_utils.py b/src/hyperion/external_interaction/nexus/nexus_utils.py index 1e5eb6dfe..fdb3bce8d 100644 --- a/src/hyperion/external_interaction/nexus/nexus_utils.py +++ b/src/hyperion/external_interaction/nexus/nexus_utils.py @@ -10,7 +10,7 @@ from hyperion.external_interaction.ispyb.ispyb_dataclass import IspybParams -def create_goniometer_axes( +def create_i03_goniometer_axes( omega_start: float, scan_points: dict | None, x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0), @@ -65,6 +65,57 @@ def create_goniometer_axes( return Goniometer(gonio_axes, scan_points) +def create_vmxm_goniometer_axes( + omega_start: float, + scan_points: dict | None, + x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0), + chi: float = 0, + phi: float = 0, +) -> Goniometer: + """Returns a Nexgen 'Goniometer' object with the dependency chain of I03's Smargon + goniometer. If scan points is provided these values will be used in preference to + those from the params object. + + Args: + omega_start (float): the starting position of omega, the only extra value that + needs to be specified except for the scan points. + scan_points (dict): a dictionary of points in the scan for each axis. Obtained + by calculating the scan path with scanspec and calling + consume() on it. + x_y_z_increments: optionally, specify the increments between each image for + the x, y, and z axes. Will be ignored if scan_points + is provided. + """ + gonio_axes = [ + Axis("omega", ".", TransformationType.ROTATION, (0.0, 1.0, 0.0), omega_start), + Axis( + name="sam_z", + depends="omega", + transformation_type=TransformationType.TRANSLATION, + vector=(0.0, 0.0, 1.0), + start_pos=0.0, + increment=x_y_z_increments[2], + ), + Axis( + name="sam_y", + depends="sam_z", + transformation_type=TransformationType.TRANSLATION, + vector=(0.0, 1.0, 0.0), + start_pos=0.0, + increment=x_y_z_increments[1], + ), + Axis( + name="sam_x", + depends="sam_y", + transformation_type=TransformationType.TRANSLATION, + vector=(1.0, 0.0, 0.0), + start_pos=0.0, + increment=x_y_z_increments[0], + ), + ] + return Goniometer(gonio_axes, scan_points) + + def get_start_and_predicted_end_time(time_expected: float) -> tuple[str, str]: time_format = r"%Y-%m-%dT%H:%M:%SZ" start = datetime.utcfromtimestamp(time.time()) @@ -83,8 +134,10 @@ def create_detector_parameters(detector_params: DetectorParams) -> Detector: """ detector_pixels = detector_params.get_detector_size_pizels() + # FIXME - Make generic enough for both VMXm and i03 (and other beamlines) + # get these from detector_params probably. eiger_params = EigerDetector( - "Eiger 16M", (detector_pixels.height, detector_pixels.width), "Si", 46051, 0 + "Eiger 9M", (detector_pixels.height, detector_pixels.width), "CdTe", 2418770, 0 ) detector_axes = [ diff --git a/src/hyperion/external_interaction/nexus/write_nexus.py b/src/hyperion/external_interaction/nexus/write_nexus.py index 3b63f9155..7c330bcfe 100644 --- a/src/hyperion/external_interaction/nexus/write_nexus.py +++ b/src/hyperion/external_interaction/nexus/write_nexus.py @@ -6,6 +6,7 @@ import math from pathlib import Path +from typing import Protocol from nexgen.nxs_utils import Detector, Goniometer, Source from nexgen.nxs_write.NXmxWriter import NXmxFileWriter @@ -13,18 +14,30 @@ from hyperion.external_interaction.nexus.nexus_utils import ( create_beam_and_attenuator_parameters, create_detector_parameters, - create_goniometer_axes, get_start_and_predicted_end_time, ) from hyperion.parameters.internal_parameters import InternalParameters +class CreateGoniometerProtocol(Protocol): + @staticmethod + def __call__( + omega_start: float, + scan_points: dict | None, + x_y_z_increments: tuple[float, float, float] = (0.0, 0.0, 0.0), + chi: float = 0.0, + phi: float = 0.0, + ) -> Goniometer: + ... + + class NexusWriter: def __init__( self, parameters: InternalParameters, scan_points: dict, data_shape: tuple[int, int, int], + create_goniometer_func: CreateGoniometerProtocol, omega_start: float | None = None, run_number: int | None = None, vds_start_index: int = 0, @@ -70,7 +83,7 @@ def __init__( chi = parameters.experiment_params.chi_start except Exception: chi = 0.0 - self.goniometer: Goniometer = create_goniometer_axes( + self.goniometer: Goniometer = create_goniometer_func( self.omega_start, self.scan_points, chi=chi ) diff --git a/src/hyperion/external_interaction/unit_tests/test_write_nexus.py b/src/hyperion/external_interaction/unit_tests/test_write_nexus.py index 243cb13ba..50c3eb31b 100644 --- a/src/hyperion/external_interaction/unit_tests/test_write_nexus.py +++ b/src/hyperion/external_interaction/unit_tests/test_write_nexus.py @@ -5,8 +5,9 @@ import h5py import numpy as np import pytest -from dodal.devices.fast_grid_scan import GridAxis, GridScanParams +from dodal.devices.fast_grid_scan_common import GridAxis, GridScanParams +from hyperion.external_interaction.nexus.nexus_utils import create_i03_goniometer_axes from hyperion.external_interaction.nexus.write_nexus import NexusWriter from hyperion.parameters.plan_specific.gridscan_internal_params import ( GridscanInternalParameters, @@ -26,10 +27,15 @@ def assert_end_data_correct(nexus_writer: NexusWriter): @pytest.fixture def dummy_nexus_writers(test_fgs_params: GridscanInternalParameters): nexus_info_1 = test_fgs_params.get_nexus_info(1) - nexus_writer_1 = NexusWriter(test_fgs_params, **nexus_info_1) + nexus_writer_1 = NexusWriter( + test_fgs_params, + create_goniometer_func=create_i03_goniometer_axes, + **nexus_info_1, + ) nexus_info_2 = test_fgs_params.get_nexus_info(2) nexus_writer_2 = NexusWriter( test_fgs_params, + create_goniometer_func=create_i03_goniometer_axes, **nexus_info_2, vds_start_index=nexus_info_1["data_shape"][0], ) @@ -49,8 +55,16 @@ def create_nexus_writers_with_many_images(parameters: GridscanInternalParameters parameters.experiment_params.y_steps = y parameters.experiment_params.z_steps = z parameters.hyperion_params.detector_params.num_triggers = x * y + x * z - nexus_writer_1 = NexusWriter(parameters, **parameters.get_nexus_info(1)) - nexus_writer_2 = NexusWriter(parameters, **parameters.get_nexus_info(2)) + nexus_writer_1 = NexusWriter( + parameters, + create_goniometer_func=create_i03_goniometer_axes, + **parameters.get_nexus_info(1), + ) + nexus_writer_2 = NexusWriter( + parameters, + create_goniometer_func=create_i03_goniometer_axes, + **parameters.get_nexus_info(2), + ) yield nexus_writer_1, nexus_writer_2 @@ -71,7 +85,11 @@ def dummy_nexus_writers_with_more_images(test_fgs_params: GridscanInternalParame @pytest.fixture def single_dummy_file(test_fgs_params: GridscanInternalParameters): - nexus_writer = NexusWriter(test_fgs_params, **test_fgs_params.get_nexus_info(1)) + nexus_writer = NexusWriter( + test_fgs_params, + create_goniometer_func=create_i03_goniometer_axes, + **test_fgs_params.get_nexus_info(1), + ) yield nexus_writer for file in [nexus_writer.nexus_file, nexus_writer.master_file]: if os.path.isfile(file): diff --git a/src/hyperion/log.py b/src/hyperion/log.py index 645514278..1c2ebf47a 100755 --- a/src/hyperion/log.py +++ b/src/hyperion/log.py @@ -36,7 +36,12 @@ def set_up_logging_handlers( Mode defaults to production and can be switched to dev with the --dev flag on run. """ - handlers = setup_dodal_logging(logging_level, dev_mode, _get_logging_file_path(), file_handler_logging_level="DEBUG") + handlers = setup_dodal_logging( + logging_level, + dev_mode, + _get_logging_file_path(), + file_handler_logging_level="DEBUG", + ) dodal_logger.addFilter(dc_group_id_filter) LOGGER.addFilter(dc_group_id_filter) diff --git a/src/hyperion/parameters/plan_specific/gridscan_internal_params.py b/src/hyperion/parameters/plan_specific/gridscan_internal_params.py index 1f2a4d240..77441740b 100644 --- a/src/hyperion/parameters/plan_specific/gridscan_internal_params.py +++ b/src/hyperion/parameters/plan_specific/gridscan_internal_params.py @@ -4,7 +4,7 @@ import numpy as np from dodal.devices.detector import DetectorParams, TriggerMode -from dodal.devices.fast_grid_scan import GridAxis, GridScanParams +from dodal.devices.fast_grid_scan_common import GridAxis, GridScanParams from pydantic import validator from scanspec.core import Path as ScanPath from scanspec.specs import Line diff --git a/test_parameter_defaults.json b/test_parameter_defaults.json index f636be52d..1b9f541d4 100644 --- a/test_parameter_defaults.json +++ b/test_parameter_defaults.json @@ -4,6 +4,7 @@ "zocalo_environment": "dev_artemis", "beamline": "BL03S", "insertion_prefix": "SR03S", + "detector": "EIGER2_X_16M", "experiment_type": "flyscan_xray_centre", "detector_params": { "current_energy_ev": 100,