Skip to content

Commit aa40631

Browse files
Add B07 and B07-1 sample stage (#1913)
* Add B07 sample stage * Add doc strings * Add b07_motors_test * Remove mock=True * Reduce number of parametrize * Corrected test name * Applied feedback
1 parent ee97fc1 commit aa40631

File tree

7 files changed

+204
-4
lines changed

7 files changed

+204
-4
lines changed

src/dodal/beamlines/b07.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
11
from dodal.beamlines.b07_shared import devices as b07_shared_devices
22
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
33
from dodal.device_manager import DeviceManager
4-
from dodal.devices.beamlines.b07 import Grating, LensMode, PsuMode
4+
from dodal.devices.beamlines.b07 import (
5+
B07SampleManipulator52B,
6+
Grating,
7+
LensMode,
8+
PsuMode,
9+
)
510
from dodal.devices.electron_analyser.base import EnergySource
611
from dodal.devices.electron_analyser.specs import SpecsDetector
12+
from dodal.devices.motors import XYZPolarStage
713
from dodal.devices.pgm import PlaneGratingMonochromator
814
from dodal.log import set_beamline as set_log_beamline
915
from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -40,3 +46,16 @@ def analyser(energy_source: EnergySource) -> SpecsDetector[LensMode, PsuMode]:
4046
psu_mode_type=PsuMode,
4147
energy_source=energy_source,
4248
)
49+
50+
51+
@devices.factory()
52+
def sm52b() -> B07SampleManipulator52B:
53+
return B07SampleManipulator52B(prefix=f"{B_PREFIX.beamline_prefix}-EA-SM-52:")
54+
55+
56+
@devices.factory()
57+
def sm21b() -> XYZPolarStage:
58+
"""Sample manipulator. NOTE: The polar attribute is equivalent to GDA roty."""
59+
return XYZPolarStage(
60+
prefix=f"{B_PREFIX.beamline_prefix}-EA-SM-21:", polar_infix="ROTY"
61+
)

src/dodal/beamlines/b07_1.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
)
1010
from dodal.devices.electron_analyser.base import EnergySource
1111
from dodal.devices.electron_analyser.specs import SpecsDetector
12+
from dodal.devices.motors import XYZPolarAzimuthStage
1213
from dodal.devices.pgm import PlaneGratingMonochromator
1314
from dodal.log import set_beamline as set_log_beamline
1415
from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -50,3 +51,16 @@ def analyser(energy_source: EnergySource) -> SpecsDetector[LensMode, PsuMode]:
5051
psu_mode_type=PsuMode,
5152
energy_source=energy_source,
5253
)
54+
55+
56+
@devices.factory()
57+
def sm() -> XYZPolarAzimuthStage:
58+
"""Sample manipulator."""
59+
return XYZPolarAzimuthStage(
60+
f"{C_PREFIX.beamline_prefix}-EA-SM-01:",
61+
x_infix="XP",
62+
y_infix="YP",
63+
z_infix="ZP",
64+
polar_infix="ROTA",
65+
azimuth_infix="ROTB",
66+
)
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1-
from dodal.devices.beamlines.b07.enums import Grating, LensMode, PsuMode
1+
from .b07_motors import B07SampleManipulator52B
2+
from .enums import Grating, LensMode, PsuMode
23

