From 25713390c3b60e4b40cb9155c781c4657b35192b Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 11:24:37 +0000 Subject: [PATCH 01/18] electro magnet devices --- src/dodal/beamlines/i10_1.py | 68 ++++++++++++++++++- src/dodal/devices/i10_1/__init__.py | 11 +++ src/dodal/devices/i10_1/current_amp.py | 24 +++++++ .../devices/i10_1/electromagnet/__init__.py | 0 .../i10_1/electromagnet/current_amp.py | 33 +++++++++ .../devices/i10_1/electromagnet/magnet.py | 16 +++++ .../i10_1/electromagnet/scaler_cards.py | 12 ++++ .../devices/i10_1/electromagnet/stages.py | 14 ++++ 8 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 src/dodal/devices/i10_1/__init__.py create mode 100644 src/dodal/devices/i10_1/current_amp.py create mode 100644 src/dodal/devices/i10_1/electromagnet/__init__.py create mode 100644 src/dodal/devices/i10_1/electromagnet/current_amp.py create mode 100644 src/dodal/devices/i10_1/electromagnet/magnet.py create mode 100644 src/dodal/devices/i10_1/electromagnet/scaler_cards.py create mode 100644 src/dodal/devices/i10_1/electromagnet/stages.py diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index 72be2ae75ba..243e2186979 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -1,7 +1,14 @@ from dodal.beamlines.i10_shared import devices as i10_shared_devices from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager +from dodal.devices.current_amplifiers.current_amplifier_detector import CurrentAmpDet from dodal.devices.i10 import I10JDiagnostic, I10JSlits, PiezoMirror +from dodal.devices.i10_1 import ( + I10JSR570, + ElectromagnetMagnetField, + ElectromagnetScalerCard1, + ElectromagnetSR570, +) from dodal.devices.temperture_controller.lakeshore.lakeshore import Lakeshore336 from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -13,11 +20,13 @@ devices = DeviceManager() devices.include(i10_shared_devices) +"""I10-1 J Beamline Devices""" + @devices.factory() -def em_temperature_controller() -> Lakeshore336: - return Lakeshore336( - prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-41:", +def mirror6_sr570() -> I10JSR570: + return I10JSR570( + prefix=f"{PREFIX.beamline_prefix}-DI-IAMP", ) @@ -36,3 +45,56 @@ def diagnostic() -> I10JDiagnostic: @devices.factory() def focusing_mirror() -> PiezoMirror: return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-FOCA-01:") + + +"""I10-1 Electromagnet Devices""" + + +@devices.factory() +def electromagnet_field() -> ElectromagnetMagnetField: + return ElectromagnetMagnetField( + prefix=f"{PREFIX.beamline_prefix}-EA-MAGC-01:", + ) + + +@devices.factory() +def electromagnet_scaler_card() -> ElectromagnetScalerCard1: + return ElectromagnetScalerCard1( + prefix=f"{PREFIX.beamline_prefix}-EA-SCLR-02:SCALERJ3", + ) + + +@devices.factory() +def electromagnet_sr570() -> ElectromagnetSR570: + return ElectromagnetSR570( + prefix=f"{PREFIX.beamline_prefix}-DI-IAMP", + ) + + +@devices.factory() +def electromagnet_sr570_scaler_tey( + electromagnet_sr570: ElectromagnetSR570, + electromagnet_scaler_card: ElectromagnetScalerCard1, +) -> CurrentAmpDet: + return CurrentAmpDet( + current_amp=electromagnet_sr570.ca1, + counter=electromagnet_scaler_card.tey, + ) + + +@devices.factory() +def electromagnet_sr570_scaler_fy( + electromagnet_sr570: ElectromagnetSR570, + electromagnet_scaler_card: ElectromagnetScalerCard1, +) -> CurrentAmpDet: + return CurrentAmpDet( + current_amp=electromagnet_sr570.ca2, + counter=electromagnet_scaler_card.fy, + ) + + +@devices.factory() +def em_temperature_controller() -> Lakeshore336: + return Lakeshore336( + prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-41:", + ) diff --git a/src/dodal/devices/i10_1/__init__.py b/src/dodal/devices/i10_1/__init__.py new file mode 100644 index 00000000000..befdbca617d --- /dev/null +++ b/src/dodal/devices/i10_1/__init__.py @@ -0,0 +1,11 @@ +from .current_amp import I10JSR570 +from .electromagnet.current_amp import ElectromagnetSR570 +from .electromagnet.magnet import ElectromagnetMagnetField +from .electromagnet.scaler_cards import ElectromagnetScalerCard1 + +__all__ = [ + "I10JSR570", + "ElectromagnetSR570", + "ElectromagnetMagnetField", + "ElectromagnetScalerCard1", +] diff --git a/src/dodal/devices/i10_1/current_amp.py b/src/dodal/devices/i10_1/current_amp.py new file mode 100644 index 00000000000..c9b23060954 --- /dev/null +++ b/src/dodal/devices/i10_1/current_amp.py @@ -0,0 +1,24 @@ +from ophyd_async.core import Device + +from dodal.devices.current_amplifiers import ( + SR570, + SR570FineGainTable, + SR570FullGainTable, + SR570GainTable, + SR570GainToCurrentTable, + SR570RaiseTimeTable, +) + + +class I10JSR570(Device): + def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None: + self.ca1 = SR570( + prefix + "-07:", + suffix=suffix, + fine_gain_table=SR570FineGainTable, + coarse_gain_table=SR570GainTable, + combined_table=SR570FullGainTable, + gain_to_current_table=SR570GainToCurrentTable, + raise_timetable=SR570RaiseTimeTable, + ) + super().__init__(name) diff --git a/src/dodal/devices/i10_1/electromagnet/__init__.py b/src/dodal/devices/i10_1/electromagnet/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/dodal/devices/i10_1/electromagnet/current_amp.py b/src/dodal/devices/i10_1/electromagnet/current_amp.py new file mode 100644 index 00000000000..e93f2794079 --- /dev/null +++ b/src/dodal/devices/i10_1/electromagnet/current_amp.py @@ -0,0 +1,33 @@ +from ophyd_async.core import Device + +from dodal.devices.current_amplifiers import ( + SR570, + SR570FineGainTable, + SR570FullGainTable, + SR570GainTable, + SR570GainToCurrentTable, + SR570RaiseTimeTable, +) + + +class ElectromagnetSR570(Device): + def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None: + self.ca1 = SR570( + prefix + "-08:", + suffix=suffix, + fine_gain_table=SR570FineGainTable, + coarse_gain_table=SR570GainTable, + combined_table=SR570FullGainTable, + gain_to_current_table=SR570GainToCurrentTable, + raise_timetable=SR570RaiseTimeTable, + ) + self.ca2 = SR570( + prefix + "-09:", + suffix=suffix, + fine_gain_table=SR570FineGainTable, + coarse_gain_table=SR570GainTable, + combined_table=SR570FullGainTable, + gain_to_current_table=SR570GainToCurrentTable, + raise_timetable=SR570RaiseTimeTable, + ) + super().__init__(name) diff --git a/src/dodal/devices/i10_1/electromagnet/magnet.py b/src/dodal/devices/i10_1/electromagnet/magnet.py new file mode 100644 index 00000000000..2ce31f84f9f --- /dev/null +++ b/src/dodal/devices/i10_1/electromagnet/magnet.py @@ -0,0 +1,16 @@ +from ophyd_async.core import StandardReadable +from ophyd_async.epics.core import epics_signal_rw + + +class ElectromagnetMagnetField(StandardReadable): + def __init__( + self, + prefix: str, + name: str = "", + ): + with self.add_children_as_readables(): + self.field = epics_signal_rw( + float, write_pv=prefix + "FIELD", read_pv=prefix + "FIELD:RBV" + ) + self.current = epics_signal_rw(float, prefix + "DMD") + super().__init__(name=name) diff --git a/src/dodal/devices/i10_1/electromagnet/scaler_cards.py b/src/dodal/devices/i10_1/electromagnet/scaler_cards.py new file mode 100644 index 00000000000..ef3cf53e990 --- /dev/null +++ b/src/dodal/devices/i10_1/electromagnet/scaler_cards.py @@ -0,0 +1,12 @@ +from ophyd_async.core import Device + +from dodal.devices.current_amplifiers import StruckScaler + + +class ElectromagnetScalerCard1(Device): + def __init__(self, prefix, name: str = "") -> None: + self.mon = StruckScaler(prefix=prefix, suffix=".S17") + self.tey = StruckScaler(prefix=prefix, suffix=".S18") + self.fy = StruckScaler(prefix=prefix, suffix=".S19") + self.fy2 = StruckScaler(prefix=prefix, suffix=".S20") + super().__init__(name) diff --git a/src/dodal/devices/i10_1/electromagnet/stages.py b/src/dodal/devices/i10_1/electromagnet/stages.py new file mode 100644 index 00000000000..a8c66b12909 --- /dev/null +++ b/src/dodal/devices/i10_1/electromagnet/stages.py @@ -0,0 +1,14 @@ +from ophyd_async.core import StandardReadable +from ophyd_async.epics.motor import Motor + + +class ElectromagnetStage(StandardReadable): + def __init__( + self, + prefix: str, + name: str = "", + ): + with self.add_children_as_readables(): + self.y = Motor(prefix + "Y") + self.pitch = Motor(prefix + "PITCH") + super().__init__(name=name) From 3af23258a4c1d45a1b83a59a05f8355a70ed5272 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 13:22:45 +0000 Subject: [PATCH 02/18] add em stage --- src/dodal/beamlines/i10_1.py | 8 ++++++++ src/dodal/devices/i10_1/__init__.py | 2 ++ 2 files changed, 10 insertions(+) diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index 243e2186979..4c4b9c8e734 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -8,6 +8,7 @@ ElectromagnetMagnetField, ElectromagnetScalerCard1, ElectromagnetSR570, + ElectromagnetStage, ) from dodal.devices.temperture_controller.lakeshore.lakeshore import Lakeshore336 from dodal.log import set_beamline as set_log_beamline @@ -98,3 +99,10 @@ def em_temperature_controller() -> Lakeshore336: return Lakeshore336( prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-41:", ) + + +@devices.factory() +def electromagnet_stage() -> ElectromagnetStage: + return ElectromagnetStage( + prefix=f"{PREFIX.beamline_prefix}-MO-CRYO-01:", + ) diff --git a/src/dodal/devices/i10_1/__init__.py b/src/dodal/devices/i10_1/__init__.py index befdbca617d..4eec3d3594c 100644 --- a/src/dodal/devices/i10_1/__init__.py +++ b/src/dodal/devices/i10_1/__init__.py @@ -2,10 +2,12 @@ from .electromagnet.current_amp import ElectromagnetSR570 from .electromagnet.magnet import ElectromagnetMagnetField from .electromagnet.scaler_cards import ElectromagnetScalerCard1 +from .electromagnet.stages import ElectromagnetStage __all__ = [ "I10JSR570", "ElectromagnetSR570", "ElectromagnetMagnetField", "ElectromagnetScalerCard1", + "ElectromagnetStage", ] From fbf8b57fdaeb65cd0b026dd757c4146e0895c94d Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 13:26:11 +0000 Subject: [PATCH 03/18] fixes docstring --- src/dodal/beamlines/i10_1.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index 4c4b9c8e734..cc7c34f85c2 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -21,7 +21,7 @@ devices = DeviceManager() devices.include(i10_shared_devices) -"""I10-1 J Beamline Devices""" +"""I10J Beamline Devices""" @devices.factory() @@ -48,7 +48,7 @@ def focusing_mirror() -> PiezoMirror: return PiezoMirror(prefix=f"{PREFIX.beamline_prefix}-OP-FOCA-01:") -"""I10-1 Electromagnet Devices""" +"""I10J Electromagnet Devices""" @devices.factory() @@ -58,6 +58,16 @@ def electromagnet_field() -> ElectromagnetMagnetField: ) +@devices.factory() +def electromagnet_stage() -> ElectromagnetStage: + return ElectromagnetStage( + prefix=f"{PREFIX.beamline_prefix}-MO-CRYO-01:", + ) + + +"""I10J Electromagnet Measurement Devices""" + + @devices.factory() def electromagnet_scaler_card() -> ElectromagnetScalerCard1: return ElectromagnetScalerCard1( @@ -99,10 +109,3 @@ def em_temperature_controller() -> Lakeshore336: return Lakeshore336( prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-41:", ) - - -@devices.factory() -def electromagnet_stage() -> ElectromagnetStage: - return ElectromagnetStage( - prefix=f"{PREFIX.beamline_prefix}-MO-CRYO-01:", - ) From 05025b53ce56c0b01160484e89a78bd8dcff841d Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 13:30:14 +0000 Subject: [PATCH 04/18] add auto detector for monitor --- src/dodal/beamlines/i10_1.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index cc7c34f85c2..ee849c94fe3 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -82,6 +82,16 @@ def electromagnet_sr570() -> ElectromagnetSR570: ) +@devices.factory() +def electromagnet_sr570_scaler_monitor( + mirror6_sr570: I10JSR570, + electromagnet_scaler_card: ElectromagnetScalerCard1, +) -> CurrentAmpDet: + return CurrentAmpDet( + current_amp=mirror6_sr570.ca1, counter=electromagnet_scaler_card.mon + ) + + @devices.factory() def electromagnet_sr570_scaler_tey( electromagnet_sr570: ElectromagnetSR570, From 25a165b01c114ddf69737814b1cf6cc79cb82337 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 13:39:16 +0000 Subject: [PATCH 05/18] add default table to base class --- src/dodal/beamlines/i10_1.py | 13 ++++------ src/dodal/devices/current_amplifiers/sr570.py | 12 ++++----- .../devices/i10/rasor/rasor_current_amp.py | 25 +------------------ src/dodal/devices/i10_1/__init__.py | 2 -- src/dodal/devices/i10_1/current_amp.py | 24 ------------------ 5 files changed, 12 insertions(+), 64 deletions(-) delete mode 100644 src/dodal/devices/i10_1/current_amp.py diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index ee849c94fe3..25c0861efc4 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -1,10 +1,9 @@ from dodal.beamlines.i10_shared import devices as i10_shared_devices from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline from dodal.device_manager import DeviceManager -from dodal.devices.current_amplifiers.current_amplifier_detector import CurrentAmpDet +from dodal.devices.current_amplifiers import SR570, CurrentAmpDet from dodal.devices.i10 import I10JDiagnostic, I10JSlits, PiezoMirror from dodal.devices.i10_1 import ( - I10JSR570, ElectromagnetMagnetField, ElectromagnetScalerCard1, ElectromagnetSR570, @@ -25,10 +24,8 @@ @devices.factory() -def mirror6_sr570() -> I10JSR570: - return I10JSR570( - prefix=f"{PREFIX.beamline_prefix}-DI-IAMP", - ) +def mirror6_sr570() -> SR570: + return SR570(prefix=f"{PREFIX.beamline_prefix}-DI-IAMP-07:") @devices.factory() @@ -84,11 +81,11 @@ def electromagnet_sr570() -> ElectromagnetSR570: @devices.factory() def electromagnet_sr570_scaler_monitor( - mirror6_sr570: I10JSR570, + mirror6_sr570: SR570, electromagnet_scaler_card: ElectromagnetScalerCard1, ) -> CurrentAmpDet: return CurrentAmpDet( - current_amp=mirror6_sr570.ca1, counter=electromagnet_scaler_card.mon + current_amp=mirror6_sr570, counter=electromagnet_scaler_card.mon ) diff --git a/src/dodal/devices/current_amplifiers/sr570.py b/src/dodal/devices/current_amplifiers/sr570.py index 73672ac59bc..fd0db0cdebd 100644 --- a/src/dodal/devices/current_amplifiers/sr570.py +++ b/src/dodal/devices/current_amplifiers/sr570.py @@ -135,12 +135,12 @@ class SR570(CurrentAmp): def __init__( self, prefix: str, - suffix: str, - fine_gain_table: type[StrictEnum], - coarse_gain_table: type[StrictEnum], - combined_table: type[Enum], - gain_to_current_table: type[Enum], - raise_timetable: type[Enum], + suffix: str = "SENS:SEL", + fine_gain_table: type[StrictEnum] = SR570FineGainTable, + coarse_gain_table: type[StrictEnum] = SR570GainTable, + combined_table: type[Enum] = SR570FullGainTable, + gain_to_current_table: type[Enum] = SR570GainToCurrentTable, + raise_timetable: type[Enum] = SR570RaiseTimeTable, upperlimit: float = 4.8, lowerlimit: float = 0.4, timeout: float = 1, diff --git a/src/dodal/devices/i10/rasor/rasor_current_amp.py b/src/dodal/devices/i10/rasor/rasor_current_amp.py index 7179a103d89..1d592e24dd0 100644 --- a/src/dodal/devices/i10/rasor/rasor_current_amp.py +++ b/src/dodal/devices/i10/rasor/rasor_current_amp.py @@ -6,11 +6,6 @@ Femto3xxGainToCurrentTable, Femto3xxRaiseTime, FemtoDDPCA, - SR570FineGainTable, - SR570FullGainTable, - SR570GainTable, - SR570GainToCurrentTable, - SR570RaiseTimeTable, ) @@ -41,32 +36,14 @@ def __init__(self, prefix: str, suffix: str = "GAIN", name: str = "") -> None: class RasorSR570(Device): - def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None: + def __init__(self, prefix: str, name: str = "") -> None: self.ca1 = SR570( prefix + "-04:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, ) self.ca2 = SR570( prefix + "-05:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, ) self.ca3 = SR570( prefix + "-06:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, ) super().__init__(name) diff --git a/src/dodal/devices/i10_1/__init__.py b/src/dodal/devices/i10_1/__init__.py index 4eec3d3594c..3f6b8f864ea 100644 --- a/src/dodal/devices/i10_1/__init__.py +++ b/src/dodal/devices/i10_1/__init__.py @@ -1,11 +1,9 @@ -from .current_amp import I10JSR570 from .electromagnet.current_amp import ElectromagnetSR570 from .electromagnet.magnet import ElectromagnetMagnetField from .electromagnet.scaler_cards import ElectromagnetScalerCard1 from .electromagnet.stages import ElectromagnetStage __all__ = [ - "I10JSR570", "ElectromagnetSR570", "ElectromagnetMagnetField", "ElectromagnetScalerCard1", diff --git a/src/dodal/devices/i10_1/current_amp.py b/src/dodal/devices/i10_1/current_amp.py deleted file mode 100644 index c9b23060954..00000000000 --- a/src/dodal/devices/i10_1/current_amp.py +++ /dev/null @@ -1,24 +0,0 @@ -from ophyd_async.core import Device - -from dodal.devices.current_amplifiers import ( - SR570, - SR570FineGainTable, - SR570FullGainTable, - SR570GainTable, - SR570GainToCurrentTable, - SR570RaiseTimeTable, -) - - -class I10JSR570(Device): - def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None: - self.ca1 = SR570( - prefix + "-07:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, - ) - super().__init__(name) From 44267fd0f9100c2ae70852cc9db4bfbea04f8cfd Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 13:44:21 +0000 Subject: [PATCH 06/18] clean up 10j SR570 --- .../i10_1/electromagnet/current_amp.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/dodal/devices/i10_1/electromagnet/current_amp.py b/src/dodal/devices/i10_1/electromagnet/current_amp.py index e93f2794079..39f433ec5bd 100644 --- a/src/dodal/devices/i10_1/electromagnet/current_amp.py +++ b/src/dodal/devices/i10_1/electromagnet/current_amp.py @@ -2,32 +2,15 @@ from dodal.devices.current_amplifiers import ( SR570, - SR570FineGainTable, - SR570FullGainTable, - SR570GainTable, - SR570GainToCurrentTable, - SR570RaiseTimeTable, ) class ElectromagnetSR570(Device): - def __init__(self, prefix: str, suffix: str = "SENS:SEL", name: str = "") -> None: + def __init__(self, prefix: str, name: str = "") -> None: self.ca1 = SR570( prefix + "-08:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, ) self.ca2 = SR570( prefix + "-09:", - suffix=suffix, - fine_gain_table=SR570FineGainTable, - coarse_gain_table=SR570GainTable, - combined_table=SR570FullGainTable, - gain_to_current_table=SR570GainToCurrentTable, - raise_timetable=SR570RaiseTimeTable, ) super().__init__(name) From 6e088912c3c38e7494e80b2163c9bb9c5c2f4bb7 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 15:18:49 +0000 Subject: [PATCH 07/18] move scaler_cards out of em as high field magnet has the same format. --- src/dodal/beamlines/i10_1.py | 12 ++++++------ src/dodal/devices/i10_1/__init__.py | 4 ++-- .../i10_1/{electromagnet => }/scaler_cards.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) rename src/dodal/devices/i10_1/{electromagnet => }/scaler_cards.py (91%) diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index 25c0861efc4..c427180387c 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -5,9 +5,9 @@ from dodal.devices.i10 import I10JDiagnostic, I10JSlits, PiezoMirror from dodal.devices.i10_1 import ( ElectromagnetMagnetField, - ElectromagnetScalerCard1, ElectromagnetSR570, ElectromagnetStage, + I10JScalerCard, ) from dodal.devices.temperture_controller.lakeshore.lakeshore import Lakeshore336 from dodal.log import set_beamline as set_log_beamline @@ -66,8 +66,8 @@ def electromagnet_stage() -> ElectromagnetStage: @devices.factory() -def electromagnet_scaler_card() -> ElectromagnetScalerCard1: - return ElectromagnetScalerCard1( +def electromagnet_scaler_card() -> I10JScalerCard: + return I10JScalerCard( prefix=f"{PREFIX.beamline_prefix}-EA-SCLR-02:SCALERJ3", ) @@ -82,7 +82,7 @@ def electromagnet_sr570() -> ElectromagnetSR570: @devices.factory() def electromagnet_sr570_scaler_monitor( mirror6_sr570: SR570, - electromagnet_scaler_card: ElectromagnetScalerCard1, + electromagnet_scaler_card: I10JScalerCard, ) -> CurrentAmpDet: return CurrentAmpDet( current_amp=mirror6_sr570, counter=electromagnet_scaler_card.mon @@ -92,7 +92,7 @@ def electromagnet_sr570_scaler_monitor( @devices.factory() def electromagnet_sr570_scaler_tey( electromagnet_sr570: ElectromagnetSR570, - electromagnet_scaler_card: ElectromagnetScalerCard1, + electromagnet_scaler_card: I10JScalerCard, ) -> CurrentAmpDet: return CurrentAmpDet( current_amp=electromagnet_sr570.ca1, @@ -103,7 +103,7 @@ def electromagnet_sr570_scaler_tey( @devices.factory() def electromagnet_sr570_scaler_fy( electromagnet_sr570: ElectromagnetSR570, - electromagnet_scaler_card: ElectromagnetScalerCard1, + electromagnet_scaler_card: I10JScalerCard, ) -> CurrentAmpDet: return CurrentAmpDet( current_amp=electromagnet_sr570.ca2, diff --git a/src/dodal/devices/i10_1/__init__.py b/src/dodal/devices/i10_1/__init__.py index 3f6b8f864ea..c00fcca7fa5 100644 --- a/src/dodal/devices/i10_1/__init__.py +++ b/src/dodal/devices/i10_1/__init__.py @@ -1,11 +1,11 @@ from .electromagnet.current_amp import ElectromagnetSR570 from .electromagnet.magnet import ElectromagnetMagnetField -from .electromagnet.scaler_cards import ElectromagnetScalerCard1 from .electromagnet.stages import ElectromagnetStage +from .scaler_cards import I10JScalerCard __all__ = [ "ElectromagnetSR570", "ElectromagnetMagnetField", - "ElectromagnetScalerCard1", + "I10JScalerCard", "ElectromagnetStage", ] diff --git a/src/dodal/devices/i10_1/electromagnet/scaler_cards.py b/src/dodal/devices/i10_1/scaler_cards.py similarity index 91% rename from src/dodal/devices/i10_1/electromagnet/scaler_cards.py rename to src/dodal/devices/i10_1/scaler_cards.py index ef3cf53e990..f467765b088 100644 --- a/src/dodal/devices/i10_1/electromagnet/scaler_cards.py +++ b/src/dodal/devices/i10_1/scaler_cards.py @@ -3,7 +3,7 @@ from dodal.devices.current_amplifiers import StruckScaler -class ElectromagnetScalerCard1(Device): +class I10JScalerCard(Device): def __init__(self, prefix, name: str = "") -> None: self.mon = StruckScaler(prefix=prefix, suffix=".S17") self.tey = StruckScaler(prefix=prefix, suffix=".S18") From 9b0f80321f5654f79ec3543363e7b8e9d52da0f7 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 28 Jan 2026 15:36:37 +0000 Subject: [PATCH 08/18] add stage --- src/dodal/beamlines/i10_1.py | 14 ++++++++++++++ .../devices/i10_1/high_field_magnet/__init__.py | 0 2 files changed, 14 insertions(+) create mode 100644 src/dodal/devices/i10_1/high_field_magnet/__init__.py diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index c427180387c..f3003cbcc0a 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -9,6 +9,7 @@ ElectromagnetStage, I10JScalerCard, ) +from dodal.devices.motors import XYPitchStage from dodal.devices.temperture_controller.lakeshore.lakeshore import Lakeshore336 from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -62,6 +63,19 @@ def electromagnet_stage() -> ElectromagnetStage: ) +"""I10J Hight Field Magnet Devices""" + + +@devices.factory() +def high_field_magnet_stage() -> XYPitchStage: + return XYPitchStage( + prefix=f"{PREFIX.beamline_prefix}-EA-MAG-01:", + x_infix="X", + y_infix="INSERT:Y", + pitch_infix="INSERT:ROTY", + ) + + """I10J Electromagnet Measurement Devices""" diff --git a/src/dodal/devices/i10_1/high_field_magnet/__init__.py b/src/dodal/devices/i10_1/high_field_magnet/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From ac90851872caa6d1697b8278b78e1e7072e64703 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Wed, 18 Feb 2026 13:33:01 +0000 Subject: [PATCH 09/18] add high_field magnet stage --- src/dodal/beamlines/i10_1.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index 92fc9f4685e..d68469b7147 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -8,6 +8,7 @@ I10JScalerCard, ) from dodal.devices.current_amplifiers import SR570, CurrentAmpDet +from dodal.devices.motors import XYPitchStage from dodal.devices.temperture_controller.lakeshore.lakeshore import Lakeshore336 from dodal.log import set_beamline as set_log_beamline from dodal.utils import BeamlinePrefix, get_beamline_name @@ -122,3 +123,16 @@ def em_temperature_controller() -> Lakeshore336: return Lakeshore336( prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-41:", ) + + +"""I10J Hight Field Magnet Devices""" + + +@devices.factory() +def high_field_magnet_stage() -> XYPitchStage: + return XYPitchStage( + prefix=f"{PREFIX.beamline_prefix}-EA-MAG-01:", + x_infix="X", + y_infix="INSERT:Y", + pitch_infix="INSERT:ROTY", + ) From 8cfeb5d6b7a9deb9e440e637749faeba8cc89a81 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 19 Feb 2026 10:26:59 +0000 Subject: [PATCH 10/18] add magnet --- src/dodal/beamlines/i10_1.py | 8 ++ src/dodal/devices/beamlines/i10_1/__init__.py | 2 + .../i10_1/high_field_magnet}/__init__.py | 0 .../high_field_magnet/high_field_magnet.py | 79 +++++++++++++++++++ src/dodal/devices/i10_1/__init__.py | 11 --- .../i10_1/electromagnet/current_amp.py | 16 ---- .../devices/i10_1/electromagnet/magnet.py | 16 ---- .../devices/i10_1/electromagnet/stages.py | 14 ---- .../i10_1/high_field_magnet/__init__.py | 0 src/dodal/devices/i10_1/scaler_cards.py | 12 --- 10 files changed, 89 insertions(+), 69 deletions(-) rename src/dodal/devices/{i10_1/electromagnet => beamlines/i10_1/high_field_magnet}/__init__.py (100%) create mode 100644 src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py delete mode 100644 src/dodal/devices/i10_1/__init__.py delete mode 100644 src/dodal/devices/i10_1/electromagnet/current_amp.py delete mode 100644 src/dodal/devices/i10_1/electromagnet/magnet.py delete mode 100644 src/dodal/devices/i10_1/electromagnet/stages.py delete mode 100644 src/dodal/devices/i10_1/high_field_magnet/__init__.py delete mode 100644 src/dodal/devices/i10_1/scaler_cards.py diff --git a/src/dodal/beamlines/i10_1.py b/src/dodal/beamlines/i10_1.py index d68469b7147..01dffe47e19 100644 --- a/src/dodal/beamlines/i10_1.py +++ b/src/dodal/beamlines/i10_1.py @@ -5,6 +5,7 @@ from dodal.devices.beamlines.i10_1 import ( ElectromagnetMagnetField, ElectromagnetStage, + HighFieldMagnet, I10JScalerCard, ) from dodal.devices.current_amplifiers import SR570, CurrentAmpDet @@ -136,3 +137,10 @@ def high_field_magnet_stage() -> XYPitchStage: y_infix="INSERT:Y", pitch_infix="INSERT:ROTY", ) + + +@devices.factory() +def high_field_magnet() -> HighFieldMagnet: + return HighFieldMagnet( + prefix=f"{PREFIX.beamline_prefix}-EA-SMC-01:", + ) diff --git a/src/dodal/devices/beamlines/i10_1/__init__.py b/src/dodal/devices/beamlines/i10_1/__init__.py index b562d0a9a59..1f6b94f0cdf 100644 --- a/src/dodal/devices/beamlines/i10_1/__init__.py +++ b/src/dodal/devices/beamlines/i10_1/__init__.py @@ -1,9 +1,11 @@ from .electromagnet.magnet import ElectromagnetMagnetField from .electromagnet.stages import ElectromagnetStage +from .high_field_magnet.high_field_magnet import HighFieldMagnet from .scaler_cards import I10JScalerCard __all__ = [ "ElectromagnetMagnetField", "I10JScalerCard", "ElectromagnetStage", + "HighFieldMagnet", ] diff --git a/src/dodal/devices/i10_1/electromagnet/__init__.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/__init__.py similarity index 100% rename from src/dodal/devices/i10_1/electromagnet/__init__.py rename to src/dodal/devices/beamlines/i10_1/high_field_magnet/__init__.py diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py new file mode 100644 index 00000000000..d9edce66494 --- /dev/null +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -0,0 +1,79 @@ +from ophyd_async.core import ( + FlyMotorInfo, + StandardReadable, + StandardReadableFormat, + StrictEnum, + SubsetEnum, + WatchableAsyncStatus, +) +from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w + + +class HighFieldMangetSweepTypes(StrictEnum): + FAST = "Fast" + SLOW = "Slow" + + +class HighFieldMagnetStatus(SubsetEnum): + HOLD = "Hold" + TO_SETPOINT = "To Setpoint" + TO_ZERO = "To Zero" + CLAMP = "Clamp" + + +class HighFieldMagnetStatusRBV(SubsetEnum): + HOLD = "Hold" + TO_SETPOINT = "To Setpoint" + TO_ZERO = "To Zero" + CLAMPED = "Clamped" + + +class HighFieldMagnet( + StandardReadable, + # Locatable[float], + # Stoppable, + # Flyable, + # Preparable, + # Subscribable[float], +): + def __init__(self, prefix: str, name: str = "") -> None: + with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): + self.sweeprate = epics_signal_rw( + float, + read_pv=prefix + "RBV:FIELDSWEEPRATE", + write_pv=prefix + "SET:FIELDSWEEPRATE", + ) + self.sweep_type = epics_signal_rw( + HighFieldMangetSweepTypes, + read_pv=prefix + "STS:SWEEPMODE:TYPE", + write_pv=prefix + "SET:SWEEPMODE:TYPE", + ) + self.set_move_readback = epics_signal_r( + HighFieldMagnetStatusRBV, + read_pv=prefix + "STS:ACTIVITY", + ) + with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): + self.user_readback = epics_signal_r(float, prefix + "RBV:DEMANDFIELD") + + self.set_move = epics_signal_w( + HighFieldMagnetStatus, + write_pv=prefix + "SET:ACTIVITY", + ) + self.user_setpoint = epics_signal_rw( + float, + read_pv=prefix + "RBV:SETPOINTFIELD", + write_pv=prefix + "SET:SETPOINTFIELD", + ) + + self._set_success = True + + self._fly_info: FlyMotorInfo | None = None + + self._fly_status: WatchableAsyncStatus | None = None + + super().__init__(name=name) + + def set_name(self, name: str, *, child_name_separator: str | None = None) -> None: + super().set_name(name, child_name_separator=child_name_separator) + # Readback should be named the same as its parent in read() + self.user_readback.set_name(name) diff --git a/src/dodal/devices/i10_1/__init__.py b/src/dodal/devices/i10_1/__init__.py deleted file mode 100644 index c00fcca7fa5..00000000000 --- a/src/dodal/devices/i10_1/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from .electromagnet.current_amp import ElectromagnetSR570 -from .electromagnet.magnet import ElectromagnetMagnetField -from .electromagnet.stages import ElectromagnetStage -from .scaler_cards import I10JScalerCard - -__all__ = [ - "ElectromagnetSR570", - "ElectromagnetMagnetField", - "I10JScalerCard", - "ElectromagnetStage", -] diff --git a/src/dodal/devices/i10_1/electromagnet/current_amp.py b/src/dodal/devices/i10_1/electromagnet/current_amp.py deleted file mode 100644 index 39f433ec5bd..00000000000 --- a/src/dodal/devices/i10_1/electromagnet/current_amp.py +++ /dev/null @@ -1,16 +0,0 @@ -from ophyd_async.core import Device - -from dodal.devices.current_amplifiers import ( - SR570, -) - - -class ElectromagnetSR570(Device): - def __init__(self, prefix: str, name: str = "") -> None: - self.ca1 = SR570( - prefix + "-08:", - ) - self.ca2 = SR570( - prefix + "-09:", - ) - super().__init__(name) diff --git a/src/dodal/devices/i10_1/electromagnet/magnet.py b/src/dodal/devices/i10_1/electromagnet/magnet.py deleted file mode 100644 index 2ce31f84f9f..00000000000 --- a/src/dodal/devices/i10_1/electromagnet/magnet.py +++ /dev/null @@ -1,16 +0,0 @@ -from ophyd_async.core import StandardReadable -from ophyd_async.epics.core import epics_signal_rw - - -class ElectromagnetMagnetField(StandardReadable): - def __init__( - self, - prefix: str, - name: str = "", - ): - with self.add_children_as_readables(): - self.field = epics_signal_rw( - float, write_pv=prefix + "FIELD", read_pv=prefix + "FIELD:RBV" - ) - self.current = epics_signal_rw(float, prefix + "DMD") - super().__init__(name=name) diff --git a/src/dodal/devices/i10_1/electromagnet/stages.py b/src/dodal/devices/i10_1/electromagnet/stages.py deleted file mode 100644 index a8c66b12909..00000000000 --- a/src/dodal/devices/i10_1/electromagnet/stages.py +++ /dev/null @@ -1,14 +0,0 @@ -from ophyd_async.core import StandardReadable -from ophyd_async.epics.motor import Motor - - -class ElectromagnetStage(StandardReadable): - def __init__( - self, - prefix: str, - name: str = "", - ): - with self.add_children_as_readables(): - self.y = Motor(prefix + "Y") - self.pitch = Motor(prefix + "PITCH") - super().__init__(name=name) diff --git a/src/dodal/devices/i10_1/high_field_magnet/__init__.py b/src/dodal/devices/i10_1/high_field_magnet/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/dodal/devices/i10_1/scaler_cards.py b/src/dodal/devices/i10_1/scaler_cards.py deleted file mode 100644 index f467765b088..00000000000 --- a/src/dodal/devices/i10_1/scaler_cards.py +++ /dev/null @@ -1,12 +0,0 @@ -from ophyd_async.core import Device - -from dodal.devices.current_amplifiers import StruckScaler - - -class I10JScalerCard(Device): - def __init__(self, prefix, name: str = "") -> None: - self.mon = StruckScaler(prefix=prefix, suffix=".S17") - self.tey = StruckScaler(prefix=prefix, suffix=".S18") - self.fy = StruckScaler(prefix=prefix, suffix=".S19") - self.fy2 = StruckScaler(prefix=prefix, suffix=".S20") - super().__init__(name) From c9d03985e09c418f597daa8f87a8aae2f0e0f7dd Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Mon, 23 Feb 2026 11:03:18 +0000 Subject: [PATCH 11/18] added within_tolerance --- .../high_field_magnet/high_field_magnet.py | 165 +++++++++++++++++- 1 file changed, 158 insertions(+), 7 deletions(-) diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index d9edce66494..a8134054b92 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -1,10 +1,31 @@ +from __future__ import annotations + +import asyncio + +from bluesky.protocols import ( + Flyable, + Locatable, + Location, + Preparable, + Reading, + Stoppable, + Subscribable, +) from ophyd_async.core import ( + DEFAULT_TIMEOUT, + AsyncStatus, + Callback, FlyMotorInfo, StandardReadable, StandardReadableFormat, StrictEnum, SubsetEnum, WatchableAsyncStatus, + WatcherUpdate, + derived_signal_r, + observe_value, + set_and_wait_for_other_value, + soft_signal_rw, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w @@ -30,13 +51,15 @@ class HighFieldMagnetStatusRBV(SubsetEnum): class HighFieldMagnet( StandardReadable, - # Locatable[float], - # Stoppable, - # Flyable, - # Preparable, - # Subscribable[float], + Locatable[float], + Stoppable, + Flyable, + Preparable, + Subscribable[float], ): - def __init__(self, prefix: str, name: str = "") -> None: + def __init__( + self, prefix: str, field_tolerance: float = 0.01, name: str = "" + ) -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): self.sweeprate = epics_signal_rw( float, @@ -52,6 +75,8 @@ def __init__(self, prefix: str, name: str = "") -> None: HighFieldMagnetStatusRBV, read_pv=prefix + "STS:ACTIVITY", ) + self.ramp_up_time = soft_signal_rw(datatype=float, initial_value=1.0) + self.field_tolerance = soft_signal_rw(float, initial_value=field_tolerance) with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.user_readback = epics_signal_r(float, prefix + "RBV:DEMANDFIELD") @@ -65,6 +90,13 @@ def __init__(self, prefix: str, name: str = "") -> None: write_pv=prefix + "SET:SETPOINTFIELD", ) + self.within_tolerance = derived_signal_r( + raw_to_derived=self._within_tolerance, + setpoint=self.user_setpoint, + readback=self.user_readback, + tolerance=self.field_tolerance, + ) + self._set_success = True self._fly_info: FlyMotorInfo | None = None @@ -73,7 +105,126 @@ def __init__(self, prefix: str, name: str = "") -> None: super().__init__(name=name) + def _within_tolerance( + self, setpoint: float, readback: float, tolerance: float + ) -> bool: + """Check if the readback is within the tolerance of the setpoint.""" + return abs(setpoint - readback) < abs(tolerance) + def set_name(self, name: str, *, child_name_separator: str | None = None) -> None: super().set_name(name, child_name_separator=child_name_separator) - # Readback should be named the same as its parent in read() self.user_readback.set_name(name) + + async def locate(self) -> Location[float]: + setpoint, readback = await asyncio.gather( + self.user_setpoint.get_value(), self.user_readback.get_value() + ) + return Location(setpoint=setpoint, readback=readback) + + async def stop(self, success=False): + self._set_success = success + await self.user_readback.get_value() + await self.user_setpoint.set(await self.user_readback.get_value(), wait=False) + + def subscribe_reading(self, function: Callback[dict[str, Reading[float]]]) -> None: + self.user_readback.subscribe_reading(function) + + subscribe = subscribe_reading + + def clear_sub(self, function: Callback[dict[str, Reading[float]]]) -> None: + """Unsubscribe.""" + self.user_readback.clear_sub(function) + + @WatchableAsyncStatus.wrap + async def set( + self, + new_position: float, + ): + self._set_success = True + ( + old_position, + sweeprate, + ramp_up_time, + ) = await asyncio.gather( + self.user_setpoint.get_value(), + self.sweeprate.get_value(), + self.ramp_up_time.get_value(), + ) + + try: + timeout = ( + abs((new_position - old_position) / sweeprate) + + 2 * ramp_up_time + + DEFAULT_TIMEOUT + ) + except ZeroDivisionError as error: + msg = "Mover has zero velocity" + raise ValueError(msg) from error + + move_status = AsyncStatus( + set_and_wait_for_other_value( + set_signal=self.user_setpoint, + set_value=new_position, + match_signal=self.within_tolerance, + match_value=True, + timeout=timeout, + ) + ) + async for current_position in observe_value( + self.user_readback, done_status=move_status + ): + yield WatcherUpdate( + current=current_position, + initial=old_position, + target=new_position, + name=self.name, + ) + if not self._set_success: + raise RuntimeError("Field was stopped") + + # @AsyncStatus.wrap + # async def prepare(self, value: FlyMotorInfo): + # """Move to the beginning of a suitable run-up distance ready for a fly scan.""" + # self._fly_info = value + + # # Velocity, at which motor travels from start_position to end_position, in motor + # # egu/s. + # max_speed, egu = await asyncio.gather( + # self.max_velocity.get_value(), self.motor_egu.get_value() + # ) + # if abs(value.velocity) > max_speed: + # raise MotorLimitsError( + # f"Velocity {abs(value.velocity)} {egu}/s was requested for a motor " + # f" with max speed of {max_speed} {egu}/s" + # ) + + # acceleration_time = await self.ramp_up_time.get_value() + # ramp_up_start_pos = value.ramp_up_start_pos(acceleration_time) + # ramp_down_end_pos = value.ramp_down_end_pos(acceleration_time) + + # await self.check_motor_limit(ramp_up_start_pos, ramp_down_end_pos) + + # # move to prepare position at maximum velocity + # await self.velocity.set(abs(max_speed)) + # await self.set(ramp_up_start_pos) + + # # Set velocity we will be using for the fly scan + # await self.velocity.set(abs(value.velocity)) + + # @AsyncStatus.wrap + # async def kickoff(self): + # """Begin moving motor from prepared position to final position.""" + # fly_info = error_if_none( + # self._fly_info, "Motor must be prepared before attempting to kickoff" + # ) + + # acceleration_time = await self.acceleration_time.get_value() + # self._fly_status = self.set( + # fly_info.ramp_down_end_pos(acceleration_time), + # timeout=fly_info.timeout, + # ) + + # def complete(self) -> WatchableAsyncStatus: + # """Mark as complete once motor reaches completed position.""" + # fly_status = error_if_none(self._fly_status, "kickoff not called") + # return fly_status From f5c226a9272c51020c12b6755c434ea80044c120 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 13:57:57 +0000 Subject: [PATCH 12/18] make hfm flyable --- .../high_field_magnet/high_field_magnet.py | 100 ++++++++---------- 1 file changed, 43 insertions(+), 57 deletions(-) diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index a8134054b92..5b6314d905a 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -15,7 +15,6 @@ DEFAULT_TIMEOUT, AsyncStatus, Callback, - FlyMotorInfo, StandardReadable, StandardReadableFormat, StrictEnum, @@ -23,11 +22,13 @@ WatchableAsyncStatus, WatcherUpdate, derived_signal_r, + error_if_none, observe_value, set_and_wait_for_other_value, soft_signal_rw, ) from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w +from pydantic import BaseModel, Field class HighFieldMangetSweepTypes(StrictEnum): @@ -49,6 +50,16 @@ class HighFieldMagnetStatusRBV(SubsetEnum): CLAMPED = "Clamped" +class FlyMagInfo(BaseModel): + """Minimal set of information required to fly a high field magnet.""" + + start_position: float = Field(frozen=True) + + end_position: float = Field(frozen=True) + + sweep_rate: float = Field(frozen=True, gt=0) + + class HighFieldMagnet( StandardReadable, Locatable[float], @@ -61,10 +72,10 @@ def __init__( self, prefix: str, field_tolerance: float = 0.01, name: str = "" ) -> None: with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL): - self.sweeprate = epics_signal_rw( + self.sweep_rate = epics_signal_rw( float, - read_pv=prefix + "RBV:FIELDSWEEPRATE", - write_pv=prefix + "SET:FIELDSWEEPRATE", + read_pv=prefix + "RBV:FIELDsweep_rate", + write_pv=prefix + "SET:FIELDsweep_rate", ) self.sweep_type = epics_signal_rw( HighFieldMangetSweepTypes, @@ -99,7 +110,7 @@ def __init__( self._set_success = True - self._fly_info: FlyMotorInfo | None = None + self._fly_info: FlyMagInfo | None = None self._fly_status: WatchableAsyncStatus | None = None @@ -143,17 +154,17 @@ async def set( self._set_success = True ( old_position, - sweeprate, + sweep_rate, ramp_up_time, ) = await asyncio.gather( - self.user_setpoint.get_value(), - self.sweeprate.get_value(), + self.user_readback.get_value(), + self.sweep_rate.get_value(), self.ramp_up_time.get_value(), ) try: timeout = ( - abs((new_position - old_position) / sweeprate) + abs((new_position - old_position) / sweep_rate) + 2 * ramp_up_time + DEFAULT_TIMEOUT ) @@ -180,51 +191,26 @@ async def set( name=self.name, ) if not self._set_success: - raise RuntimeError("Field was stopped") - - # @AsyncStatus.wrap - # async def prepare(self, value: FlyMotorInfo): - # """Move to the beginning of a suitable run-up distance ready for a fly scan.""" - # self._fly_info = value - - # # Velocity, at which motor travels from start_position to end_position, in motor - # # egu/s. - # max_speed, egu = await asyncio.gather( - # self.max_velocity.get_value(), self.motor_egu.get_value() - # ) - # if abs(value.velocity) > max_speed: - # raise MotorLimitsError( - # f"Velocity {abs(value.velocity)} {egu}/s was requested for a motor " - # f" with max speed of {max_speed} {egu}/s" - # ) - - # acceleration_time = await self.ramp_up_time.get_value() - # ramp_up_start_pos = value.ramp_up_start_pos(acceleration_time) - # ramp_down_end_pos = value.ramp_down_end_pos(acceleration_time) - - # await self.check_motor_limit(ramp_up_start_pos, ramp_down_end_pos) - - # # move to prepare position at maximum velocity - # await self.velocity.set(abs(max_speed)) - # await self.set(ramp_up_start_pos) - - # # Set velocity we will be using for the fly scan - # await self.velocity.set(abs(value.velocity)) - - # @AsyncStatus.wrap - # async def kickoff(self): - # """Begin moving motor from prepared position to final position.""" - # fly_info = error_if_none( - # self._fly_info, "Motor must be prepared before attempting to kickoff" - # ) - - # acceleration_time = await self.acceleration_time.get_value() - # self._fly_status = self.set( - # fly_info.ramp_down_end_pos(acceleration_time), - # timeout=fly_info.timeout, - # ) - - # def complete(self) -> WatchableAsyncStatus: - # """Mark as complete once motor reaches completed position.""" - # fly_status = error_if_none(self._fly_status, "kickoff not called") - # return fly_status + raise RuntimeError("Field changewas stopped") + + @AsyncStatus.wrap + async def prepare(self, value: FlyMagInfo) -> None: + """Move to the beginning of a suitable run-up distance ready for a fly scan.""" + self._fly_info = value + + await self.set(value.start_position) + + # Set velocity we will be using for the fly scan + await self.sweep_rate.set(abs(value.sweep_rate)) + + @AsyncStatus.wrap + async def kickoff(self): + fly_info = error_if_none( + self._fly_info, "Magnet must be prepared before attempting to kickoff" + ) + + self._fly_status = self.set(fly_info.end_position) + + def complete(self) -> WatchableAsyncStatus: + fly_status = error_if_none(self._fly_status, "kickoff not called") + return fly_status From 24c9e16db283b7cfa46cf9aa9d49b03d970e690e Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 15:40:39 +0000 Subject: [PATCH 13/18] add high field magnet test --- .../high_field_magnet/high_field_magnet.py | 3 +- .../i10_1/high_field_magnet/__init__.py | 0 .../test_high_field_magnet.py | 175 ++++++++++++++++++ 3 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 tests/devices/beamlines/i10_1/high_field_magnet/__init__.py create mode 100644 tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index 5b6314d905a..c3081039cf9 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -169,7 +169,7 @@ async def set( + DEFAULT_TIMEOUT ) except ZeroDivisionError as error: - msg = "Mover has zero velocity" + msg = "Magnet has zero sweep_rate." raise ValueError(msg) from error move_status = AsyncStatus( @@ -200,7 +200,6 @@ async def prepare(self, value: FlyMagInfo) -> None: await self.set(value.start_position) - # Set velocity we will be using for the fly scan await self.sweep_rate.set(abs(value.sweep_rate)) @AsyncStatus.wrap diff --git a/tests/devices/beamlines/i10_1/high_field_magnet/__init__.py b/tests/devices/beamlines/i10_1/high_field_magnet/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py new file mode 100644 index 00000000000..2114beccb0a --- /dev/null +++ b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py @@ -0,0 +1,175 @@ +import pytest +from ophyd_async.core import AsyncStatus, init_devices, set_mock_value +from ophyd_async.testing import assert_reading + +from dodal.devices.beamlines.i10_1.high_field_magnet.high_field_magnet import ( + FlyMagInfo, + HighFieldMagnet, +) + + +@pytest.fixture +async def high_field_magnet() -> HighFieldMagnet: + """Create a HighFieldMagnet device with mock signals.""" + async with init_devices(mock=True): + magnet = HighFieldMagnet(prefix="TEST:") + return magnet + + +async def test_within_tolerance_when_in_range(high_field_magnet: HighFieldMagnet): + """Test _within_tolerance returns True when readback is within tolerance of setpoint.""" + result = high_field_magnet._within_tolerance( + setpoint=10.0, readback=10.005, tolerance=0.01 + ) + assert result is True + + +async def test_within_tolerance_when_outside_range(high_field_magnet: HighFieldMagnet): + """Test _within_tolerance returns False when readback exceeds tolerance.""" + result = high_field_magnet._within_tolerance( + setpoint=10.0, readback=10.02, tolerance=0.01 + ) + assert result is False + + +async def test_within_tolerance_with_negative_tolerance( + high_field_magnet: HighFieldMagnet, +): + """Test _within_tolerance handles negative tolerance correctly.""" + result = high_field_magnet._within_tolerance( + setpoint=10.0, readback=9.995, tolerance=-0.01 + ) + assert result is True + + +async def test_locate(high_field_magnet: HighFieldMagnet): + """Test locate() returns current setpoint and readback.""" + set_mock_value(high_field_magnet.user_setpoint, 5.0) + set_mock_value(high_field_magnet.user_readback, 5.0) + + location = await high_field_magnet.locate() + assert location["setpoint"] == 5.0 + assert location["readback"] == 5.0 + + +async def test_stop_success(high_field_magnet: HighFieldMagnet): + """Test stop() sets success and updates setpoint to readback.""" + set_mock_value(high_field_magnet.user_readback, 7.5) + set_mock_value(high_field_magnet.user_readback, 1.5) + await high_field_magnet.stop() + assert high_field_magnet._set_success is False + assert await high_field_magnet.user_setpoint.get_value() == 1.5 + + +async def test_subscribe_and_clear_sub(high_field_magnet: HighFieldMagnet): + """Test subscribe_reading and clear_sub for callbacks.""" + readings = [] + + def callback(reading_dict): + readings.append(reading_dict) + + high_field_magnet.subscribe_reading(callback) + + set_mock_value(high_field_magnet.user_readback, 3.0) + + high_field_magnet.clear_sub(callback) + + # Verify subscription was cleared by checking behavior after clear + assert callable(callback) + + +async def test_set_raises_on_zero_sweep_rate(high_field_magnet: HighFieldMagnet): + """Test that set() raises ValueError when sweep_rate is zero.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 0.0) + + with pytest.raises(ValueError, match="Magnet has zero sweep_rate."): + status = high_field_magnet.set(10.0) + await status + + +async def test_set_calculates_correct_timeout(high_field_magnet: HighFieldMagnet): + """Test that set() calculates timeout based on sweep_rate and ramp_up_time.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + set_mock_value(high_field_magnet.ramp_up_time, 1.0) + set_mock_value(high_field_magnet.user_setpoint, 0.0) + + # Start the set operation + status = high_field_magnet.set(10.0) + assert isinstance(status, AsyncStatus) or hasattr(status, "watch") + + +async def test_set_returns_watchable_async_status(high_field_magnet: HighFieldMagnet): + """Test that set() returns a WatchableAsyncStatus.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + + status = high_field_magnet.set(5.0) + assert status is not None + + +async def test_prepare(high_field_magnet: HighFieldMagnet): + """Test prepare() sets fly_info and moves to start position.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + + fly_info = FlyMagInfo(start_position=1.0, end_position=10.0, sweep_rate=2.0) + await high_field_magnet.prepare(fly_info) + assert high_field_magnet._fly_info == fly_info + assert await high_field_magnet.user_setpoint.get_value() == 1.0 + assert await high_field_magnet.sweep_rate.get_value() == 2.0 + + +async def test_kickoff_without_prepare_raises(high_field_magnet: HighFieldMagnet): + """Test that kickoff() raises error if prepare() wasn't called.""" + with pytest.raises(RuntimeError, match="Magnet must be prepared"): + status = high_field_magnet.kickoff() + await status + + +async def test_kickoff_after_prepare(high_field_magnet: HighFieldMagnet): + """Test kickoff() starts a set operation to end_position after prepare.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + + fly_info = FlyMagInfo(start_position=1.0, end_position=10.0, sweep_rate=2.0) + + prepare_status = high_field_magnet.prepare(fly_info) + await prepare_status + + kickoff_status = high_field_magnet.kickoff() + assert isinstance(kickoff_status, AsyncStatus) + + +async def test_complete_without_kickoff_raises(high_field_magnet: HighFieldMagnet): + """Test that complete() raises error if kickoff() wasn't called.""" + with pytest.raises(RuntimeError, match="kickoff not called"): + high_field_magnet.complete() + + +async def test_complete_after_kickoff(high_field_magnet: HighFieldMagnet): + """Test complete() returns the fly_status from kickoff.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + + fly_info = FlyMagInfo(start_position=1.0, end_position=10.0, sweep_rate=2.0) + + prepare_status = high_field_magnet.prepare(fly_info) + await prepare_status + + kickoff_status = high_field_magnet.kickoff() + await kickoff_status + + complete_status = high_field_magnet.complete() + assert complete_status is high_field_magnet._fly_status + + +async def test_read(high_field_magnet: HighFieldMagnet): + """Test read() returns current setpoint and readback.""" + set_mock_value(high_field_magnet.user_setpoint, 5.0) + set_mock_value(high_field_magnet.user_readback, 5.0) + await assert_reading( + high_field_magnet, + expected_reading={"magnet": {"value": 5.0}}, + ) From 54e405f0f8eb49ffcafc3b793bbb9669708c6efa Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 15:43:58 +0000 Subject: [PATCH 14/18] fix old ophyd --- .../beamlines/i10_1/high_field_magnet/high_field_magnet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index c3081039cf9..ac4f77541d9 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -135,7 +135,7 @@ async def locate(self) -> Location[float]: async def stop(self, success=False): self._set_success = success await self.user_readback.get_value() - await self.user_setpoint.set(await self.user_readback.get_value(), wait=False) + await self.user_setpoint.set(await self.user_readback.get_value()) def subscribe_reading(self, function: Callback[dict[str, Reading[float]]]) -> None: self.user_readback.subscribe_reading(function) From f30f00b36c1c177aba2969db2a80fe63dd7c08d8 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 16:07:05 +0000 Subject: [PATCH 15/18] add stopped raise expection --- .../high_field_magnet/test_high_field_magnet.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py index 2114beccb0a..cfdd99289a9 100644 --- a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py +++ b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py @@ -61,6 +61,20 @@ async def test_stop_success(high_field_magnet: HighFieldMagnet): assert await high_field_magnet.user_setpoint.get_value() == 1.5 +async def test_set_raises_runtime_error_when_stopped(high_field_magnet): + """Test that set() raises RuntimeError when _set_success is False.""" + set_mock_value(high_field_magnet.user_readback, 0.0) + set_mock_value(high_field_magnet.sweep_rate, 1.0) + set_mock_value(high_field_magnet.ramp_up_time, 1.0) + + # Set _set_success to False to simulate a stopped operation + high_field_magnet._set_success = False + + with pytest.raises(RuntimeError, match="Field changewas stopped"): + status = high_field_magnet.set(5.0) + await status + + async def test_subscribe_and_clear_sub(high_field_magnet: HighFieldMagnet): """Test subscribe_reading and clear_sub for callbacks.""" readings = [] From 10c0666c8cd297bed1e0c8e09e11512a62ae7a77 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 16:27:53 +0000 Subject: [PATCH 16/18] rename set_move --- .../beamlines/i10_1/high_field_magnet/high_field_magnet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index ac4f77541d9..81e65462ff4 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -51,7 +51,7 @@ class HighFieldMagnetStatusRBV(SubsetEnum): class FlyMagInfo(BaseModel): - """Minimal set of information required to fly a high field magnet.""" + """Minimal set of information required to fly high field magnet.""" start_position: float = Field(frozen=True) @@ -91,7 +91,7 @@ def __init__( with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): self.user_readback = epics_signal_r(float, prefix + "RBV:DEMANDFIELD") - self.set_move = epics_signal_w( + self.set_mode = epics_signal_w( HighFieldMagnetStatus, write_pv=prefix + "SET:ACTIVITY", ) From 729960fd768dbf3f7f65a4d931343217417e2fcb Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Thu, 26 Feb 2026 16:29:31 +0000 Subject: [PATCH 17/18] docstring --- .../test_high_field_magnet.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py index cfdd99289a9..ca06cb4921a 100644 --- a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py +++ b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py @@ -10,14 +10,12 @@ @pytest.fixture async def high_field_magnet() -> HighFieldMagnet: - """Create a HighFieldMagnet device with mock signals.""" async with init_devices(mock=True): magnet = HighFieldMagnet(prefix="TEST:") return magnet async def test_within_tolerance_when_in_range(high_field_magnet: HighFieldMagnet): - """Test _within_tolerance returns True when readback is within tolerance of setpoint.""" result = high_field_magnet._within_tolerance( setpoint=10.0, readback=10.005, tolerance=0.01 ) @@ -25,7 +23,6 @@ async def test_within_tolerance_when_in_range(high_field_magnet: HighFieldMagnet async def test_within_tolerance_when_outside_range(high_field_magnet: HighFieldMagnet): - """Test _within_tolerance returns False when readback exceeds tolerance.""" result = high_field_magnet._within_tolerance( setpoint=10.0, readback=10.02, tolerance=0.01 ) @@ -35,7 +32,6 @@ async def test_within_tolerance_when_outside_range(high_field_magnet: HighFieldM async def test_within_tolerance_with_negative_tolerance( high_field_magnet: HighFieldMagnet, ): - """Test _within_tolerance handles negative tolerance correctly.""" result = high_field_magnet._within_tolerance( setpoint=10.0, readback=9.995, tolerance=-0.01 ) @@ -43,7 +39,6 @@ async def test_within_tolerance_with_negative_tolerance( async def test_locate(high_field_magnet: HighFieldMagnet): - """Test locate() returns current setpoint and readback.""" set_mock_value(high_field_magnet.user_setpoint, 5.0) set_mock_value(high_field_magnet.user_readback, 5.0) @@ -53,7 +48,6 @@ async def test_locate(high_field_magnet: HighFieldMagnet): async def test_stop_success(high_field_magnet: HighFieldMagnet): - """Test stop() sets success and updates setpoint to readback.""" set_mock_value(high_field_magnet.user_readback, 7.5) set_mock_value(high_field_magnet.user_readback, 1.5) await high_field_magnet.stop() @@ -62,12 +56,11 @@ async def test_stop_success(high_field_magnet: HighFieldMagnet): async def test_set_raises_runtime_error_when_stopped(high_field_magnet): - """Test that set() raises RuntimeError when _set_success is False.""" + set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) set_mock_value(high_field_magnet.ramp_up_time, 1.0) - # Set _set_success to False to simulate a stopped operation high_field_magnet._set_success = False with pytest.raises(RuntimeError, match="Field changewas stopped"): @@ -88,12 +81,10 @@ def callback(reading_dict): high_field_magnet.clear_sub(callback) - # Verify subscription was cleared by checking behavior after clear assert callable(callback) async def test_set_raises_on_zero_sweep_rate(high_field_magnet: HighFieldMagnet): - """Test that set() raises ValueError when sweep_rate is zero.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 0.0) @@ -103,7 +94,6 @@ async def test_set_raises_on_zero_sweep_rate(high_field_magnet: HighFieldMagnet) async def test_set_calculates_correct_timeout(high_field_magnet: HighFieldMagnet): - """Test that set() calculates timeout based on sweep_rate and ramp_up_time.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) set_mock_value(high_field_magnet.ramp_up_time, 1.0) @@ -115,7 +105,6 @@ async def test_set_calculates_correct_timeout(high_field_magnet: HighFieldMagnet async def test_set_returns_watchable_async_status(high_field_magnet: HighFieldMagnet): - """Test that set() returns a WatchableAsyncStatus.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) @@ -124,7 +113,6 @@ async def test_set_returns_watchable_async_status(high_field_magnet: HighFieldMa async def test_prepare(high_field_magnet: HighFieldMagnet): - """Test prepare() sets fly_info and moves to start position.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) @@ -136,14 +124,12 @@ async def test_prepare(high_field_magnet: HighFieldMagnet): async def test_kickoff_without_prepare_raises(high_field_magnet: HighFieldMagnet): - """Test that kickoff() raises error if prepare() wasn't called.""" with pytest.raises(RuntimeError, match="Magnet must be prepared"): status = high_field_magnet.kickoff() await status async def test_kickoff_after_prepare(high_field_magnet: HighFieldMagnet): - """Test kickoff() starts a set operation to end_position after prepare.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) @@ -157,13 +143,11 @@ async def test_kickoff_after_prepare(high_field_magnet: HighFieldMagnet): async def test_complete_without_kickoff_raises(high_field_magnet: HighFieldMagnet): - """Test that complete() raises error if kickoff() wasn't called.""" with pytest.raises(RuntimeError, match="kickoff not called"): high_field_magnet.complete() async def test_complete_after_kickoff(high_field_magnet: HighFieldMagnet): - """Test complete() returns the fly_status from kickoff.""" set_mock_value(high_field_magnet.user_readback, 0.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) @@ -180,7 +164,6 @@ async def test_complete_after_kickoff(high_field_magnet: HighFieldMagnet): async def test_read(high_field_magnet: HighFieldMagnet): - """Test read() returns current setpoint and readback.""" set_mock_value(high_field_magnet.user_setpoint, 5.0) set_mock_value(high_field_magnet.user_readback, 5.0) await assert_reading( From 34e35f246a2e10a7996dbb017c5bfaa7cc649f50 Mon Sep 17 00:00:00 2001 From: Relm-Arrowny Date: Fri, 27 Feb 2026 11:39:13 +0000 Subject: [PATCH 18/18] fix racing condition --- .../high_field_magnet/high_field_magnet.py | 2 +- .../test_high_field_magnet.py | 25 ++++++++++++------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py index 81e65462ff4..393882132fa 100644 --- a/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py +++ b/src/dodal/devices/beamlines/i10_1/high_field_magnet/high_field_magnet.py @@ -191,7 +191,7 @@ async def set( name=self.name, ) if not self._set_success: - raise RuntimeError("Field changewas stopped") + raise RuntimeError("Field change was stopped") @AsyncStatus.wrap async def prepare(self, value: FlyMagInfo) -> None: diff --git a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py index ca06cb4921a..fd5ea075fbb 100644 --- a/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py +++ b/tests/devices/beamlines/i10_1/high_field_magnet/test_high_field_magnet.py @@ -1,6 +1,10 @@ import pytest -from ophyd_async.core import AsyncStatus, init_devices, set_mock_value -from ophyd_async.testing import assert_reading +from ophyd_async.core import ( + AsyncStatus, + init_devices, + set_mock_value, +) +from ophyd_async.testing import assert_reading, wait_for_pending_wakeups from dodal.devices.beamlines.i10_1.high_field_magnet.high_field_magnet import ( FlyMagInfo, @@ -55,16 +59,19 @@ async def test_stop_success(high_field_magnet: HighFieldMagnet): assert await high_field_magnet.user_setpoint.get_value() == 1.5 -async def test_set_raises_runtime_error_when_stopped(high_field_magnet): - - set_mock_value(high_field_magnet.user_readback, 0.0) +async def test_set_raises_runtime_error_when_stopped( + high_field_magnet: HighFieldMagnet, +): + set_mock_value(high_field_magnet.user_readback, -5.0) set_mock_value(high_field_magnet.sweep_rate, 1.0) set_mock_value(high_field_magnet.ramp_up_time, 1.0) + status = high_field_magnet.set(5.0) - high_field_magnet._set_success = False - - with pytest.raises(RuntimeError, match="Field changewas stopped"): - status = high_field_magnet.set(5.0) + assert status is not None + with pytest.raises(RuntimeError, match="Field change was stopped"): + await wait_for_pending_wakeups() + await high_field_magnet.stop(success=False) + set_mock_value(high_field_magnet.user_readback, 5.0) await status