Skip to content

Commit bfbf803

Browse files
Villtordeir17846oliwenmandiamond
authored
Undulator order (harmonics) class (#1661)
* all in one shot * improve error message, rename internal signal --------- Co-authored-by: eir17846 <[email protected]> Co-authored-by: oliwenmandiamond <[email protected]>
1 parent b565a1c commit bfbf803

File tree

3 files changed

+78
-1
lines changed

3 files changed

+78
-1
lines changed

src/dodal/beamlines/i09_1.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from dodal.devices.electron_analyser.specs import SpecsDetector
1212
from dodal.devices.i09_1 import LensMode, PsuMode
1313
from dodal.devices.synchrotron import Synchrotron
14+
from dodal.devices.undulator import UndulatorOrder
1415
from dodal.log import set_beamline as set_log_beamline
1516
from dodal.utils import BeamlinePrefix, get_beamline_name
1617

@@ -47,3 +48,8 @@ def analyser() -> SpecsDetector[LensMode, PsuMode]:
4748
psu_mode_type=PsuMode,
4849
energy_source=energy_source(),
4950
)
51+
52+
53+
@device_factory()
54+
def harmonics() -> UndulatorOrder:
55+
return UndulatorOrder(name="harmonics")

src/dodal/devices/undulator.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import os
22

33
import numpy as np
4-
from bluesky.protocols import Movable
4+
from bluesky.protocols import Locatable, Location, Movable
55
from numpy import ndarray
66
from ophyd_async.core import (
77
AsyncStatus,
88
Reference,
99
StandardReadable,
1010
StandardReadableFormat,
1111
soft_signal_r_and_setter,
12+
soft_signal_rw,
1213
)
1314
from ophyd_async.epics.core import epics_signal_r
1415
from ophyd_async.epics.motor import Motor
@@ -157,3 +158,30 @@ async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
157158
energy_kev * 1000,
158159
energy_to_distance_table,
159160
)
161+
162+
163+
class UndulatorOrder(StandardReadable, Locatable[int]):
164+
"""
165+
Represents the order of an undulator device. Allows setting and locating the order.
166+
"""
167+
168+
def __init__(self, name: str = "") -> None:
169+
"""
170+
Args:
171+
name: Name for device. Defaults to ""
172+
"""
173+
with self.add_children_as_readables():
174+
self._value = soft_signal_rw(int, initial_value=3)
175+
super().__init__(name=name)
176+
177+
@AsyncStatus.wrap
178+
async def set(self, value: int) -> None:
179+
if (value >= 0) and isinstance(value, int):
180+
await self._value.set(value)
181+
else:
182+
raise ValueError(
183+
f"Undulator order must be a positive integer. Requested value: {value}"
184+
)
185+
186+
async def locate(self) -> Location[int]:
187+
return await self._value.locate()

tests/devices/test_undulator.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
import numpy as np
55
import pytest
6+
from bluesky import RunEngine
7+
from bluesky.plan_stubs import mv
68
from ophyd_async.core import init_devices
79
from ophyd_async.testing import (
810
assert_configuration,
@@ -17,6 +19,7 @@
1719
from dodal.devices.undulator import (
1820
AccessError,
1921
Undulator,
22+
UndulatorOrder,
2023
_get_gap_for_energy,
2124
)
2225
from dodal.testing import patch_all_motors
@@ -48,6 +51,13 @@ def undulator_in_commissioning_mode(
4851
yield undulator
4952

5053

54+
@pytest.fixture
55+
async def undulator_order() -> UndulatorOrder:
56+
async with init_devices(mock=True):
57+
order = UndulatorOrder(name="undulator_order")
58+
return order
59+
60+
5161
async def test_reading_includes_read_fields(undulator: Undulator):
5262
await assert_reading(
5363
undulator,
@@ -147,3 +157,36 @@ async def test_gap_access_check_move_not_inhibited_when_commissioning_mode_disab
147157
get_mock_put(undulator.gap_motor.user_setpoint).assert_called_once_with(
148158
15.0, wait=True
149159
)
160+
161+
162+
async def test_order_read(
163+
undulator_order: UndulatorOrder,
164+
):
165+
await assert_reading(
166+
undulator_order,
167+
{"undulator_order-_value": partial_reading(3)},
168+
)
169+
170+
171+
async def test_move_order(
172+
undulator_order: UndulatorOrder,
173+
run_engine: RunEngine,
174+
):
175+
assert (await undulator_order.locate())["readback"] == 3 # default order
176+
run_engine(mv(undulator_order, 1))
177+
assert (await undulator_order.locate())["readback"] == 1 # no error
178+
179+
180+
@pytest.mark.parametrize(
181+
"order_value",
182+
[-1, 1.56],
183+
)
184+
async def test_move_order_fails(
185+
undulator_order: UndulatorOrder,
186+
order_value: float | int,
187+
):
188+
with pytest.raises(
189+
ValueError,
190+
match=f"Undulator order must be a positive integer. Requested value: {order_value}",
191+
):
192+
await undulator_order.set(order_value) # type: ignore

0 commit comments

Comments
 (0)