3-
__all__ = ["Grating", "LensMode", "PsuMode"]
4+
__all__ = ["B07SampleManipulator52B", "Grating", "LensMode", "PsuMode"]
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
from bluesky.protocols import Movable
2+
from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
3+
from ophyd_async.epics.core import epics_signal_rw_rbv
4+
from ophyd_async.epics.motor import Motor
5+
6+
from dodal.devices.motors import _OMEGA, XYZStage
7+
8+
9+
class VirtualAxis(StandardReadable, Movable[float]):
10+
"""Represents the virtual axis coordinate system for the B07SampleManipulator52B.
11+
The value signal is the read signal for this device.
12+
"""
13+
14+
def __init__(self, pv: str, name: str = ""):
15+
"""Initialise the device via pv and name configuration.
16+
17+
Args:
18+
pv (str): The base PV to connect to the virtual axis.
19+
name (str, optional): Name of this device.
20+
"""
21+
with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
22+
self.value = epics_signal_rw_rbv(float, pv, read_suffix=":RBV")
23+
super().__init__(name)
24+
25+
@AsyncStatus.wrap
26+
async def set(self, value: float):
27+
await self.value.set(value)
28+
29+
def set_name(self, name: str, *, child_name_separator: str | None = None) -> None:
30+
super().set_name(name, child_name_separator=child_name_separator)
31+
# Value should be named the same as its parent in read()
32+
self.value.set_name(name)
33+
34+
35+
class B07SampleManipulator52B(XYZStage):
36+
"""Sample Manipulator of several Motors and VirtaulAxis.
37+
38+
Attributes:
39+
x (Motor): Controls x direction.
40+
y (Motor): Controls y direction.
41+
z (Motor): Controls z direction.
42+
roty (Motor): Controls y rotation.
43+
rotz (Motor): Controls z rotation.
44+
xp (Motor): Compound motor for x.
45+
yp (Motor): Compound motor for y.
46+
zp (Motor): Compound motor for z.
47+
omega (VirtualAxis): Virtal rotational axis for the x direction.
48+
phi (VirtualAxis): Virtal rotational axis for the y direction.
49+
kappa (VirtualAxis): Virtal rotational axis for the z direction.
50+
"""
51+
52+
def __init__(
53+
self,
54+
prefix: str,
55+
x_infix: str = "XP",
56+
y_infix: str = "YP",
57+
z_infix: str = "ZP",
58+
roty_infix: str = "ROTY",
59+
rotz_infix: str = "ROTZ",
60+
kappa_infix: str = "KAPPA",
61+
phi_infix: str = "PHI",
62+
omega_infix: str = _OMEGA,
63+
name: str = "",
64+
):
65+
"""Initialise the device via prefix and infix PV configuration.
66+
67+
Args:
68+
prefix (str): Base PV used for connecting signals.
69+
x_infix (str, optional): Infix between base prefix and x motor record.
70+
y_infix (str, optional): Infix between base prefix and y motor record.
71+
z_infix (str, optional): Infix between base prefix and z motor record.
72+
roty_infix (str, optional): Infix between base prefix and roty motor record.
73+
rotz_infix (str, optional): Infix between base prefix and rotz motor record.
74+
kappa_infix (str, optional): Infix between base prefix and kappa virtual axis.
75+
phi_infix (str, optional): Infix between base prefix and phi virtual axis.
76+
omega_infix (str, optional): Infix between base prefix and omega virtual axis.
77+
name (str, optional): The name of this device.
78+
"""
79+
with self.add_children_as_readables():
80+
# Compound motors
81+
self.xp = Motor(prefix + x_infix)
82+
self.yp = Motor(prefix + y_infix)
83+
self.zp = Motor(prefix + z_infix)
84+
85+
# Raw motors
86+
self.roty = Motor(prefix + roty_infix)
87+
self.rotz = Motor(prefix + rotz_infix)
88+
89+
# Not standard motors, virtual axes coordinate system.
90+
self.kappa = VirtualAxis(prefix + kappa_infix)
91+
self.phi = VirtualAxis(prefix + phi_infix)
92+
self.omega = VirtualAxis(prefix + omega_infix)
93+
94+
super().__init__(
95+
prefix=prefix,
96+
x_infix=x_infix,
97+
y_infix=y_infix,
98+
z_infix=z_infix,
99+
name=name,
100+
)

src/dodal/devices/motors.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ def __init__(
275275
with self.add_children_as_readables():
276276
self.kappa = Motor(prefix + kappa_infix)
277277
self.phi = Motor(prefix + phi_infix)
278-
super().__init__(prefix, name, x_infix, y_infix, z_infix)
278+
super().__init__(prefix, name, x_infix, y_infix, z_infix, omega_infix)
279279

280280
self.vertical_in_lab_space = create_axis_perp_to_rotation(
281281
self.omega, self.y, self.z

tests/devices/beamlines/b07/__init__.py

Whitespace-only changes.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import asyncio
2+
3+
import pytest
4+
from ophyd_async.core import init_devices
5+
from ophyd_async.testing import assert_reading, partial_reading
6+
7+
from dodal.devices.beamlines.b07 import B07SampleManipulator52B
8+
9+
10+
@pytest.fixture
11+
def sm52b() -> B07SampleManipulator52B:
12+
with init_devices(mock=True):
13+
sm52b = B07SampleManipulator52B(prefix="TEST:")
14+
return sm52b
15+
16+
17+
@pytest.mark.parametrize(
18+
"x, y, z, xp, yp, zp, roty, rotz, kappa, phi, omega",
19+
[
20+
(1.23, 2.40, 0.0, 0.0, 0.0, 0.0, 1.0, 1.2, 0, 2.0, 1.7),
21+
(2.0, 1.2, 3.51, 24.0, 1.0, 2.0, 5.0, 21.0, 1.0, 25.0, 19.2),
22+
],
23+
)
24+
async def test_sm52b_read(
25+
sm52b: B07SampleManipulator52B,
26+
x: float,
27+
y: float,
28+
z: float,
29+
xp: float,
30+
yp: float,
31+
zp: float,
32+
roty: float,
33+
rotz: float,
34+
kappa: float,
35+
phi: float,
36+
omega: float,
37+
) -> None:
38+
await asyncio.gather(
39+
sm52b.x.set(x),
40+
sm52b.y.set(y),
41+
sm52b.z.set(z),
42+
sm52b.xp.set(xp),
43+
sm52b.yp.set(yp),
44+
sm52b.zp.set(zp),
45+
sm52b.roty.set(roty),
46+
sm52b.rotz.set(rotz),
47+
sm52b.kappa.set(kappa),
48+
sm52b.phi.set(phi),
49+
sm52b.omega.set(omega),
50+
)
51+
await assert_reading(
52+
sm52b,
53+
{
54+
"sm52b-x": partial_reading(x),
55+
"sm52b-y": partial_reading(y),
56+
"sm52b-z": partial_reading(z),
57+
"sm52b-xp": partial_reading(xp),
58+
"sm52b-yp": partial_reading(yp),
59+
"sm52b-zp": partial_reading(zp),
60+
"sm52b-roty": partial_reading(roty),
61+
"sm52b-rotz": partial_reading(rotz),
62+
"sm52b-kappa": partial_reading(kappa),
63+
"sm52b-phi": partial_reading(phi),
64+
"sm52b-omega": partial_reading(omega),
65+
},
66+
)

0 commit comments

Comments
 (0)