From a51667518d9bf24fdce79096659c6078ef661d34 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Tue, 17 Oct 2023 17:14:56 +0100 Subject: [PATCH 01/11] [#207] Add VMXm fast grid scan devices --- src/dodal/beamlines/vmxm.py | 57 +++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/dodal/beamlines/vmxm.py diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py new file mode 100644 index 0000000000..1792eb4c6d --- /dev/null +++ b/src/dodal/beamlines/vmxm.py @@ -0,0 +1,57 @@ +from dodal.beamlines.beamline_utils import device_instantiation +from dodal.beamlines.beamline_utils import set_beamline as set_utils_beamline +from dodal.devices.eiger import EigerDetector +from dodal.devices.fast_grid_scan import FastGridScan +from dodal.devices.zebra import Zebra +from dodal.log import set_beamline as set_log_beamline +from dodal.utils import get_beamline_name, skip_device + +SIM_BEAMLINE_NAME = "svmxm" + +BL = get_beamline_name(SIM_BEAMLINE_NAME) +set_log_beamline(BL) +set_utils_beamline(BL) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def eiger(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> EigerDetector: + """Get the vmxm eiger device, instantiate it if it hasn't already been. + If this is called when already instantiated in vmxm, it will return the existing object. + """ + return device_instantiation( + device_factory=EigerDetector, + name="eiger", + prefix="-EA-EIGER-01:", + wait=wait_for_connection, + fake=fake_with_ophyd_sim, + ) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def fast_grid_scan( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> FastGridScan: + """Get the vmxm fast_grid_scan device, instantiate it if it hasn't already been. + If this is called when already instantiated in vmxm, it will return the existing object. + """ + return device_instantiation( + device_factory=FastGridScan, + name="fast_grid_scan", + prefix="-MO-SAMP-11:FGS:", # TODO: currently needs Pxxxx prefixes in EPICS on VMXm, ask controls if we can alias to versions without these prefixes? + wait=wait_for_connection, + fake=fake_with_ophyd_sim, + ) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def zebra(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> Zebra: + """Get the vmxm zebra device, instantiate it if it hasn't already been. + If this is called when already instantiated in vmxm, it will return the existing object. + """ + return device_instantiation( + Zebra, + "zebra", + "-EA-ZEBRA-01:", + wait_for_connection, + fake_with_ophyd_sim, + ) From 2380dfdc84db8904e11d5f0a6d0cf8fc6b247751 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Wed, 18 Oct 2023 11:37:32 +0100 Subject: [PATCH 02/11] Move to i02-1 rather than VMXm --- src/dodal/beamlines/{vmxm.py => i02_1.py} | 6 ++++-- tests/beamlines/unit_tests/test_i02_1.py | 14 ++++++++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) rename src/dodal/beamlines/{vmxm.py => i02_1.py} (93%) create mode 100644 tests/beamlines/unit_tests/test_i02_1.py diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/i02_1.py similarity index 93% rename from src/dodal/beamlines/vmxm.py rename to src/dodal/beamlines/i02_1.py index 1792eb4c6d..de56c6120b 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/i02_1.py @@ -6,7 +6,7 @@ from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device -SIM_BEAMLINE_NAME = "svmxm" +SIM_BEAMLINE_NAME = "S02-2" BL = get_beamline_name(SIM_BEAMLINE_NAME) set_log_beamline(BL) @@ -14,7 +14,9 @@ @skip_device(lambda: BL == SIM_BEAMLINE_NAME) -def eiger(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) -> EigerDetector: +def eiger( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> EigerDetector: """Get the vmxm eiger device, instantiate it if it hasn't already been. If this is called when already instantiated in vmxm, it will return the existing object. """ diff --git a/tests/beamlines/unit_tests/test_i02_1.py b/tests/beamlines/unit_tests/test_i02_1.py new file mode 100644 index 0000000000..c22e35548d --- /dev/null +++ b/tests/beamlines/unit_tests/test_i02_1.py @@ -0,0 +1,14 @@ +from dodal.beamlines import beamline_utils, i02_1 + + +def test_list(): + beamline_utils.clear_devices() + i02_1.eiger(wait_for_connection=False) + i02_1.fast_grid_scan(wait_for_connection=False) + i02_1.zebra(wait_for_connection=False) + + assert beamline_utils.list_active_devices() == [ + "eiger", + "fast_grid_scan", + "zebra", + ] From 8cbc6e9cb837cdee208accedf85339dbd46bed8b Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Wed, 18 Oct 2023 16:27:17 +0100 Subject: [PATCH 03/11] Fix unit tests; make set_beamline more generic; add VMXm system test --- src/dodal/beamlines/beamline_utils.py | 20 +++++++++---- src/dodal/beamlines/{i02_1.py => vmxm.py} | 6 ++-- src/dodal/utils.py | 16 +++++++++-- tests/beamlines/unit_tests/test_i02_1.py | 14 --------- tests/beamlines/unit_tests/test_i24.py | 2 ++ tests/beamlines/unit_tests/test_vmxm.py | 35 +++++++++++++++++++++++ tests/system_tests/test_vmxm_system.py | 18 ++++++++++++ 7 files changed, 86 insertions(+), 25 deletions(-) rename src/dodal/beamlines/{i02_1.py => vmxm.py} (90%) delete mode 100644 tests/beamlines/unit_tests/test_i02_1.py create mode 100644 tests/beamlines/unit_tests/test_vmxm.py create mode 100644 tests/system_tests/test_vmxm_system.py diff --git a/src/dodal/beamlines/beamline_utils.py b/src/dodal/beamlines/beamline_utils.py index 43f5bdc335..024b81c4db 100644 --- a/src/dodal/beamlines/beamline_utils.py +++ b/src/dodal/beamlines/beamline_utils.py @@ -13,11 +13,23 @@ ACTIVE_DEVICES: Dict[str, AnyDevice] = {} BL = "" +PREFIX: BeamlinePrefix = None # type: ignore -def set_beamline(beamline: str): - global BL +def set_beamline( + beamline: str, + suffix: Optional[str] = None, + beamline_prefix: Optional[str] = None, + insertion_prefix: Optional[str] = None, +): + global BL, PREFIX BL = beamline + PREFIX = BeamlinePrefix( + ixx=beamline, + suffix=suffix, + beamline_prefix=beamline_prefix, + insertion_prefix=insertion_prefix, + ) def clear_devices(): @@ -95,9 +107,7 @@ def device_instantiation( if already_existing_device is None: device_instance = device_factory( name=name, - prefix=f"{(BeamlinePrefix(BL).beamline_prefix)}{prefix}" - if bl_prefix - else prefix, + prefix=f"{PREFIX.beamline_prefix}{prefix}" if bl_prefix else prefix, **kwargs, ) ACTIVE_DEVICES[name] = device_instance diff --git a/src/dodal/beamlines/i02_1.py b/src/dodal/beamlines/vmxm.py similarity index 90% rename from src/dodal/beamlines/i02_1.py rename to src/dodal/beamlines/vmxm.py index de56c6120b..6fa0e492a0 100644 --- a/src/dodal/beamlines/i02_1.py +++ b/src/dodal/beamlines/vmxm.py @@ -6,11 +6,11 @@ from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device -SIM_BEAMLINE_NAME = "S02-2" +SIM_BEAMLINE_NAME = "S02-1" BL = get_beamline_name(SIM_BEAMLINE_NAME) set_log_beamline(BL) -set_utils_beamline(BL) +set_utils_beamline(BL, suffix="J", beamline_prefix="BL02J", insertion_prefix="SR-DI-J02") @skip_device(lambda: BL == SIM_BEAMLINE_NAME) @@ -39,7 +39,7 @@ def fast_grid_scan( return device_instantiation( device_factory=FastGridScan, name="fast_grid_scan", - prefix="-MO-SAMP-11:FGS:", # TODO: currently needs Pxxxx prefixes in EPICS on VMXm, ask controls if we can alias to versions without these prefixes? + prefix="-MO-SAMP-11:FGS:", wait=wait_for_connection, fake=fake_with_ophyd_sim, ) diff --git a/src/dodal/utils.py b/src/dodal/utils.py index d79c23165f..0b86f994ee 100644 --- a/src/dodal/utils.py +++ b/src/dodal/utils.py @@ -81,11 +81,21 @@ def get_hostname() -> str: class BeamlinePrefix: ixx: str suffix: Optional[str] = None + beamline_prefix: Optional[str] = None + insertion_prefix: Optional[str] = None def __post_init__(self): - self.suffix = self.ixx[0].upper() if not self.suffix else self.suffix - self.beamline_prefix = f"BL{self.ixx[1:3]}{self.suffix}" - self.insertion_prefix = f"SR{self.ixx[1:3]}{self.suffix}" + self.suffix = self.ixx[0].upper() if self.suffix is None else self.suffix + self.beamline_prefix = ( + f"BL{self.ixx[1:3]}{self.suffix}" + if self.beamline_prefix is None + else self.beamline_prefix + ) + self.insertion_prefix = ( + f"SR{self.ixx[1:3]}{self.suffix}" + if self.insertion_prefix is None + else self.insertion_prefix + ) T = TypeVar("T", bound=AnyDevice) diff --git a/tests/beamlines/unit_tests/test_i02_1.py b/tests/beamlines/unit_tests/test_i02_1.py deleted file mode 100644 index c22e35548d..0000000000 --- a/tests/beamlines/unit_tests/test_i02_1.py +++ /dev/null @@ -1,14 +0,0 @@ -from dodal.beamlines import beamline_utils, i02_1 - - -def test_list(): - beamline_utils.clear_devices() - i02_1.eiger(wait_for_connection=False) - i02_1.fast_grid_scan(wait_for_connection=False) - i02_1.zebra(wait_for_connection=False) - - assert beamline_utils.list_active_devices() == [ - "eiger", - "fast_grid_scan", - "zebra", - ] diff --git a/tests/beamlines/unit_tests/test_i24.py b/tests/beamlines/unit_tests/test_i24.py index aa985b285b..b96c590862 100644 --- a/tests/beamlines/unit_tests/test_i24.py +++ b/tests/beamlines/unit_tests/test_i24.py @@ -1,6 +1,7 @@ import sys from unittest.mock import patch +from dodal.beamlines import beamline_utils from dodal.devices.i24.i24_vgonio import VGonio with patch.dict("os.environ", {"BEAMLINE": "i24"}, clear=True): @@ -9,6 +10,7 @@ def test_device_creation(): + beamline_utils.set_beamline("i24") devices = make_all_devices(i24, fake_with_ophyd_sim=True) assert len(devices) > 0 for device_name in devices.keys(): diff --git a/tests/beamlines/unit_tests/test_vmxm.py b/tests/beamlines/unit_tests/test_vmxm.py new file mode 100644 index 0000000000..ee55365514 --- /dev/null +++ b/tests/beamlines/unit_tests/test_vmxm.py @@ -0,0 +1,35 @@ +from unittest.mock import patch + +from dodal.beamlines import beamline_utils, vmxm + + +@patch.dict("os.environ", {"BEAMLINE": "i02-1"}, clear=True) +def test_list(): + beamline_utils.clear_devices() + beamline_utils.set_beamline( + "vmxm", suffix="J", beamline_prefix="BL02", insertion_prefix="SR-DI-J02" + ) + vmxm.eiger(wait_for_connection=False) + vmxm.fast_grid_scan(wait_for_connection=False) + vmxm.zebra(wait_for_connection=False) + + assert beamline_utils.list_active_devices() == [ + "eiger", + "fast_grid_scan", + "zebra", + ] + + +@patch.dict("os.environ", {"BEAMLINE": "i02-1"}, clear=True) +def test_prefixes(): + beamline_utils.clear_devices() + beamline_utils.set_beamline( + "vmxm", suffix="J", beamline_prefix="BL02J", insertion_prefix="SR-DI-J02" + ) + eiger = vmxm.eiger(wait_for_connection=False) + fgs = vmxm.fast_grid_scan(wait_for_connection=False) + zebra = vmxm.zebra(wait_for_connection=False) + + assert eiger.prefix == "BL02J-EA-EIGER-01:" + assert fgs.prefix == "BL02J-MO-SAMP-11:FGS:" + assert zebra.prefix == "BL02J-EA-ZEBRA-01:" diff --git a/tests/system_tests/test_vmxm_system.py b/tests/system_tests/test_vmxm_system.py new file mode 100644 index 0000000000..24ea2ab36c --- /dev/null +++ b/tests/system_tests/test_vmxm_system.py @@ -0,0 +1,18 @@ +import os + +from bluesky.run_engine import RunEngine + +from dodal.utils import make_all_devices + +if __name__ == "__main__": + """This test runs against the real beamline and confirms that all the devices connect + i.e. that we match the beamline PVs. + """ + os.environ["BEAMLINE"] = "i02-1" + from dodal.beamlines import vmxm + + RE = RunEngine() + + print("Making all VMXm devices") + make_all_devices(vmxm) + print("Successfully connected") \ No newline at end of file From 335ef4ae7a2dbfbe10aa95cf59a82aa63074d7bf Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Wed, 18 Oct 2023 16:35:09 +0100 Subject: [PATCH 04/11] lint --- src/dodal/beamlines/vmxm.py | 4 +++- tests/system_tests/test_vmxm_system.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index 6fa0e492a0..84ccf6da5d 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -10,7 +10,9 @@ BL = get_beamline_name(SIM_BEAMLINE_NAME) set_log_beamline(BL) -set_utils_beamline(BL, suffix="J", beamline_prefix="BL02J", insertion_prefix="SR-DI-J02") +set_utils_beamline( + BL, suffix="J", beamline_prefix="BL02J", insertion_prefix="SR-DI-J02" +) @skip_device(lambda: BL == SIM_BEAMLINE_NAME) diff --git a/tests/system_tests/test_vmxm_system.py b/tests/system_tests/test_vmxm_system.py index 24ea2ab36c..c98049e4a6 100644 --- a/tests/system_tests/test_vmxm_system.py +++ b/tests/system_tests/test_vmxm_system.py @@ -15,4 +15,4 @@ print("Making all VMXm devices") make_all_devices(vmxm) - print("Successfully connected") \ No newline at end of file + print("Successfully connected") From 900467a07f94b670cf5870e46c5e0afb8f28789a Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Wed, 18 Oct 2023 17:09:04 +0100 Subject: [PATCH 05/11] Return correct beamline module on VMXm --- src/dodal/utils.py | 8 ++++++++ tests/test_utils.py | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dodal/utils.py b/src/dodal/utils.py index 0b86f994ee..5f93ec5ac5 100644 --- a/src/dodal/utils.py +++ b/src/dodal/utils.py @@ -220,12 +220,20 @@ def is_v1_device_type(obj: Type[Any]) -> bool: return is_class and follows_protocols and not is_v2_device_type(obj) +# Special case for i02-1 -> VMXm +# Scientists refer to it as VMXm but $BEAMLINE env var is i02-1 +BEAMLINE_NAME_TO_MODULE_NAME_OVERRIDES = { + "i02-1": "vmxm", +} + + def get_beamline_based_on_environment_variable() -> ModuleType: """ Gets the dodal module for the current beamline, as specified by the BEAMLINE environment variable. """ beamline = get_beamline_name("") + beamline = BEAMLINE_NAME_TO_MODULE_NAME_OVERRIDES.get(beamline, beamline) if beamline == "": raise ValueError( diff --git a/tests/test_utils.py b/tests/test_utils.py index 9c576348ec..224e323bae 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,7 +5,7 @@ from bluesky.protocols import Readable from ophyd import EpicsMotor -from dodal.beamlines import i03, i23 +from dodal.beamlines import i03, i23, vmxm from dodal.utils import ( collect_factories, get_beamline_based_on_environment_variable, @@ -81,7 +81,7 @@ def test_invalid_beamline_variable_causes_get_device_module_to_raise(bl): get_beamline_based_on_environment_variable() -@pytest.mark.parametrize("bl,module", [("i03", i03), ("i23", i23)]) +@pytest.mark.parametrize("bl,module", [("i03", i03), ("i23", i23), ("i02-1", vmxm)]) def test_valid_beamline_variable_causes_get_device_module_to_return_module(bl, module): with patch.dict(os.environ, {"BEAMLINE": bl}): assert get_beamline_based_on_environment_variable() == module From 75e5928a3e202f6adbddc2a03fa11aabd2618c17 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Mon, 23 Oct 2023 16:38:03 +0100 Subject: [PATCH 06/11] Add VMXm attenuator --- src/dodal/beamlines/vmxm.py | 17 ++++++++ src/dodal/devices/vmxm/__init__.py | 0 src/dodal/devices/vmxm/vmxm_attenuator.py | 47 +++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 src/dodal/devices/vmxm/__init__.py create mode 100644 src/dodal/devices/vmxm/vmxm_attenuator.py diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index 84ccf6da5d..c62dd47bf2 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -2,6 +2,7 @@ from dodal.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan import FastGridScan +from dodal.devices.vmxm.vmxm_attenuator import VmxmAttenuator from dodal.devices.zebra import Zebra from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device @@ -59,3 +60,19 @@ def zebra(wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False) - wait_for_connection, fake_with_ophyd_sim, ) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def attenuator( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> VmxmAttenuator: + """Get the vmxm attenuator device, instantiate it if it hasn't already been. + If this is called when already instantiated in vmxm, it will return the existing object. + """ + return device_instantiation( + VmxmAttenuator, + "attenuator", + "-OP-ATTN-01:", + wait_for_connection, + fake_with_ophyd_sim, + ) diff --git a/src/dodal/devices/vmxm/__init__.py b/src/dodal/devices/vmxm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/dodal/devices/vmxm/vmxm_attenuator.py b/src/dodal/devices/vmxm/vmxm_attenuator.py new file mode 100644 index 0000000000..d47d1b8249 --- /dev/null +++ b/src/dodal/devices/vmxm/vmxm_attenuator.py @@ -0,0 +1,47 @@ +from typing import Optional + +from ophyd import Component, Device, EpicsSignal, Kind +from ophyd.status import Status, SubscriptionStatus + +from dodal.devices.detector import DetectorParams +from dodal.devices.status import await_value +from dodal.log import LOGGER + + +class VmxmAttenuator(Device): + """Any reference to transmission (both read and write) in this Device is fraction + e.g. 0-1""" + + def set(self, transmission: float) -> SubscriptionStatus: + """Set the transmission to the fractional value given. + Args: + transmission (float): A fraction to set transmission to between 0-1 + Get desired states and calculated states, return a status which is complete once they are equal + """ + + LOGGER.info("Using current energy ") + self.use_current_energy.set(1).wait() + LOGGER.info(f"Setting desired transmission to {transmission}") + self.desired_transmission.set(transmission).wait() + LOGGER.info("Sending change filter command") + self.change.set(1).wait() + + status = Status(done=True, success=True) + status &= await_value(self.filter1_inpos, 1, timeout=10) + status &= await_value(self.filter2_inpos, 1, timeout=10) + status &= await_value(self.filter3_inpos, 1, timeout=10) + status &= await_value(self.filter4_inpos, 1, timeout=10) + return status + + desired_transmission: EpicsSignal = Component(EpicsSignal, "T2A:SETVAL1") + use_current_energy: EpicsSignal = Component( + EpicsSignal, "E2WL:USECURRENTENERGY.PROC" + ) + actual_transmission: EpicsSignal = Component(EpicsSignal, "MATCH", kind=Kind.hinted) + + detector_params: Optional[DetectorParams] = None + + filter1_inpos: EpicsSignal = Component(EpicsSignal, "MP1:INPOS") + filter2_inpos: EpicsSignal = Component(EpicsSignal, "MP2:INPOS") + filter3_inpos: EpicsSignal = Component(EpicsSignal, "MP3:INPOS") + filter4_inpos: EpicsSignal = Component(EpicsSignal, "MP4:INPOS") From 429eb1da2e756edf9d93c3bc4b98e9be0d9a07e5 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Fri, 27 Oct 2023 16:22:27 +0100 Subject: [PATCH 07/11] VMXM - changes on beamline --- src/dodal/beamlines/vmxm.py | 36 +++++++++++++++++++ src/dodal/devices/backlight.py | 4 +-- src/dodal/devices/detector.py | 4 +-- src/dodal/devices/eiger.py | 20 ++++++++--- src/dodal/devices/fast_grid_scan.py | 43 +++++++++++++---------- src/dodal/devices/utils.py | 2 +- src/dodal/devices/vmxm/vmxm_attenuator.py | 2 -- 7 files changed, 80 insertions(+), 31 deletions(-) diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index c62dd47bf2..d490e63ab5 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -4,6 +4,8 @@ from dodal.devices.fast_grid_scan import FastGridScan from dodal.devices.vmxm.vmxm_attenuator import VmxmAttenuator from dodal.devices.zebra import Zebra +from dodal.devices.backlight import Backlight +from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device @@ -76,3 +78,37 @@ def attenuator( wait_for_connection, fake_with_ophyd_sim, ) + + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def backlight( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> Backlight: + """Get the i03 backlight device, instantiate it if it hasn't already been. + If this is called when already instantiated in i03, it will return the existing object. + """ + return device_instantiation( + device_factory=Backlight, + name="backlight", + prefix="", + wait=wait_for_connection, + fake=fake_with_ophyd_sim, + ) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def synchrotron( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> Synchrotron: + """Get the i03 synchrotron device, instantiate it if it hasn't already been. + If this is called when already instantiated in i03, it will return the existing object. + """ + return device_instantiation( + Synchrotron, + "synchrotron", + "", + wait_for_connection, + fake_with_ophyd_sim, + bl_prefix=False, + ) diff --git a/src/dodal/devices/backlight.py b/src/dodal/devices/backlight.py index da07aae014..1f984cd757 100644 --- a/src/dodal/devices/backlight.py +++ b/src/dodal/devices/backlight.py @@ -7,9 +7,9 @@ class Backlight(Device): OUT = 0 IN = 1 - pos: EpicsSignal = Component(EpicsSignal, "-EA-BL-01:CTRL") + pos: EpicsSignal = Component(EpicsSignal, "-DI-IOC-02:LED:INOUT") # Toggle to switch it On or Off - toggle: EpicsSignal = Component(EpicsSignal, "-EA-BLIT-01:TOGGLE") + toggle: EpicsSignal = Component(EpicsSignal, "-EA-OAV-01:FZOOM:TOGGLE") def set(self, position: int) -> StatusBase: status = self.pos.set(position) diff --git a/src/dodal/devices/detector.py b/src/dodal/devices/detector.py index b6c10b355a..a2304621c9 100644 --- a/src/dodal/devices/detector.py +++ b/src/dodal/devices/detector.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, validator from dodal.devices.det_dim_constants import ( - EIGER2_X_16M_SIZE, + EIGER2_X_9M_SIZE, DetectorSize, DetectorSizeConstants, constants_from_type, @@ -40,7 +40,7 @@ class DetectorParams(BaseModel): use_roi_mode: bool det_dist_to_beam_converter_path: str trigger_mode: TriggerMode = TriggerMode.SET_FRAMES - detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE + detector_size_constants: DetectorSizeConstants = EIGER2_X_9M_SIZE # This looks like it's always using the default and not taken from the json beam_xy_converter: DetectorDistanceToBeamXYConverter = None class Config: diff --git a/src/dodal/devices/eiger.py b/src/dodal/devices/eiger.py index 1ebc46cc7f..cebcc1cb71 100644 --- a/src/dodal/devices/eiger.py +++ b/src/dodal/devices/eiger.py @@ -58,6 +58,8 @@ def with_params( return det def set_detector_parameters(self, detector_params: DetectorParams): + LOGGER.info(f"Exposure time {detector_params.exposure_time}") + self.detector_params = detector_params if self.detector_params is None: raise Exception("Parameters for scan must be specified") @@ -103,7 +105,7 @@ def stage(self): self.async_stage().wait(timeout=self.ARMING_TIMEOUT) def stop_odin_when_all_frames_collected(self): - LOGGER.info("Waiting on all frames") + LOGGER.info(f"Waiting on all frames, expected {self.detector_params.full_number_of_images}") try: await_value( self.odin.file_writer.num_captured, @@ -151,6 +153,8 @@ def change_roi_mode(self, enable: bool) -> Status: else self.detector_params.detector_size_constants.det_size_pixels ) + LOGGER.info(f"Setting height and width on odin to {detector_dimensions.height}, {detector_dimensions.width}") + status = self.cam.roi_mode.set( 1 if enable else 0, timeout=self.GENERAL_STATUS_TIMEOUT ) @@ -170,6 +174,7 @@ def change_roi_mode(self, enable: bool) -> Status: return status def set_cam_pvs(self) -> AndStatus: + LOGGER.info("Setting cam pvs") assert self.detector_params is not None status = self.cam.acquire_time.set( self.detector_params.exposure_time, timeout=self.GENERAL_STATUS_TIMEOUT @@ -188,6 +193,7 @@ def set_cam_pvs(self) -> AndStatus: return status def set_odin_number_of_frame_chunks(self) -> Status: + LOGGER.info("Setting num frames") assert self.detector_params is not None status = self.odin.file_writer.num_frames_chunks.set( 1, timeout=self.GENERAL_STATUS_TIMEOUT @@ -195,6 +201,7 @@ def set_odin_number_of_frame_chunks(self) -> Status: return status def set_odin_pvs(self) -> Status: + LOGGER.info("Setting odin PVs") assert self.detector_params is not None file_prefix = self.detector_params.full_filename status = self.odin.file_writer.file_path.set( @@ -212,6 +219,7 @@ def set_odin_pvs(self) -> Status: return status def set_mx_settings_pvs(self): + LOGGER.info("Setting mx settings") assert self.detector_params is not None beam_x_pixels, beam_y_pixels = self.detector_params.get_beam_position_pixels( self.detector_params.detector_distance @@ -241,7 +249,7 @@ def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Statu tolerance (float, optional): If the energy is already set to within this tolerance it is not set again. Defaults to 0.1eV. """ - + LOGGER.info("Setting threshold energy") current_energy = self.cam.photon_energy.get() if abs(current_energy - energy) > tolerance: return self.cam.photon_energy.set( @@ -257,7 +265,7 @@ def set_num_triggers_and_captures(self) -> Status: during the datacollection. The number of images is the number of images per trigger. """ - + LOGGER.info("Num triggers") assert self.detector_params is not None status = self.cam.num_images.set( self.detector_params.num_images_per_trigger, @@ -284,6 +292,7 @@ def set_num_triggers_and_captures(self) -> Status: return status def _wait_for_odin_status(self) -> Status: + LOGGER.info("Wait for odin status") self.forward_bit_depth_to_filewriter() status = self.odin.file_writer.capture.set( 1, timeout=self.GENERAL_STATUS_TIMEOUT @@ -313,8 +322,9 @@ def disarm_detector(self): def do_arming_chain(self) -> Status: functions_to_do_arm = [] detector_params: DetectorParams = self.detector_params - if detector_params.use_roi_mode: - functions_to_do_arm.append(lambda: self.change_roi_mode(enable=True)) + # Default is no ROI on i03 so we only switych one way + #if detector_params.use_roi_mode: + functions_to_do_arm.append(lambda: self.change_roi_mode(detector_params.use_roi_mode)) functions_to_do_arm.extend( [ diff --git a/src/dodal/devices/fast_grid_scan.py b/src/dodal/devices/fast_grid_scan.py index a71a4a380d..73954f093e 100644 --- a/src/dodal/devices/fast_grid_scan.py +++ b/src/dodal/devices/fast_grid_scan.py @@ -12,6 +12,7 @@ EpicsSignalRO, EpicsSignalWithRBV, Signal, + Kind ) from ophyd.status import DeviceStatus, StatusBase from pydantic import BaseModel, validator @@ -208,35 +209,38 @@ def clean_up(self): class FastGridScan(Device): x_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_NUM_STEPS") y_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_NUM_STEPS") - z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS") + #z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS") + z_steps: Signal = Component(Signal, kind=Kind.hinted, value=1) x_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_STEP_SIZE") y_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_STEP_SIZE") - z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE") + # z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE") + z_step_size: Signal = Component(Signal, kind=Kind.hinted, value=1) - dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "DWELL_TIME") + dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "EXPOSURE_TIME") x_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_START") y1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_START") - y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START") + # y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START") z1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_START") - z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START") + # z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START") position_counter: EpicsSignal = Component( - EpicsSignal, "POS_COUNTER", write_pv="POS_COUNTER_WRITE" + EpicsSignal, "POS_COUNTER_RBV", write_pv="POS_COUNTER" ) x_counter: EpicsSignalRO = Component(EpicsSignalRO, "X_COUNTER") y_counter: EpicsSignalRO = Component(EpicsSignalRO, "Y_COUNTER") - scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID") + # scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID") + scan_invalid: Signal = Component(Signal, kind=Kind.hinted, value=0) run_cmd: EpicsSignal = Component(EpicsSignal, "RUN.PROC") stop_cmd: EpicsSignal = Component(EpicsSignal, "STOP.PROC") - status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS") + status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS_RBV") expected_images: Signal = Component(Signal) # Kickoff timeout in seconds - KICKOFF_TIMEOUT: float = 5.0 + KICKOFF_TIMEOUT: float = 20.0 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -262,10 +266,11 @@ def kickoff(self) -> StatusBase: def scan(): try: - self.log.debug("Running scan") + self.log.info("Running scan") self.run_cmd.put(1) - self.log.debug("Waiting for scan to start") + self.log.info("Waiting for scan to start") await_value(self.status, 1).wait() + self.log.info("Kickoff finished") st.set_finished() except Exception as e: st.set_exception(e) @@ -289,26 +294,26 @@ def set_fast_grid_scan_params(scan: FastGridScan, params: GridScanParams): params.x_steps, scan.y_steps, params.y_steps, - scan.z_steps, - params.z_steps, + #scan.z_steps, + #params.z_steps, scan.x_step_size, params.x_step_size, scan.y_step_size, params.y_step_size, - scan.z_step_size, - params.z_step_size, + #scan.z_step_size, + #params.z_step_size, scan.dwell_time, params.dwell_time, scan.x_start, params.x_start, scan.y1_start, params.y1_start, - scan.y2_start, - params.y2_start, + #scan.y2_start, + #params.y2_start, scan.z1_start, params.z1_start, - scan.z2_start, - params.z2_start, + #scan.z2_start, + #params.z2_start, scan.position_counter, 0, ) diff --git a/src/dodal/devices/utils.py b/src/dodal/devices/utils.py index 303d699d08..74c85cdcf0 100644 --- a/src/dodal/devices/utils.py +++ b/src/dodal/devices/utils.py @@ -2,7 +2,7 @@ from typing import Callable, Optional from ophyd import Component, Device, EpicsSignal -from ophyd.status import Status, StatusBase +from ophyd.status import AndStatus, Status, StatusBase from dodal.log import LOGGER diff --git a/src/dodal/devices/vmxm/vmxm_attenuator.py b/src/dodal/devices/vmxm/vmxm_attenuator.py index d47d1b8249..dee7d2cf88 100644 --- a/src/dodal/devices/vmxm/vmxm_attenuator.py +++ b/src/dodal/devices/vmxm/vmxm_attenuator.py @@ -23,8 +23,6 @@ def set(self, transmission: float) -> SubscriptionStatus: self.use_current_energy.set(1).wait() LOGGER.info(f"Setting desired transmission to {transmission}") self.desired_transmission.set(transmission).wait() - LOGGER.info("Sending change filter command") - self.change.set(1).wait() status = Status(done=True, success=True) status &= await_value(self.filter1_inpos, 1, timeout=10) From 9adc9ee996ff70a1e3f340def3f85e10a318af82 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Tue, 7 Nov 2023 14:51:25 +0000 Subject: [PATCH 08/11] VMXm fast grid scans: Dodal tidy-ups --- src/dodal/beamlines/vmxm.py | 23 +- src/dodal/devices/backlight.py | 21 +- src/dodal/devices/detector.py | 2 +- src/dodal/devices/eiger.py | 24 +-- src/dodal/devices/fast_grid_scan.py | 233 ++------------------- src/dodal/devices/fast_grid_scan_2d.py | 111 ++++++++++ src/dodal/devices/fast_grid_scan_common.py | 194 +++++++++++++++++ src/dodal/devices/utils.py | 2 +- src/dodal/utils.py | 1 + tests/devices/unit_tests/test_eiger.py | 2 +- 10 files changed, 367 insertions(+), 246 deletions(-) create mode 100644 src/dodal/devices/fast_grid_scan_2d.py create mode 100644 src/dodal/devices/fast_grid_scan_common.py diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index d490e63ab5..7cb9114e6b 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -1,11 +1,11 @@ from dodal.beamlines.beamline_utils import device_instantiation from dodal.beamlines.beamline_utils import set_beamline as set_utils_beamline +from dodal.devices.backlight import VmxmBacklight from dodal.devices.eiger import EigerDetector -from dodal.devices.fast_grid_scan import FastGridScan +from dodal.devices.fast_grid_scan_2d import FastGridScan2D +from dodal.devices.synchrotron import Synchrotron from dodal.devices.vmxm.vmxm_attenuator import VmxmAttenuator from dodal.devices.zebra import Zebra -from dodal.devices.backlight import Backlight -from dodal.devices.synchrotron import Synchrotron from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device @@ -37,12 +37,12 @@ def eiger( @skip_device(lambda: BL == SIM_BEAMLINE_NAME) def fast_grid_scan( wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False -) -> FastGridScan: +) -> FastGridScan2D: """Get the vmxm fast_grid_scan device, instantiate it if it hasn't already been. If this is called when already instantiated in vmxm, it will return the existing object. """ return device_instantiation( - device_factory=FastGridScan, + device_factory=FastGridScan2D, name="fast_grid_scan", prefix="-MO-SAMP-11:FGS:", wait=wait_for_connection, @@ -80,16 +80,15 @@ def attenuator( ) - @skip_device(lambda: BL == SIM_BEAMLINE_NAME) def backlight( wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False -) -> Backlight: - """Get the i03 backlight device, instantiate it if it hasn't already been. - If this is called when already instantiated in i03, it will return the existing object. +) -> VmxmBacklight: + """Get the VMXm backlight device, instantiate it if it hasn't already been. + If this is called when already instantiated in VMXm, it will return the existing object. """ return device_instantiation( - device_factory=Backlight, + device_factory=VmxmBacklight, name="backlight", prefix="", wait=wait_for_connection, @@ -101,8 +100,8 @@ def backlight( def synchrotron( wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False ) -> Synchrotron: - """Get the i03 synchrotron device, instantiate it if it hasn't already been. - If this is called when already instantiated in i03, it will return the existing object. + """Get the VMXm synchrotron device, instantiate it if it hasn't already been. + If this is called when already instantiated in VMXm, it will return the existing object. """ return device_instantiation( Synchrotron, diff --git a/src/dodal/devices/backlight.py b/src/dodal/devices/backlight.py index 1f984cd757..f1b6485172 100644 --- a/src/dodal/devices/backlight.py +++ b/src/dodal/devices/backlight.py @@ -2,13 +2,30 @@ class Backlight(Device): - """Simple device to trigger the pneumatic in/out.""" + """Simple device for i03 to trigger the pneumatic in/out.""" + + OUT = 0 + IN = 1 + + pos: EpicsSignal = Component(EpicsSignal, "-EA-BL-01:CTRL") + toggle: EpicsSignal = Component(EpicsSignal, "-EA-BLIT-01:TOGGLE") + + def set(self, position: int) -> StatusBase: + status = self.pos.set(position) + if position == self.OUT: + status &= self.toggle.set("Off") + else: + status &= self.toggle.set("On") + return status + + +class VmxmBacklight(Device): + """Simple device for VMXm to control the backlight.""" OUT = 0 IN = 1 pos: EpicsSignal = Component(EpicsSignal, "-DI-IOC-02:LED:INOUT") - # Toggle to switch it On or Off toggle: EpicsSignal = Component(EpicsSignal, "-EA-OAV-01:FZOOM:TOGGLE") def set(self, position: int) -> StatusBase: diff --git a/src/dodal/devices/detector.py b/src/dodal/devices/detector.py index a2304621c9..4a9a51d959 100644 --- a/src/dodal/devices/detector.py +++ b/src/dodal/devices/detector.py @@ -40,7 +40,7 @@ class DetectorParams(BaseModel): use_roi_mode: bool det_dist_to_beam_converter_path: str trigger_mode: TriggerMode = TriggerMode.SET_FRAMES - detector_size_constants: DetectorSizeConstants = EIGER2_X_9M_SIZE # This looks like it's always using the default and not taken from the json + detector_size_constants: DetectorSizeConstants = EIGER2_X_9M_SIZE # This looks like it's always using the default and not taken from the json beam_xy_converter: DetectorDistanceToBeamXYConverter = None class Config: diff --git a/src/dodal/devices/eiger.py b/src/dodal/devices/eiger.py index cebcc1cb71..66177bf712 100644 --- a/src/dodal/devices/eiger.py +++ b/src/dodal/devices/eiger.py @@ -58,8 +58,6 @@ def with_params( return det def set_detector_parameters(self, detector_params: DetectorParams): - LOGGER.info(f"Exposure time {detector_params.exposure_time}") - self.detector_params = detector_params if self.detector_params is None: raise Exception("Parameters for scan must be specified") @@ -105,7 +103,9 @@ def stage(self): self.async_stage().wait(timeout=self.ARMING_TIMEOUT) def stop_odin_when_all_frames_collected(self): - LOGGER.info(f"Waiting on all frames, expected {self.detector_params.full_number_of_images}") + LOGGER.info( + f"Waiting on all frames, expected {self.detector_params.full_number_of_images}" + ) try: await_value( self.odin.file_writer.num_captured, @@ -153,7 +153,9 @@ def change_roi_mode(self, enable: bool) -> Status: else self.detector_params.detector_size_constants.det_size_pixels ) - LOGGER.info(f"Setting height and width on odin to {detector_dimensions.height}, {detector_dimensions.width}") + LOGGER.info( + f"Setting height and width on odin to {detector_dimensions.height}, {detector_dimensions.width}" + ) status = self.cam.roi_mode.set( 1 if enable else 0, timeout=self.GENERAL_STATUS_TIMEOUT @@ -174,7 +176,6 @@ def change_roi_mode(self, enable: bool) -> Status: return status def set_cam_pvs(self) -> AndStatus: - LOGGER.info("Setting cam pvs") assert self.detector_params is not None status = self.cam.acquire_time.set( self.detector_params.exposure_time, timeout=self.GENERAL_STATUS_TIMEOUT @@ -193,7 +194,6 @@ def set_cam_pvs(self) -> AndStatus: return status def set_odin_number_of_frame_chunks(self) -> Status: - LOGGER.info("Setting num frames") assert self.detector_params is not None status = self.odin.file_writer.num_frames_chunks.set( 1, timeout=self.GENERAL_STATUS_TIMEOUT @@ -201,7 +201,6 @@ def set_odin_number_of_frame_chunks(self) -> Status: return status def set_odin_pvs(self) -> Status: - LOGGER.info("Setting odin PVs") assert self.detector_params is not None file_prefix = self.detector_params.full_filename status = self.odin.file_writer.file_path.set( @@ -219,7 +218,6 @@ def set_odin_pvs(self) -> Status: return status def set_mx_settings_pvs(self): - LOGGER.info("Setting mx settings") assert self.detector_params is not None beam_x_pixels, beam_y_pixels = self.detector_params.get_beam_position_pixels( self.detector_params.detector_distance @@ -249,7 +247,6 @@ def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Statu tolerance (float, optional): If the energy is already set to within this tolerance it is not set again. Defaults to 0.1eV. """ - LOGGER.info("Setting threshold energy") current_energy = self.cam.photon_energy.get() if abs(current_energy - energy) > tolerance: return self.cam.photon_energy.set( @@ -265,7 +262,6 @@ def set_num_triggers_and_captures(self) -> Status: during the datacollection. The number of images is the number of images per trigger. """ - LOGGER.info("Num triggers") assert self.detector_params is not None status = self.cam.num_images.set( self.detector_params.num_images_per_trigger, @@ -292,7 +288,6 @@ def set_num_triggers_and_captures(self) -> Status: return status def _wait_for_odin_status(self) -> Status: - LOGGER.info("Wait for odin status") self.forward_bit_depth_to_filewriter() status = self.odin.file_writer.capture.set( 1, timeout=self.GENERAL_STATUS_TIMEOUT @@ -322,9 +317,10 @@ def disarm_detector(self): def do_arming_chain(self) -> Status: functions_to_do_arm = [] detector_params: DetectorParams = self.detector_params - # Default is no ROI on i03 so we only switych one way - #if detector_params.use_roi_mode: - functions_to_do_arm.append(lambda: self.change_roi_mode(detector_params.use_roi_mode)) + + functions_to_do_arm.append( + lambda: self.change_roi_mode(detector_params.use_roi_mode) + ) functions_to_do_arm.extend( [ diff --git a/src/dodal/devices/fast_grid_scan.py b/src/dodal/devices/fast_grid_scan.py index 73954f093e..9b633f3a47 100644 --- a/src/dodal/devices/fast_grid_scan.py +++ b/src/dodal/devices/fast_grid_scan.py @@ -1,10 +1,6 @@ import threading -import time -from typing import Any -import numpy as np from bluesky.plan_stubs import mv -from numpy import ndarray from ophyd import ( Component, Device, @@ -12,235 +8,45 @@ EpicsSignalRO, EpicsSignalWithRBV, Signal, - Kind ) from ophyd.status import DeviceStatus, StatusBase -from pydantic import BaseModel, validator -from pydantic.dataclasses import dataclass -from dodal.devices.motors import XYZLimitBundle +from dodal.devices.fast_grid_scan_common import GridScanCompleteStatus, GridScanParams from dodal.devices.status import await_value -from dodal.parameters.experiment_parameter_base import AbstractExperimentParameterBase - - -@dataclass -class GridAxis: - start: float - step_size: float - full_steps: int - - def steps_to_motor_position(self, steps): - """Gives the motor position based on steps, where steps are 0 indexed""" - return self.start + self.step_size * steps - - @property - def end(self): - """Gives the point where the final frame is taken""" - # Note that full_steps is one indexed e.g. if there is one step then the end is - # refering to the first position - return self.steps_to_motor_position(self.full_steps - 1) - - def is_within(self, steps): - return 0 <= steps <= self.full_steps - - -class GridScanParams(BaseModel, AbstractExperimentParameterBase): - """ - Holder class for the parameters of a grid scan in a similar - layout to EPICS. - - Motion program will do a grid in x-y then rotate omega +90 and perform - a grid in x-z. - - The grid specified is where data is taken e.g. it can be assumed the first frame is - at x_start, y1_start, z1_start and subsequent frames are N*step_size away. - """ - - x_steps: int = 1 - y_steps: int = 1 - z_steps: int = 0 - x_step_size: float = 0.1 - y_step_size: float = 0.1 - z_step_size: float = 0.1 - dwell_time: float = 0.1 - x_start: float = 0.1 - y1_start: float = 0.1 - y2_start: float = 0.1 - z1_start: float = 0.1 - z2_start: float = 0.1 - x_axis: GridAxis = GridAxis(0, 0, 0) - y_axis: GridAxis = GridAxis(0, 0, 0) - z_axis: GridAxis = GridAxis(0, 0, 0) - - class Config: - arbitrary_types_allowed = True - fields = { - "x_axis": {"exclude": True}, - "y_axis": {"exclude": True}, - "z_axis": {"exclude": True}, - } - - @validator("x_axis", always=True) - def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"]) - - @validator("y_axis", always=True) - def _get_y_axis(cls, y_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["y1_start"], values["y_step_size"], values["y_steps"]) - - @validator("z_axis", always=True) - def _get_z_axis(cls, z_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["z2_start"], values["z_step_size"], values["z_steps"]) - - def is_valid(self, limits: XYZLimitBundle) -> bool: - """ - Validates scan parameters - - :param limits: The motor limits against which to validate - the parameters - :return: True if the scan is valid - """ - x_in_limits = limits.x.is_within(self.x_axis.start) and limits.x.is_within( - self.x_axis.end - ) - y_in_limits = limits.y.is_within(self.y_axis.start) and limits.y.is_within( - self.y_axis.end - ) - - first_grid_in_limits = ( - x_in_limits and y_in_limits and limits.z.is_within(self.z1_start) - ) - - z_in_limits = limits.z.is_within(self.z_axis.start) and limits.z.is_within( - self.z_axis.end - ) - - second_grid_in_limits = ( - x_in_limits and z_in_limits and limits.y.is_within(self.y2_start) - ) - - return first_grid_in_limits and second_grid_in_limits - - def get_num_images(self): - return self.x_steps * self.y_steps + self.x_steps * self.z_steps - - @property - def is_3d_grid_scan(self): - return self.z_steps > 0 - - def grid_position_to_motor_position(self, grid_position: ndarray) -> ndarray: - """Converts a grid position, given as steps in the x, y, z grid, - to a real motor position. - - :param grid_position: The x, y, z position in grid steps - :return: The motor position this corresponds to. - :raises: IndexError if the desired position is outside the grid.""" - for position, axis in zip( - grid_position, [self.x_axis, self.y_axis, self.z_axis] - ): - if not axis.is_within(position): - raise IndexError(f"{grid_position} is outside the bounds of the grid") - - return np.array( - [ - self.x_axis.steps_to_motor_position(grid_position[0]), - self.y_axis.steps_to_motor_position(grid_position[1]), - self.z_axis.steps_to_motor_position(grid_position[2]), - ] - ) - - -class GridScanCompleteStatus(DeviceStatus): - """ - A Status for the grid scan completion - A special status object that notifies watchers (progress bars) - based on comparing device.expected_images to device.position_counter. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.start_ts = time.time() - - self.device.position_counter.subscribe(self._notify_watchers) - self.device.status.subscribe(self._running_changed) - - self._name = self.device.name - self._target_count = self.device.expected_images.get() - - def _notify_watchers(self, value, *args, **kwargs): - if not self._watchers: - return - time_elapsed = time.time() - self.start_ts - try: - fraction = 1 - value / self._target_count - except ZeroDivisionError: - fraction = 0 - time_remaining = 0 - except Exception as e: - fraction = None - time_remaining = None - self.set_exception(e) - self.clean_up() - else: - time_remaining = time_elapsed / fraction - for watcher in self._watchers: - watcher( - name=self._name, - current=value, - initial=0, - target=self._target_count, - unit="images", - precision=0, - fraction=fraction, - time_elapsed=time_elapsed, - time_remaining=time_remaining, - ) - - def _running_changed(self, value=None, old_value=None, **kwargs): - if (old_value == 1) and (value == 0): - self.set_finished() - self.clean_up() - - def clean_up(self): - self.device.position_counter.clear_sub(self._notify_watchers) - self.device.status.clear_sub(self._running_changed) class FastGridScan(Device): x_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_NUM_STEPS") y_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_NUM_STEPS") - #z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS") - z_steps: Signal = Component(Signal, kind=Kind.hinted, value=1) + z_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_NUM_STEPS") x_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_STEP_SIZE") y_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_STEP_SIZE") - # z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE") - z_step_size: Signal = Component(Signal, kind=Kind.hinted, value=1) + z_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_STEP_SIZE") - dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "EXPOSURE_TIME") + dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "DWELL_TIME") x_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_START") y1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_START") - # y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START") + y2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y2_START") z1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_START") - # z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START") + z2_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z2_START") position_counter: EpicsSignal = Component( - EpicsSignal, "POS_COUNTER_RBV", write_pv="POS_COUNTER" + EpicsSignal, "POS_COUNTER", write_pv="POS_COUNTER_WRITE" ) x_counter: EpicsSignalRO = Component(EpicsSignalRO, "X_COUNTER") y_counter: EpicsSignalRO = Component(EpicsSignalRO, "Y_COUNTER") - # scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID") - scan_invalid: Signal = Component(Signal, kind=Kind.hinted, value=0) + scan_invalid: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_INVALID") run_cmd: EpicsSignal = Component(EpicsSignal, "RUN.PROC") stop_cmd: EpicsSignal = Component(EpicsSignal, "STOP.PROC") - status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS_RBV") + status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS") expected_images: Signal = Component(Signal) # Kickoff timeout in seconds - KICKOFF_TIMEOUT: float = 20.0 + KICKOFF_TIMEOUT: float = 5.0 def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -266,11 +72,8 @@ def kickoff(self) -> StatusBase: def scan(): try: - self.log.info("Running scan") self.run_cmd.put(1) - self.log.info("Waiting for scan to start") await_value(self.status, 1).wait() - self.log.info("Kickoff finished") st.set_finished() except Exception as e: st.set_exception(e) @@ -294,26 +97,26 @@ def set_fast_grid_scan_params(scan: FastGridScan, params: GridScanParams): params.x_steps, scan.y_steps, params.y_steps, - #scan.z_steps, - #params.z_steps, + scan.z_steps, + params.z_steps, scan.x_step_size, params.x_step_size, scan.y_step_size, params.y_step_size, - #scan.z_step_size, - #params.z_step_size, + scan.z_step_size, + params.z_step_size, scan.dwell_time, params.dwell_time, scan.x_start, params.x_start, scan.y1_start, params.y1_start, - #scan.y2_start, - #params.y2_start, + scan.y2_start, + params.y2_start, scan.z1_start, params.z1_start, - #scan.z2_start, - #params.z2_start, + scan.z2_start, + params.z2_start, scan.position_counter, 0, ) diff --git a/src/dodal/devices/fast_grid_scan_2d.py b/src/dodal/devices/fast_grid_scan_2d.py new file mode 100644 index 0000000000..1989125bd1 --- /dev/null +++ b/src/dodal/devices/fast_grid_scan_2d.py @@ -0,0 +1,111 @@ +import threading + +from bluesky.plan_stubs import mv +from ophyd import ( + Component, + Device, + EpicsSignal, + EpicsSignalRO, + EpicsSignalWithRBV, + Kind, + Signal, +) +from ophyd.status import DeviceStatus, StatusBase + +from dodal.devices.fast_grid_scan_common import GridScanCompleteStatus, GridScanParams +from dodal.devices.status import await_value + + +class FastGridScan2D(Device): + x_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_NUM_STEPS") + y_steps: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_NUM_STEPS") + + x_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_STEP_SIZE") + y_step_size: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_STEP_SIZE") + + dwell_time: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "EXPOSURE_TIME") + + x_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "X_START") + y1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Y_START") + z1_start: EpicsSignalWithRBV = Component(EpicsSignalWithRBV, "Z_START") + + position_counter: EpicsSignal = Component( + EpicsSignal, "POS_COUNTER_RBV", write_pv="POS_COUNTER" + ) + x_counter: EpicsSignalRO = Component(EpicsSignalRO, "X_COUNTER") + y_counter: EpicsSignalRO = Component(EpicsSignalRO, "Y_COUNTER") + + # note: 2d scans on VMXm don't currently have a 'scan invalid' indication + # but this may be added in EPICS later. So fake one for now. + scan_invalid: Signal = Component(Signal, kind=Kind.hinted, value=0) + + run_cmd: EpicsSignal = Component(EpicsSignal, "RUN.PROC") + stop_cmd: EpicsSignal = Component(EpicsSignal, "STOP.PROC") + status: EpicsSignalRO = Component(EpicsSignalRO, "SCAN_STATUS_RBV") + + expected_images: Signal = Component(Signal) + + # Kickoff timeout in seconds + KICKOFF_TIMEOUT: float = 20.0 + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def set_expected_images(*_, **__): + x, y = self.x_steps.get(), self.y_steps.get() + self.expected_images.put(x * y) + + self.x_steps.subscribe(set_expected_images) + self.y_steps.subscribe(set_expected_images) + + def is_invalid(self) -> bool: + if "GONP" in self.scan_invalid.pvname: + return False + return self.scan_invalid.get() + + def kickoff(self) -> StatusBase: + # Check running already here? + st = DeviceStatus(device=self, timeout=self.KICKOFF_TIMEOUT) + + def scan(): + try: + self.run_cmd.put(1) + await_value(self.status, 1).wait() + st.set_finished() + except Exception as e: + st.set_exception(e) + + threading.Thread(target=scan, daemon=True).start() + return st + + def complete(self) -> DeviceStatus: + return GridScanCompleteStatus(self) + + def collect(self): + return {} + + def describe_collect(self): + return {} + + +def set_fast_grid_scan_params(scan: FastGridScan2D, params: GridScanParams): + yield from mv( + scan.x_steps, + params.x_steps, + scan.y_steps, + params.y_steps, + scan.x_step_size, + params.x_step_size, + scan.y_step_size, + params.y_step_size, + scan.dwell_time, + params.dwell_time, + scan.x_start, + params.x_start, + scan.y1_start, + params.y1_start, + scan.z1_start, + params.z1_start, + scan.position_counter, + 0, + ) diff --git a/src/dodal/devices/fast_grid_scan_common.py b/src/dodal/devices/fast_grid_scan_common.py new file mode 100644 index 0000000000..62d58a0c10 --- /dev/null +++ b/src/dodal/devices/fast_grid_scan_common.py @@ -0,0 +1,194 @@ +import time +from typing import Any + +import numpy as np +from numpy import ndarray +from ophyd.status import DeviceStatus +from pydantic import BaseModel, validator +from pydantic.dataclasses import dataclass + +from dodal.devices.motors import XYZLimitBundle +from dodal.parameters.experiment_parameter_base import AbstractExperimentParameterBase + + +@dataclass +class GridAxis: + start: float + step_size: float + full_steps: int + + def steps_to_motor_position(self, steps): + """Gives the motor position based on steps, where steps are 0 indexed""" + return self.start + self.step_size * steps + + @property + def end(self): + """Gives the point where the final frame is taken""" + # Note that full_steps is one indexed e.g. if there is one step then the end is + # refering to the first position + return self.steps_to_motor_position(self.full_steps - 1) + + def is_within(self, steps): + return 0 <= steps <= self.full_steps + + +class GridScanParams(BaseModel, AbstractExperimentParameterBase): + """ + Holder class for the parameters of a grid scan in a similar + layout to EPICS. + + Motion program will do a grid in x-y then rotate omega +90 and perform + a grid in x-z. + + The grid specified is where data is taken e.g. it can be assumed the first frame is + at x_start, y1_start, z1_start and subsequent frames are N*step_size away. + """ + + x_steps: int = 1 + y_steps: int = 1 + z_steps: int = 0 + x_step_size: float = 0.1 + y_step_size: float = 0.1 + z_step_size: float = 0.1 + dwell_time: float = 0.1 + x_start: float = 0.1 + y1_start: float = 0.1 + y2_start: float = 0.1 + z1_start: float = 0.1 + z2_start: float = 0.1 + x_axis: GridAxis = GridAxis(0, 0, 0) + y_axis: GridAxis = GridAxis(0, 0, 0) + z_axis: GridAxis = GridAxis(0, 0, 0) + + class Config: + arbitrary_types_allowed = True + fields = { + "x_axis": {"exclude": True}, + "y_axis": {"exclude": True}, + "z_axis": {"exclude": True}, + } + + @validator("x_axis", always=True) + def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis: + return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"]) + + @validator("y_axis", always=True) + def _get_y_axis(cls, y_axis: GridAxis, values: dict[str, Any]) -> GridAxis: + return GridAxis(values["y1_start"], values["y_step_size"], values["y_steps"]) + + @validator("z_axis", always=True) + def _get_z_axis(cls, z_axis: GridAxis, values: dict[str, Any]) -> GridAxis: + return GridAxis(values["z2_start"], values["z_step_size"], values["z_steps"]) + + def is_valid(self, limits: XYZLimitBundle) -> bool: + """ + Validates scan parameters + + :param limits: The motor limits against which to validate + the parameters + :return: True if the scan is valid + """ + x_in_limits = limits.x.is_within(self.x_axis.start) and limits.x.is_within( + self.x_axis.end + ) + y_in_limits = limits.y.is_within(self.y_axis.start) and limits.y.is_within( + self.y_axis.end + ) + + first_grid_in_limits = ( + x_in_limits and y_in_limits and limits.z.is_within(self.z1_start) + ) + + z_in_limits = limits.z.is_within(self.z_axis.start) and limits.z.is_within( + self.z_axis.end + ) + + second_grid_in_limits = ( + x_in_limits and z_in_limits and limits.y.is_within(self.y2_start) + ) + + return first_grid_in_limits and second_grid_in_limits + + def get_num_images(self): + return self.x_steps * self.y_steps + self.x_steps * self.z_steps + + @property + def is_3d_grid_scan(self): + return self.z_steps > 0 + + def grid_position_to_motor_position(self, grid_position: ndarray) -> ndarray: + """Converts a grid position, given as steps in the x, y, z grid, + to a real motor position. + + :param grid_position: The x, y, z position in grid steps + :return: The motor position this corresponds to. + :raises: IndexError if the desired position is outside the grid.""" + for position, axis in zip( + grid_position, [self.x_axis, self.y_axis, self.z_axis] + ): + if not axis.is_within(position): + raise IndexError(f"{grid_position} is outside the bounds of the grid") + + return np.array( + [ + self.x_axis.steps_to_motor_position(grid_position[0]), + self.y_axis.steps_to_motor_position(grid_position[1]), + self.z_axis.steps_to_motor_position(grid_position[2]), + ] + ) + + +class GridScanCompleteStatus(DeviceStatus): + """ + A Status for the grid scan completion + A special status object that notifies watchers (progress bars) + based on comparing device.expected_images to device.position_counter. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.start_ts = time.time() + + self.device.position_counter.subscribe(self._notify_watchers) + self.device.status.subscribe(self._running_changed) + + self._name = self.device.name + self._target_count = self.device.expected_images.get() + + def _notify_watchers(self, value, *args, **kwargs): + if not self._watchers: + return + time_elapsed = time.time() - self.start_ts + try: + fraction = 1 - value / self._target_count + except ZeroDivisionError: + fraction = 0 + time_remaining = 0 + except Exception as e: + fraction = None + time_remaining = None + self.set_exception(e) + self.clean_up() + else: + time_remaining = time_elapsed / fraction + for watcher in self._watchers: + watcher( + name=self._name, + current=value, + initial=0, + target=self._target_count, + unit="images", + precision=0, + fraction=fraction, + time_elapsed=time_elapsed, + time_remaining=time_remaining, + ) + + def _running_changed(self, value=None, old_value=None, **kwargs): + if (old_value == 1) and (value == 0): + self.set_finished() + self.clean_up() + + def clean_up(self): + self.device.position_counter.clear_sub(self._notify_watchers) + self.device.status.clear_sub(self._running_changed) diff --git a/src/dodal/devices/utils.py b/src/dodal/devices/utils.py index 74c85cdcf0..303d699d08 100644 --- a/src/dodal/devices/utils.py +++ b/src/dodal/devices/utils.py @@ -2,7 +2,7 @@ from typing import Callable, Optional from ophyd import Component, Device, EpicsSignal -from ophyd.status import AndStatus, Status, StatusBase +from ophyd.status import Status, StatusBase from dodal.log import LOGGER diff --git a/src/dodal/utils.py b/src/dodal/utils.py index 5f93ec5ac5..a391e193ff 100644 --- a/src/dodal/utils.py +++ b/src/dodal/utils.py @@ -224,6 +224,7 @@ def is_v1_device_type(obj: Type[Any]) -> bool: # Scientists refer to it as VMXm but $BEAMLINE env var is i02-1 BEAMLINE_NAME_TO_MODULE_NAME_OVERRIDES = { "i02-1": "vmxm", + "i02-2": "vmxi", } diff --git a/tests/devices/unit_tests/test_eiger.py b/tests/devices/unit_tests/test_eiger.py index 97dad19fd4..d69a9c31b6 100644 --- a/tests/devices/unit_tests/test_eiger.py +++ b/tests/devices/unit_tests/test_eiger.py @@ -207,7 +207,7 @@ def test_stage_raises_exception_if_odin_initialisation_status_not_ok(fake_eiger) @pytest.mark.parametrize( - "roi_mode, expected_num_change_roi_calls", [(True, 1), (False, 0)] + "roi_mode, expected_num_change_roi_calls", [(True, 1), (False, 1)] ) @patch("dodal.devices.eiger.await_value") def test_stage_enables_roi_mode_correctly( From 39084723b73adec1df9199b9fe601d68242eeb3d Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Tue, 7 Nov 2023 17:03:59 +0000 Subject: [PATCH 09/11] Correct merge --- src/dodal/beamlines/vmxm.py | 18 ++- src/dodal/devices/detector.py | 4 +- src/dodal/devices/fast_grid_scan.py | 187 ---------------------------- 3 files changed, 17 insertions(+), 192 deletions(-) diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index 7cb9114e6b..ad9b7e447d 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -1,6 +1,9 @@ +from typing import Optional + from dodal.beamlines.beamline_utils import device_instantiation from dodal.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.devices.backlight import VmxmBacklight +from dodal.devices.detector import DetectorParams from dodal.devices.eiger import EigerDetector from dodal.devices.fast_grid_scan_2d import FastGridScan2D from dodal.devices.synchrotron import Synchrotron @@ -20,17 +23,26 @@ @skip_device(lambda: BL == SIM_BEAMLINE_NAME) def eiger( - wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False + wait_for_connection: bool = True, + fake_with_ophyd_sim: bool = False, + params: Optional[DetectorParams] = None, ) -> EigerDetector: - """Get the vmxm eiger device, instantiate it if it hasn't already been. - If this is called when already instantiated in vmxm, it will return the existing object. + """Get the i24 Eiger device, instantiate it if it hasn't already been. + If this is called when already instantiated, it will return the existing object. + If called with params, will update those params to the Eiger object. """ + + def set_params(eiger: EigerDetector): + if params is not None: + eiger.set_detector_parameters(params) + return device_instantiation( device_factory=EigerDetector, name="eiger", prefix="-EA-EIGER-01:", wait=wait_for_connection, fake=fake_with_ophyd_sim, + post_create=set_params, ) diff --git a/src/dodal/devices/detector.py b/src/dodal/devices/detector.py index 4a9a51d959..b6c10b355a 100644 --- a/src/dodal/devices/detector.py +++ b/src/dodal/devices/detector.py @@ -4,7 +4,7 @@ from pydantic import BaseModel, validator from dodal.devices.det_dim_constants import ( - EIGER2_X_9M_SIZE, + EIGER2_X_16M_SIZE, DetectorSize, DetectorSizeConstants, constants_from_type, @@ -40,7 +40,7 @@ class DetectorParams(BaseModel): use_roi_mode: bool det_dist_to_beam_converter_path: str trigger_mode: TriggerMode = TriggerMode.SET_FRAMES - detector_size_constants: DetectorSizeConstants = EIGER2_X_9M_SIZE # This looks like it's always using the default and not taken from the json + detector_size_constants: DetectorSizeConstants = EIGER2_X_16M_SIZE beam_xy_converter: DetectorDistanceToBeamXYConverter = None class Config: diff --git a/src/dodal/devices/fast_grid_scan.py b/src/dodal/devices/fast_grid_scan.py index a4809ed5dd..9b506dd009 100644 --- a/src/dodal/devices/fast_grid_scan.py +++ b/src/dodal/devices/fast_grid_scan.py @@ -13,193 +13,6 @@ from dodal.devices.fast_grid_scan_common import GridScanCompleteStatus, GridScanParams from dodal.devices.status import await_value -<<<<<<< HEAD -======= -from dodal.parameters.experiment_parameter_base import AbstractExperimentParameterBase - - -@dataclass -class GridAxis: - start: float - step_size: float - full_steps: int - - def steps_to_motor_position(self, steps): - """Gives the motor position based on steps, where steps are 0 indexed""" - return self.start + self.step_size * steps - - @property - def end(self): - """Gives the point where the final frame is taken""" - # Note that full_steps is one indexed e.g. if there is one step then the end is - # refering to the first position - return self.steps_to_motor_position(self.full_steps - 1) - - def is_within(self, steps): - return 0 <= steps <= self.full_steps - - -class GridScanParams(BaseModel, AbstractExperimentParameterBase): - """ - Holder class for the parameters of a grid scan in a similar - layout to EPICS. - - Motion program will do a grid in x-y then rotate omega +90 and perform - a grid in x-z. - - The grid specified is where data is taken e.g. it can be assumed the first frame is - at x_start, y1_start, z1_start and subsequent frames are N*step_size away. - """ - - x_steps: int = 1 - y_steps: int = 1 - z_steps: int = 0 - x_step_size: float = 0.1 - y_step_size: float = 0.1 - z_step_size: float = 0.1 - dwell_time_ms: float = 0.1 - x_start: float = 0.1 - y1_start: float = 0.1 - y2_start: float = 0.1 - z1_start: float = 0.1 - z2_start: float = 0.1 - x_axis: GridAxis = GridAxis(0, 0, 0) - y_axis: GridAxis = GridAxis(0, 0, 0) - z_axis: GridAxis = GridAxis(0, 0, 0) - - class Config: - arbitrary_types_allowed = True - fields = { - "x_axis": {"exclude": True}, - "y_axis": {"exclude": True}, - "z_axis": {"exclude": True}, - } - - @validator("x_axis", always=True) - def _get_x_axis(cls, x_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["x_start"], values["x_step_size"], values["x_steps"]) - - @validator("y_axis", always=True) - def _get_y_axis(cls, y_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["y1_start"], values["y_step_size"], values["y_steps"]) - - @validator("z_axis", always=True) - def _get_z_axis(cls, z_axis: GridAxis, values: dict[str, Any]) -> GridAxis: - return GridAxis(values["z2_start"], values["z_step_size"], values["z_steps"]) - - def is_valid(self, limits: XYZLimitBundle) -> bool: - """ - Validates scan parameters - - :param limits: The motor limits against which to validate - the parameters - :return: True if the scan is valid - """ - x_in_limits = limits.x.is_within(self.x_axis.start) and limits.x.is_within( - self.x_axis.end - ) - y_in_limits = limits.y.is_within(self.y_axis.start) and limits.y.is_within( - self.y_axis.end - ) - - first_grid_in_limits = ( - x_in_limits and y_in_limits and limits.z.is_within(self.z1_start) - ) - - z_in_limits = limits.z.is_within(self.z_axis.start) and limits.z.is_within( - self.z_axis.end - ) - - second_grid_in_limits = ( - x_in_limits and z_in_limits and limits.y.is_within(self.y2_start) - ) - - return first_grid_in_limits and second_grid_in_limits - - def get_num_images(self): - return self.x_steps * self.y_steps + self.x_steps * self.z_steps - - @property - def is_3d_grid_scan(self): - return self.z_steps > 0 - - def grid_position_to_motor_position(self, grid_position: ndarray) -> ndarray: - """Converts a grid position, given as steps in the x, y, z grid, - to a real motor position. - - :param grid_position: The x, y, z position in grid steps - :return: The motor position this corresponds to. - :raises: IndexError if the desired position is outside the grid.""" - for position, axis in zip( - grid_position, [self.x_axis, self.y_axis, self.z_axis] - ): - if not axis.is_within(position): - raise IndexError(f"{grid_position} is outside the bounds of the grid") - - return np.array( - [ - self.x_axis.steps_to_motor_position(grid_position[0]), - self.y_axis.steps_to_motor_position(grid_position[1]), - self.z_axis.steps_to_motor_position(grid_position[2]), - ] - ) - - -class GridScanCompleteStatus(DeviceStatus): - """ - A Status for the grid scan completion - A special status object that notifies watchers (progress bars) - based on comparing device.expected_images to device.position_counter. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.start_ts = time.time() - - self.device.position_counter.subscribe(self._notify_watchers) - self.device.status.subscribe(self._running_changed) - - self._name = self.device.name - self._target_count = self.device.expected_images.get() - - def _notify_watchers(self, value, *args, **kwargs): - if not self._watchers: - return - time_elapsed = time.time() - self.start_ts - try: - fraction = 1 - value / self._target_count - except ZeroDivisionError: - fraction = 0 - time_remaining = 0 - except Exception as e: - fraction = None - time_remaining = None - self.set_exception(e) - self.clean_up() - else: - time_remaining = time_elapsed / fraction - for watcher in self._watchers: - watcher( - name=self._name, - current=value, - initial=0, - target=self._target_count, - unit="images", - precision=0, - fraction=fraction, - time_elapsed=time_elapsed, - time_remaining=time_remaining, - ) - - def _running_changed(self, value=None, old_value=None, **kwargs): - if (old_value == 1) and (value == 0): - self.set_finished() - self.clean_up() - - def clean_up(self): - self.device.position_counter.clear_sub(self._notify_watchers) - self.device.status.clear_sub(self._running_changed) ->>>>>>> origin/main class FastGridScan(Device): From a8696682bdfffac8a0373b69fd4e3aa2f860599e Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Fri, 17 Nov 2023 09:30:35 +0000 Subject: [PATCH 10/11] Add VMXm samp motors --- src/dodal/beamlines/vmxm.py | 18 ++++++++++++++++++ src/dodal/devices/vmxm/vmxm_sample_motors.py | 7 +++++++ 2 files changed, 25 insertions(+) create mode 100644 src/dodal/devices/vmxm/vmxm_sample_motors.py diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index ad9b7e447d..7462a149df 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -8,6 +8,7 @@ from dodal.devices.fast_grid_scan_2d import FastGridScan2D 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 from dodal.log import set_beamline as set_log_beamline from dodal.utils import get_beamline_name, skip_device @@ -123,3 +124,20 @@ def synchrotron( fake_with_ophyd_sim, bl_prefix=False, ) + + +@skip_device(lambda: BL == SIM_BEAMLINE_NAME) +def sample_motors( + wait_for_connection: bool = True, fake_with_ophyd_sim: bool = False +) -> VmxmSampleMotors: + """Get the VMXm sample_motors device, instantiate it if it hasn't already been. + If this is called when already instantiated in VMXm, it will return the existing object. + """ + return device_instantiation( + VmxmSampleMotors, + "sample_motors", + "-MO-SAMP-01:", + wait_for_connection, + fake_with_ophyd_sim, + bl_prefix=False, + ) diff --git a/src/dodal/devices/vmxm/vmxm_sample_motors.py b/src/dodal/devices/vmxm/vmxm_sample_motors.py new file mode 100644 index 0000000000..17d769cc4b --- /dev/null +++ b/src/dodal/devices/vmxm/vmxm_sample_motors.py @@ -0,0 +1,7 @@ +from ophyd import Component as Cpt +from ophyd import EpicsMotor +from ophyd.epics_motor import MotorBundle + + +class VmxmSampleMotors(MotorBundle): + omega: EpicsMotor = Cpt(EpicsMotor, "OMEGA") From 76c639100b06f89be82089708becc11de499a9e2 Mon Sep 17 00:00:00 2001 From: Tom Willemsen Date: Fri, 24 Nov 2023 10:27:03 +0000 Subject: [PATCH 11/11] Changes from VMXm beamtime --- src/dodal/beamlines/vmxm.py | 1 - src/dodal/devices/eiger.py | 12 ++++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/dodal/beamlines/vmxm.py b/src/dodal/beamlines/vmxm.py index 7462a149df..5d6cb3f17d 100644 --- a/src/dodal/beamlines/vmxm.py +++ b/src/dodal/beamlines/vmxm.py @@ -139,5 +139,4 @@ def sample_motors( "-MO-SAMP-01:", wait_for_connection, fake_with_ophyd_sim, - bl_prefix=False, ) diff --git a/src/dodal/devices/eiger.py b/src/dodal/devices/eiger.py index e2a6f644a2..3e42c3a934 100644 --- a/src/dodal/devices/eiger.py +++ b/src/dodal/devices/eiger.py @@ -1,4 +1,5 @@ from enum import Enum +from time import sleep from typing import Optional from ophyd import Component, Device, EpicsSignalRO, Signal @@ -35,7 +36,9 @@ def set(self, value, *, timeout=None, settle_time=None, **kwargs): STALE_PARAMS_TIMEOUT = 60 GENERAL_STATUS_TIMEOUT = 10 - ALL_FRAMES_TIMEOUT = 120 + ALL_FRAMES_TIMEOUT = ( + 600 # FIXME: VMXm hack - we need to get this passed in a a param probably? + ) ARMING_TIMEOUT = 60 filewriters_finished: SubscriptionStatus @@ -290,6 +293,9 @@ def set_num_triggers_and_captures(self) -> Status: def _wait_for_odin_status(self) -> Status: self.forward_bit_depth_to_filewriter() + sleep( + 2 + ) # FIXME - VMXm hack - on VMXm ODIN will *occasionally* fail to start without this sleep. Need a better solution. status = self.odin.file_writer.capture.set( 1, timeout=self.GENERAL_STATUS_TIMEOUT ) @@ -310,7 +316,9 @@ def _finish_arm(self) -> Status: def forward_bit_depth_to_filewriter(self): bit_depth = self.bit_depth.get() - self.odin.file_writer.data_type.put(f"UInt{bit_depth}") + self.odin.file_writer.data_type.set(f"UInt{bit_depth}").wait( + self.GENERAL_STATUS_TIMEOUT + ) def disarm_detector(self): self.cam.acquire.put(0)