Skip to content

Commit ec7b7b5

Browse files
Simplify assert rotated function, add doc string, improve type checking
1 parent 0689c23 commit ec7b7b5

File tree

3 files changed

+117
-73
lines changed

3 files changed

+117
-73
lines changed

tests/devices/i05/test_i05_motors.py

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from dodal.devices.i05 import I05Goniometer
99
from tests.devices.i05_shared.rotation_signal_test_util import (
10-
AxesTestConfig,
11-
assert_set_axis_preserves_other,
10+
RotatedCartesianFrameTestConfig,
11+
assert_rotated_axes_are_orthogonal,
1212
)
1313

1414

@@ -21,7 +21,7 @@ def expected_long_read(x: float, y: float, theta: float) -> float:
2121

2222

2323
@pytest.fixture
24-
async def goniometer():
24+
async def goniometer() -> I05Goniometer:
2525
with init_devices(mock=True):
2626
goniometer = I05Goniometer("TEST:")
2727
return goniometer
@@ -50,26 +50,23 @@ async def test_goniometer_read(goniometer: I05Goniometer) -> None:
5050
)
5151

5252

53-
async def test_goniometer_perp_and_long_set(
54-
goniometer: I05Goniometer,
55-
) -> None:
56-
axes_test_config = AxesTestConfig(
53+
async def test_goniometer_perp_and_long_set(goniometer: I05Goniometer) -> None:
54+
frame_config = RotatedCartesianFrameTestConfig(
5755
i_read=goniometer.x.user_readback,
5856
j_read=goniometer.y.user_readback,
59-
i_write=goniometer.x, # type: ignore
60-
j_write=goniometer.y, # type: ignore
57+
i_write=goniometer.x,
58+
j_write=goniometer.y,
6159
angle_deg=goniometer.rotation_angle_deg,
6260
expected_i_read_func=expected_perp_read,
6361
expected_j_read_func=expected_long_read,
6462
i_rotation_axis=goniometer.perp,
6563
j_rotation_axis=goniometer.long,
6664
)
67-
68-
await assert_set_axis_preserves_other(
65+
await assert_rotated_axes_are_orthogonal(
6966
i_val=10,
7067
j_val=5,
7168
angle_deg_val=goniometer.rotation_angle_deg,
7269
new_i_axis_value=20,
7370
new_j_axis_value=20,
74-
axes_config=axes_test_config,
71+
frame_config=frame_config,
7572
)

tests/devices/i05_1/test_i05_1_motors.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from dodal.devices.i05_1 import XYZPolarAzimuthDefocusStage
99
from tests.devices.i05_shared.rotation_signal_test_util import (
10-
AxesTestConfig,
11-
assert_set_axis_preserves_other,
10+
RotatedCartesianFrameTestConfig,
11+
assert_rotated_axes_are_orthogonal,
1212
)
1313

1414

@@ -55,7 +55,6 @@ async def test_xyzpad_stage_read(xyzpad_stage: XYZPolarAzimuthDefocusStage) -> N
5555
xyzpad_stage.azimuth.set(azimuth_angle_deg),
5656
xyzpad_stage.polar.set(polar_angle_deg),
5757
)
58-
5958
await assert_reading(
6059
xyzpad_stage,
6160
{
@@ -76,7 +75,7 @@ async def test_xyzpad_stage_read(xyzpad_stage: XYZPolarAzimuthDefocusStage) -> N
7675
async def test_xyzpad_hor_and_vert_set(
7776
xyzpad_stage: XYZPolarAzimuthDefocusStage,
7877
) -> None:
79-
axes_test_config = AxesTestConfig(
78+
frame_config = RotatedCartesianFrameTestConfig(
8079
i_read=xyzpad_stage.x.user_readback,
8180
j_read=xyzpad_stage.y.user_readback,
8281
i_write=xyzpad_stage.x,
@@ -87,21 +86,20 @@ async def test_xyzpad_hor_and_vert_set(
8786
i_rotation_axis=xyzpad_stage.hor,
8887
j_rotation_axis=xyzpad_stage.vert,
8988
)
90-
91-
await assert_set_axis_preserves_other(
89+
await assert_rotated_axes_are_orthogonal(
9290
i_val=10,
9391
j_val=5,
9492
angle_deg_val=45,
9593
new_i_axis_value=20,
9694
new_j_axis_value=20,
97-
axes_config=axes_test_config,
95+
frame_config=frame_config,
9896
)
9997

10098

10199
async def test_xyzpad_long_and_perp_set(
102100
xyzpad_stage: XYZPolarAzimuthDefocusStage,
103101
) -> None:
104-
axes_test_config = AxesTestConfig(
102+
frame_config = RotatedCartesianFrameTestConfig(
105103
i_read=xyzpad_stage.z.user_readback,
106104
j_read=xyzpad_stage.hor,
107105
i_write=xyzpad_stage.z,
@@ -112,12 +110,11 @@ async def test_xyzpad_long_and_perp_set(
112110
i_rotation_axis=xyzpad_stage.long,
113111
j_rotation_axis=xyzpad_stage.perp,
114112
)
115-
116-
await assert_set_axis_preserves_other(
113+
await assert_rotated_axes_are_orthogonal(
117114
i_val=10,
118115
j_val=5,
119116
angle_deg_val=45,
120117
new_i_axis_value=20,
121118
new_j_axis_value=20,
122-
axes_config=axes_test_config,
119+
frame_config=frame_config,
123120
)

tests/devices/i05_shared/rotation_signal_test_util.py

Lines changed: 100 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111

1212
@dataclass
13-
class AxesTestConfig:
13+
class RotatedCartesianFrameTestConfig:
1414
i_read: SignalR[float]
1515
j_read: SignalR[float]
1616
i_write: Movable[float]
@@ -22,68 +22,118 @@ class AxesTestConfig:
2222
j_rotation_axis: SignalRW[float]
2323

2424

25-
async def assert_set_axis_preserves_other(
26-
*,
25+
async def _set_initial_state(
2726
i_val: float,
2827
j_val: float,
2928
angle_deg_val: float,
30-
new_i_axis_value: float,
31-
new_j_axis_value: float,
32-
axes_config: AxesTestConfig,
29+
frame_config: RotatedCartesianFrameTestConfig,
3330
) -> None:
3431
await asyncio.gather(
35-
maybe_await(axes_config.i_write.set(i_val)),
36-
maybe_await(axes_config.j_write.set(j_val)),
32+
maybe_await(frame_config.i_write.set(i_val)),
33+
maybe_await(frame_config.j_write.set(j_val)),
3734
)
38-
angle_deg = axes_config.angle_deg
39-
if isinstance(angle_deg, Movable):
40-
await maybe_await(angle_deg.set(angle_deg_val))
41-
42-
theta = radians(angle_deg_val)
35+
if isinstance(frame_config.angle_deg, Movable):
36+
await maybe_await(frame_config.angle_deg.set(angle_deg_val))
4337

44-
# Capture invariant component before move
45-
expected_i = new_i_axis_value
46-
expected_j = axes_config.expected_j_read_func(i_val, j_val, theta)
4738

48-
# Move axis to new position
49-
await axes_config.i_rotation_axis.set(expected_i)
50-
51-
# Read back motors
39+
async def _read_rotated_components(
40+
frame_config: RotatedCartesianFrameTestConfig, theta: float
41+
) -> tuple[float, float]:
5242
i_new, j_new = await asyncio.gather(
53-
axes_config.i_read.get_value(), axes_config.j_read.get_value()
43+
frame_config.i_read.get_value(),
44+
frame_config.j_read.get_value(),
5445
)
55-
56-
# Compute rotated components after move
57-
i_val_after = axes_config.expected_i_read_func(i_new, j_new, theta)
58-
j_val_after = axes_config.expected_j_read_func(i_new, j_new, theta)
59-
60-
assert i_val_after == pytest.approx(expected_i)
61-
assert j_val_after == pytest.approx(expected_j)
62-
63-
await asyncio.gather(
64-
maybe_await(axes_config.i_write.set(i_val)),
65-
maybe_await(axes_config.j_write.set(j_val)),
46+
return (
47+
frame_config.expected_i_read_func(i_new, j_new, theta),
48+
frame_config.expected_j_read_func(i_new, j_new, theta),
6649
)
67-
angle_deg = axes_config.angle_deg
68-
if isinstance(angle_deg, Movable):
69-
await maybe_await(angle_deg.set(angle_deg_val))
70-
71-
theta = radians(angle_deg_val)
72-
73-
expected_j = new_j_axis_value
74-
expected_i = axes_config.expected_i_read_func(i_val, j_val, theta)
7550

76-
# Move axis to new position
77-
await axes_config.j_rotation_axis.set(expected_j)
7851

79-
# Read back motors
80-
i_new, j_new = await asyncio.gather(
81-
axes_config.i_read.get_value(), axes_config.j_read.get_value()
52+
async def _assert_setting_axis_preserves_other(
53+
set_axis: SignalRW[float],
54+
set_value: float,
55+
expected_i: float,
56+
expected_j: float,
57+
frame_config: RotatedCartesianFrameTestConfig,
58+
theta: float,
59+
) -> None:
60+
await set_axis.set(set_value)
61+
i_after, j_after = await _read_rotated_components(
62+
frame_config=frame_config, theta=theta
8263
)
64+
assert i_after == pytest.approx(expected_i)
65+
assert j_after == pytest.approx(expected_j)
8366

84-
# Compute rotated components after move
85-
i_val_after = axes_config.expected_i_read_func(i_new, j_new, theta)
86-
j_val_after = axes_config.expected_j_read_func(i_new, j_new, theta)
8767

88-
assert i_val_after == pytest.approx(expected_i)
89-
assert j_val_after == pytest.approx(expected_j)
68+
async def assert_rotated_axes_are_orthogonal(
69+
i_val: float,
70+
j_val: float,
71+
angle_deg_val: float,
72+
new_i_axis_value: float,
73+
new_j_axis_value: float,
74+
frame_config: RotatedCartesianFrameTestConfig,
75+
) -> None:
76+
"""Assert that the virtual rotated axes behave as independent Cartesian
77+
coordinates.
78+
79+
This helper verifies that setting one virtual axis in a rotated frame
80+
updates only that axis while preserving the orthogonal axis in the same
81+
rotated frame.
82+
83+
Specifically, the test:
84+
- Initializes the underlying i/j motors and rotation angle.
85+
- Sets the virtual i' axis and asserts that:
86+
* i' moves to the requested value
87+
* j' remains unchanged
88+
- Resets the system.
89+
- Sets the virtual j' axis and asserts that:
90+
* j' moves to the requested value
91+
* i' remains unchanged
92+
93+
This confirms that the virtual axes form a proper orthogonal Cartesian
94+
coordinate frame, and that inverse rotations are applied correctly when
95+
writing to the underlying motors.
96+
97+
Args:
98+
i_val: Initial value for the underlying i-axis.
99+
j_val: Initial value for the underlying j-axis.
100+
angle_deg_val: Rotation angle (in degrees) defining the rotated frame.
101+
new_i_axis_value: Target value for the virtual i' axis.
102+
new_j_axis_value: Target value for the virtual j' axis.
103+
frame_config: Configuration describing how virtual rotated axes are derived from
104+
the underlying motors, including expected readout functions.
105+
"""
106+
# Test setting i′ preserves j′
107+
await _set_initial_state(
108+
i_val=i_val,
109+
j_val=j_val,
110+
angle_deg_val=angle_deg_val,
111+
frame_config=frame_config,
112+
)
113+
theta = radians(angle_deg_val)
114+
expected_j = frame_config.expected_j_read_func(i_val, j_val, theta)
115+
116+
await _assert_setting_axis_preserves_other(
117+
set_axis=frame_config.i_rotation_axis,
118+
set_value=new_i_axis_value,
119+
expected_i=new_i_axis_value,
120+
expected_j=expected_j,
121+
frame_config=frame_config,
122+
theta=theta,
123+
)
124+
# Test setting j′ preserves i′
125+
await _set_initial_state(
126+
i_val=i_val,
127+
j_val=j_val,
128+
angle_deg_val=angle_deg_val,
129+
frame_config=frame_config,
130+
)
131+
expected_i = frame_config.expected_i_read_func(i_val, j_val, theta)
132+
await _assert_setting_axis_preserves_other(
133+
set_axis=frame_config.j_rotation_axis,
134+
set_value=new_j_axis_value,
135+
expected_i=expected_i,
136+
expected_j=new_j_axis_value,
137+
frame_config=frame_config,
138+
theta=theta,
139+
)

0 commit comments

Comments
 (0)