-
Notifications
You must be signed in to change notification settings - Fork 12
Support for mod-360 omega #1929
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
491ba34
fc52d40
61b8c6a
c14006a
8a8e448
efb02eb
758d609
fc725d0
870454e
00bac27
43dfc2d
26a8934
fa2ea42
aeed52a
95b0a6f
80649ec
e2a80c1
0d0c428
7c54725
b19a9a8
5ba6cd2
7c634b6
41e09c5
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 |
|---|---|---|
|
|
@@ -14,7 +14,8 @@ | |
| from ophyd_async.epics.core import epics_signal_r, epics_signal_rw | ||
| from ophyd_async.epics.motor import Motor | ||
|
|
||
| from dodal.devices.motors import XYZOmegaStage | ||
| from dodal.common.maths import AngleWithPhase | ||
| from dodal.devices.motors import XYZWrappedOmegaStage | ||
| from dodal.devices.util.epics_util import SetWhenEnabled | ||
|
|
||
|
|
||
|
|
@@ -78,7 +79,7 @@ class CombinedMove(TypedDict, total=False): | |
| chi: float | None | ||
|
|
||
|
|
||
| class Smargon(XYZOmegaStage, Movable): | ||
| class Smargon(XYZWrappedOmegaStage, Movable): | ||
|
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 is the only device that uses the wrapped omega axis. Rather than creating class Smargon(XYZWrappedOmegaStage, Movable[float]):
def __init__(self, prefix: str, name: str = ""):
super().__init__(prefix=prefix, name=name)
with self.add_children_as_readables():
self.wrapped_omega = WrappedAxis(self.omega)
# Rest of smargon logic
...
Contributor
Author
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.
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. Hmmm. My preference would be to still use @pydantic.dataclasses.dataclass(config={"arbitrary_types_allowed": True})
class PinTipCentringComposite:
"""All devices which are directly or indirectly required by this plan"""
oav: OAV
gonio: XYZOmegaStage
gonio_wrapped_axis: WrappedAxis
pin_tip_detection: PinTipDetectionThe reason being is this makes it way more generic. Any axis can now be wrapped. This means we don't need to create a new subclass for every possible stage combination which needs an axis wrapped. However, looking at it more I'm not sure this would be supported by BlueAPI as it is a child device and from what I can tell, composites are extracted from being a unique type (correct me if wrong?). |
||
| """Real motors added to allow stops following pin load (e.g. real_x1.stop() ) | ||
| X1 and X2 real motors provide compound chi motion as well as the compound X travel, | ||
| increasing the gap between x1 and x2 changes chi, moving together changes virtual x. | ||
|
|
@@ -104,6 +105,15 @@ def __init__(self, prefix: str, name: str = ""): | |
|
|
||
| super().__init__(prefix, name) | ||
|
|
||
| async def _get_target_value(self, motor_name: str, value: float) -> float: | ||
| if motor_name == "omega": | ||
| current_angle = AngleWithPhase.from_iterable( | ||
| await self.wrapped_omega.offset_and_phase.get_value() | ||
| ) | ||
| return current_angle.nearest_with_phase(value).unwrap() | ||
| else: | ||
| return value | ||
|
|
||
| @AsyncStatus.wrap | ||
| async def set(self, value: CombinedMove): | ||
|
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. Should: I'm not sure I like that the co-ordinate system of this set is different to that of
Contributor
Author
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. I think making the underlying motor object represent phase is a bad idea for reasons I will state elsewhere in this PR, however I agree that having the omega coordinate as the only wrapped value in
|
||
| """This will move all motion together in a deferred move. | ||
|
|
@@ -114,11 +124,16 @@ async def set(self, value: CombinedMove): | |
| only come back after the motion on that axis finished. | ||
| """ | ||
| await self.defer_move.set(DeferMoves.ON) | ||
| # TODO Hotfix required here until https://github.com/DiamondLightSource/dodal/issues/1998 | ||
| # is implemented in separate PR | ||
| try: | ||
| finished_moving = [] | ||
| for motor_name, new_setpoint in value.items(): | ||
| if new_setpoint is not None and isinstance(new_setpoint, int | float): | ||
| axis: Motor = getattr(self, motor_name) | ||
| new_setpoint = await self._get_target_value( | ||
| motor_name, new_setpoint | ||
| ) | ||
| axis = getattr(self, motor_name) | ||
| await axis.check_motor_limit( | ||
| await axis.user_setpoint.get_value(), new_setpoint | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| import numpy as np | ||
| from ophyd_async.core import ( | ||
| Reference, | ||
| StandardReadable, | ||
| StandardReadableFormat, | ||
| derived_signal_rw, | ||
| ) | ||
| from ophyd_async.epics.motor import Motor | ||
|
|
||
| from dodal.common.maths import AngleWithPhase, MotorOffsetAndPhase | ||
|
|
||
|
|
||
| class WrappedAxis(StandardReadable): | ||
| """This device is a wrapper around a rotational Motor that presents the unwrapped coordinate system of the | ||
|
rtuck99 marked this conversation as resolved.
|
||
| underlying motor as a combination of a phase angle and an offset from the motor's origin coordinate. | ||
|
|
||
| Attributes: | ||
| phase (float): This is a read-write signal that corresponds to the motor's phase angle, relative to the offset. | ||
| The behaviour of the phase signal differs from that of the underlying motor setpoint/readback signal | ||
| in the following ways: | ||
| * Readback values are constrained to 0 <= omega < 360 | ||
| * Write values are normalised to 0 <= omega < 360, and then mapped to the nearest unwrapped angle. The | ||
| underlying motor will be moved via the shortest path to the required phase angle, thus the direction | ||
| of an unwrapped axis move is not always the same as the direction of a move in the wrapped axis. | ||
| * Write values are normalised so for un-normalised writes, the readback will differ. | ||
| * Bluesky mvr operations greater of 180 degrees or more will not result in the expected moves. | ||
| * set_and_wait_for_value() is unreliable close to the wrap-around (however in the common case this is | ||
| where deadband is 0.001 and values are rounded to this level, the default equality comparison is sufficient). | ||
| * Sequences of moves on the unwrapped axis that would not result in cumulative motion axis will | ||
| result in a cumulative motion in the wrapped axis. e.g. 0 -> 120 -> 240 -> 0 is 0 degrees of real | ||
| cumulative motion when performed in the unwrapped case, but is 360 degrees of real motion when performed | ||
| on the wrapped axis. | ||
| * The reverse is also true 0 -> 240 -> 360 -> is 0 degrees of real motion when executed on the wrapped axis | ||
| but 360 degrees when performed in the unwrapped axis. | ||
| * The above means that use of phase to set motor position is unsuitable for axes | ||
| where the underlying motor rotation is constrained. | ||
| offset_and_phase (Array1D[np.float32]): retrieve the offset and phase together, for use when | ||
| mapping to the underlying unwrapped axis. These values can be converted and manipulated using the | ||
| AngleWithPhase helper class. | ||
| """ | ||
|
|
||
| def __init__(self, real_motor: Motor, name=""): | ||
| self._real_motor = Reference(real_motor) | ||
| with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): | ||
| self.offset_and_phase = derived_signal_rw( | ||
| self._get_motor_offset_and_phase, | ||
| self._set_motor_offset_and_phase, | ||
| motor_pos=real_motor, | ||
| ) | ||
| with self.add_children_as_readables(): | ||
| self.phase = derived_signal_rw( | ||
| self._get_phase, self._set_phase, offset_and_phase=self.offset_and_phase | ||
| ) | ||
|
Comment on lines
+50
to
+53
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. We have a signal that has
Contributor
Author
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. phase is very convenient to directly read and write from plans, most plans are not interested in absolute position, but the extra information is useful when you are.
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. Would it not make more sense to just have one signal for |
||
| super().__init__(name=name) | ||
|
|
||
| def _get_motor_offset_and_phase(self, motor_pos: float) -> MotorOffsetAndPhase: | ||
| angle = AngleWithPhase.wrap(motor_pos) | ||
| return np.array([angle.offset, angle.phase]) | ||
|
|
||
| async def _set_motor_offset_and_phase(self, value: MotorOffsetAndPhase): | ||
| await self._real_motor().set(AngleWithPhase.from_iterable(value).unwrap()) | ||
|
|
||
| def _get_phase(self, offset_and_phase: MotorOffsetAndPhase) -> float: | ||
| return offset_and_phase[1].item() | ||
|
|
||
| async def _set_phase(self, value: float): | ||
| """Set the motor phase to the specified phase value in degrees. | ||
| The motor will travel via the shortest distance path. | ||
| """ | ||
| offset_and_phase = await self.offset_and_phase.get_value() | ||
| current_position = AngleWithPhase.from_iterable(offset_and_phase) | ||
| target_value = current_position.nearest_with_phase(value).unwrap() | ||
| await self._real_motor().set(target_value) | ||
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.
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.
See my comment below re Smargon