-
Notifications
You must be signed in to change notification settings - Fork 12
Add i09-2 stage using StandardMovable #2093
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
67e7c37
9fd140e
44468b2
ec3b250
4f72275
983b14b
1d1fc1f
c98dd31
e223a43
4c81aa5
3f51519
5018146
dea0e8a
238529d
0033645
26c8056
87c063b
5e405a6
ab853b8
75dbc8f
92640ea
346f026
2edbd27
bc265ae
bbb9ab2
ac0f5e0
79e938b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from .i09_2_motors import ( | ||
| I092SampleManipulator, | ||
| PiezoElectricMotor, | ||
| PiezoElectricMovableLogic, | ||
| ) | ||
|
|
||
| __all__ = ["I092SampleManipulator", "PiezoElectricMotor", "PiezoElectricMovableLogic"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| from dataclasses import dataclass | ||
| from functools import cached_property | ||
|
|
||
| from ophyd_async.core import ( | ||
| SignalW, | ||
| StandardReadable, | ||
| StandardReadableFormat, | ||
| soft_signal_rw, | ||
| ) | ||
| from ophyd_async.epics.core import epics_signal_r, epics_signal_rw, epics_signal_w | ||
| from ophyd_async.epics.motor import Motor | ||
|
|
||
| from dodal.devices.movable import MovableWithTolerance, MovableWithToleranceLogic | ||
|
|
||
|
|
||
| @dataclass | ||
| class PiezoElectricMovableLogic(MovableWithToleranceLogic): | ||
| motor_stop: SignalW[int] | ||
|
|
||
| async def stop(self) -> None: | ||
| await self.motor_stop.set(1) | ||
|
|
||
|
|
||
| class PiezoElectricMotor(MovableWithTolerance): | ||
| """A piezoelectric positioning stage with configurable move tolerance. | ||
|
|
||
| This device exposes EPICS signals for readback, setpoint, and motion stop commands. | ||
| Motion completion is determined by comparing the readback and setpoint positions | ||
| using a configurable tolerance. | ||
| """ | ||
|
|
||
| def __init__(self, prefix: str, tolerance: float = 0.01, name: str = ""): | ||
| with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): | ||
| self.user_readback = epics_signal_r(float, prefix + ":POS:RD") | ||
|
|
||
| self.user_setpoint = epics_signal_rw(float, prefix + ":MOV:RD") | ||
| self.tolerance = soft_signal_rw(float, initial_value=tolerance) | ||
| self.motor_stop = epics_signal_w(int, prefix + ":HLT:WR.PROC") | ||
| super().__init__( | ||
| tolerance=self.tolerance, | ||
| setpoint=self.user_setpoint, | ||
| readback=self.user_readback, | ||
| name=name, | ||
| ) | ||
|
Comment on lines
+35
to
+44
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feel really wrong, |
||
|
|
||
| @cached_property | ||
| def movable_logic(self) -> PiezoElectricMovableLogic: | ||
| return PiezoElectricMovableLogic( | ||
| readback=self.user_readback, | ||
| setpoint=self.user_setpoint, | ||
| within_tolerance=self.within_tolerance, | ||
| motor_stop=self.motor_stop, | ||
| ) | ||
|
|
||
|
|
||
| class I092SampleManipulator(StandardReadable): | ||
| def __init__(self, prefix: str, name: str = ""): | ||
| with self.add_children_as_readables(): | ||
| self.x1 = PiezoElectricMotor(prefix + "X1") | ||
| self.x2 = PiezoElectricMotor(prefix + "X2") | ||
| self.x3 = PiezoElectricMotor(prefix + "X3") | ||
| self.y = PiezoElectricMotor(prefix + "Y") | ||
| self.z1 = PiezoElectricMotor(prefix + "Z1") | ||
| self.z2 = PiezoElectricMotor(prefix + "Z2") | ||
|
|
||
| self.xc = Motor(prefix + "X") | ||
| self.zc = Motor(prefix + "Z") | ||
|
|
||
| super().__init__(name) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,63 @@ | ||||||
| from dataclasses import dataclass | ||||||
| from functools import cached_property | ||||||
|
|
||||||
| from ophyd_async.core import ( | ||||||
| MovableLogic, | ||||||
| Reference, | ||||||
| SignalR, | ||||||
| SignalRW, | ||||||
| StandardMovable, | ||||||
| StandardReadable, | ||||||
| derived_signal_r, | ||||||
| wait_for_value, | ||||||
| ) | ||||||
|
|
||||||
|
|
||||||
| @dataclass | ||||||
| class MovableWithToleranceLogic(MovableLogic[float]): | ||||||
| within_tolerance: SignalR[bool] | ||||||
|
|
||||||
| async def move(self, new_position: float, timeout: float | None) -> None: | ||||||
| await self.setpoint.set(new_position, timeout=timeout) | ||||||
| await wait_for_value(self.setpoint, new_position, timeout=timeout) | ||||||
| # Once setpoint is at new position, we can check for tolerance signal to see if | ||||||
| # true now as the within_tolerance window has updated to the new setpoint | ||||||
| # position. Now it doesn't matter if motor steps are small or large. | ||||||
| await wait_for_value(self.within_tolerance, True, timeout=timeout) | ||||||
|
|
||||||
|
|
||||||
| def _within_tolerance_read(setpoint: float, readback: float, tolerance: float) -> bool: | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is not really doing read?
Suggested change
|
||||||
| return abs(setpoint - readback) < abs(tolerance) | ||||||
|
|
||||||
|
|
||||||
| class MovableWithTolerance(StandardMovable[float], StandardReadable): | ||||||
| """Movable with a signal to configure the tolerance of when the device is done | ||||||
| moving if it the readback and setpoint difference is within the tolerance. | ||||||
| """ | ||||||
|
|
||||||
| def __init__( | ||||||
| self, | ||||||
| tolerance: SignalR[float], | ||||||
| setpoint: SignalRW[float], | ||||||
| readback: SignalR[float], | ||||||
| name: str = "", | ||||||
| ): | ||||||
| # Use reference so sub classes still have flexibility to name signals to what | ||||||
| # they want. | ||||||
| self._setpoint_ref = Reference(setpoint) | ||||||
| self._readback_ref = Reference(readback) | ||||||
| self.within_tolerance = derived_signal_r( | ||||||
| _within_tolerance_read, | ||||||
| tolerance=tolerance, | ||||||
| setpoint=setpoint, | ||||||
| readback=readback, | ||||||
| ) | ||||||
| super().__init__(name) | ||||||
|
|
||||||
| @cached_property | ||||||
| def movable_logic(self) -> MovableWithToleranceLogic: | ||||||
| return MovableWithToleranceLogic( | ||||||
| readback=self._readback_ref(), | ||||||
| setpoint=self._setpoint_ref(), | ||||||
| within_tolerance=self.within_tolerance, | ||||||
| ) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| from unittest.mock import AsyncMock | ||
|
|
||
| import pytest | ||
| from ophyd_async.core import init_devices | ||
| from ophyd_async.testing import assert_reading, partial_reading | ||
|
|
||
| from dodal.devices.beamlines.i09_2 import I092SampleManipulator, PiezoElectricMotor | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def sm() -> I092SampleManipulator: | ||
| with init_devices(mock=True): | ||
| sm = I092SampleManipulator("TEST:") | ||
| return sm | ||
|
|
||
|
|
||
| async def test_sm_read(sm: I092SampleManipulator) -> None: | ||
| await assert_reading( | ||
| sm, | ||
| { | ||
| "sm-x1": partial_reading(0), | ||
| "sm-x2": partial_reading(0), | ||
| "sm-x3": partial_reading(0), | ||
| "sm-y": partial_reading(0), | ||
| "sm-z1": partial_reading(0), | ||
| "sm-z2": partial_reading(0), | ||
| "sm-xc": partial_reading(0), | ||
| "sm-zc": partial_reading(0), | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| async def piezo_motor() -> PiezoElectricMotor: | ||
| with init_devices(mock=True): | ||
| piezo_motor = PiezoElectricMotor("TEST:") | ||
| return piezo_motor | ||
|
|
||
|
|
||
| async def test_piezo_motor_stop(piezo_motor: PiezoElectricMotor) -> None: | ||
| piezo_motor.motor_stop.set = AsyncMock() | ||
| await piezo_motor.stop() | ||
| piezo_motor.motor_stop.set.assert_awaited_once_with(1) | ||
|
|
||
|
|
||
| async def test_piezo_motor_set(piezo_motor: PiezoElectricMotor) -> None: | ||
| await piezo_motor.set(4) | ||
| assert await piezo_motor.user_readback.get_value() == 4 | ||
| assert await piezo_motor.user_setpoint.get_value() == 4 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does epics do the cleanup after motor_stop is call? If a move is currently in progress, it will be stuck awaiting within_tolerance until timeout as the readback never get to the setpoint.