Skip to content

Commit a1bcbe9

Browse files
Villtordeir17846
andauthored
add PolynomCompoundMotors class (#1471)
* first try doesn't work * same * first working version * refactor input parameter to dict of motors and coefficients * rename few variables * small docstring change * add tests * few tests added * added more tests * remove motor set motor speed from tests * order of variables change * updated docs * update tests * rename few variables inside class * fix tests * finish merging with main * rework docstring * replace simmotor with motor * remove name parameter * rework docstring --------- Co-authored-by: eir17846 <victor.rogalev@diamond.ac.uk>
1 parent 39fc8b9 commit a1bcbe9

File tree

4 files changed

+164
-1
lines changed

4 files changed

+164
-1
lines changed
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from dodal.devices.beamlines.i05.compound_motors import PolynomCompoundMotors
12
from dodal.devices.beamlines.i05.enums import Grating
23

3-
__all__ = ["Grating"]
4+
__all__ = ["Grating", "PolynomCompoundMotors"]
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import asyncio
2+
3+
import numpy as np
4+
from bluesky.protocols import (
5+
Locatable,
6+
Location,
7+
Stoppable,
8+
)
9+
from ophyd_async.core import (
10+
Array1D,
11+
AsyncStatus,
12+
Reference,
13+
StandardReadable,
14+
StandardReadableFormat,
15+
)
16+
from ophyd_async.epics.motor import Motor
17+
18+
19+
class PolynomCompoundMotors(
20+
StandardReadable,
21+
Locatable[float],
22+
Stoppable,
23+
):
24+
"""Compound motor controller that synchronizes the movement of multiple motors
25+
based on polynomial relationships.
26+
27+
When the master motor is moved, all driven (slave) motors are asynchronously moved
28+
to positions calculated using polynomial coefficients from master motor position.
29+
The master motor is always included with a default polynomial coefficient
30+
of [0.0, 1.0], representing a direct mapping. Driven motors' positions are
31+
calculated using NumPy's polynomial evaluation.
32+
33+
Args:
34+
master(Motor): master motor.
35+
driven_dict (dict[Motor, Array1D[np.float64]]): dictionary that defines mapping
36+
of each driven motor to its polynomial coefficients (NumPy array).
37+
name (str,optional): name of the device. Defaults to an empty string.
38+
"""
39+
40+
def __init__(
41+
self,
42+
master: Motor,
43+
driven_dict: dict[Motor, Array1D[np.float64]],
44+
name: str = "",
45+
) -> None:
46+
self.motor_coeff_dict: dict[Reference[Motor], Array1D[np.float64]] = {}
47+
48+
# master motor added with polynomial coeff (0,1)
49+
self.master_ref = Reference(master)
50+
self.motor_coeff_dict[self.master_ref] = np.array([0.0, 1.0])
51+
52+
# slave motors added with coefficients from input parameters
53+
for slave in driven_dict.keys():
54+
self.motor_coeff_dict[Reference(slave)] = driven_dict[slave]
55+
56+
self.add_readables(
57+
[ref().user_readback for ref in self.motor_coeff_dict.keys()],
58+
StandardReadableFormat.HINTED_SIGNAL,
59+
)
60+
super().__init__(name=name)
61+
62+
@AsyncStatus.wrap
63+
async def set(self, new_position: float) -> None:
64+
await asyncio.gather(
65+
*[
66+
ref().set(float(np.polynomial.polynomial.polyval(new_position, coeff)))
67+
for ref, coeff in self.motor_coeff_dict.items()
68+
]
69+
)
70+
71+
async def stop(self, success=False):
72+
await asyncio.gather(*[ref().stop() for ref in self.motor_coeff_dict.keys()])
73+
74+
async def locate(self) -> Location[float]:
75+
return await self.master_ref().locate()

tests/devices/beamlines/i05/__init__.py

Whitespace-only changes.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import numpy as np
2+
import pytest
3+
from bluesky import plan_stubs as bps
4+
from bluesky.run_engine import RunEngine
5+
from ophyd_async.core import init_devices
6+
from ophyd_async.epics.motor import Motor
7+
from ophyd_async.testing import assert_configuration, assert_reading, partial_reading
8+
9+
from dodal.devices.beamlines.i05 import PolynomCompoundMotors
10+
11+
12+
@pytest.fixture
13+
async def x_motor() -> Motor:
14+
async with init_devices(mock=True):
15+
x_motor = Motor("")
16+
return x_motor
17+
18+
19+
@pytest.fixture
20+
async def y_motor() -> Motor:
21+
async with init_devices(mock=True):
22+
y_motor = Motor("")
23+
return y_motor
24+
25+
26+
@pytest.fixture
27+
async def z_motor() -> Motor:
28+
async with init_devices(mock=True):
29+
z_motor = Motor("")
30+
return z_motor
31+
32+
33+
@pytest.fixture
34+
async def mock_compound(
35+
x_motor: Motor,
36+
y_motor: Motor,
37+
z_motor: Motor,
38+
) -> PolynomCompoundMotors:
39+
async with init_devices(mock=True):
40+
mock_compound = PolynomCompoundMotors(
41+
x_motor,
42+
{
43+
y_motor: np.array([1.0, 2.0], dtype=np.float64),
44+
z_motor: np.array([-1.0, 0.5], dtype=np.float64),
45+
},
46+
)
47+
return mock_compound
48+
49+
50+
async def test_config_includes(mock_compound: PolynomCompoundMotors):
51+
await assert_configuration(
52+
mock_compound,
53+
{},
54+
)
55+
56+
57+
async def test_read_includes(mock_compound: PolynomCompoundMotors):
58+
await assert_reading(
59+
mock_compound,
60+
{
61+
"x_motor": partial_reading(0.0),
62+
"y_motor": partial_reading(0.0),
63+
"z_motor": partial_reading(0.0),
64+
},
65+
)
66+
67+
68+
async def test_move(
69+
mock_compound: PolynomCompoundMotors,
70+
run_engine: RunEngine,
71+
):
72+
new_position = 10.0
73+
run_engine(bps.mv(mock_compound, new_position))
74+
for motor, coeff in mock_compound.motor_coeff_dict.items():
75+
expected_position = float(np.polynomial.polynomial.polyval(new_position, coeff))
76+
actual_position = await motor().user_readback.get_value()
77+
assert actual_position == expected_position
78+
79+
80+
async def test_move_and_locate(
81+
mock_compound: PolynomCompoundMotors,
82+
run_engine: RunEngine,
83+
):
84+
new_position = 1.23
85+
run_engine(bps.mv(mock_compound, new_position))
86+
located_position = await mock_compound.locate()
87+
assert located_position == {"setpoint": new_position, "readback": new_position}

0 commit comments

Comments
 (0)