Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1929 +/- ##
==========================================
- Coverage 99.12% 99.08% -0.04%
==========================================
Files 319 319
Lines 12201 12260 +59
==========================================
+ Hits 12094 12148 +54
- Misses 107 112 +5 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
Sorry to jump in here, but I am working on a Secondly, I don't think you should be altering the existing |
jacob720
left a comment
There was a problem hiding this comment.
Looks good! Logic is clean and easy to follow
| self.omega = Motor(prefix + omega_infix) | ||
| real_motor = Motor(prefix + omega_infix) | ||
| self.omega = real_motor | ||
| self.omega_axis = WrappedAxis(self.omega) |
There was a problem hiding this comment.
Should something similar be added to XYZThetaStage or anything else with a rotational axis?
| set_mock_value(stage.omega.user_readback, real_value) | ||
| offset, phase = await stage.omega_axis.offset_and_phase.get_value() | ||
| assert 0 <= expected_phase < 360 | ||
| assert math.isclose(phase, expected_phase, abs_tol=1e-6) |
There was a problem hiding this comment.
Could: add expected offset to this test
DominicOram
left a comment
There was a problem hiding this comment.
Thanks, some comments
| only come back after the motion on that axis finished. | ||
| """ | ||
| await self.defer_move.set(DeferMoves.ON) | ||
| # TODO something something i03 broken smargon serialise moves workaround |
There was a problem hiding this comment.
Should: Can this be an issue please?
| await axis.user_setpoint.get_value(), new_setpoint | ||
| ) | ||
| put_completion = await set_and_wait_for_value( | ||
| axis.user_setpoint, |
There was a problem hiding this comment.
Should: I don't really like the check against the name "omega" above, it would be much cleaner if we can make the omega object more like a thing that just handles the logic itself. I think if we change the phase signal to be user_setpoint in WrappedAxis then we can just remove the logic in here for getting the target value and we can set it like any other motor?
| self.omega = Motor(prefix + omega_infix) | ||
| real_motor = Motor(prefix + omega_infix) | ||
| self.omega = real_motor | ||
| self.omega_axis = WrappedAxis(self.omega) |
There was a problem hiding this comment.
Should: I agree with @oliwenmandiamond that we shouldn't be polluting the common XYZOmegaStage with this logic. We should do that overriding when we instantiate the smargon. Maybe the cleanest way to do this is to change these collection classes to protocols?
There was a problem hiding this comment.
I think having a protocol is overkill as were effectively defining the device twice, more boilerplate.
It should be done like this:
class MyCustomStage(XYZOmegaStage):
def __init__(
self,
prefix: str,
name: str = "",
x_infix: str = _X,
y_infix: str = _Y,
z_infix: str = _Z,
omega_infix: str = _OMEGA,
) -> None:
super().__init__(prefix, name, x_infix, y_infix, z_infix, omega_infix)
with self.add_children_as_readables():
self.omega_axis = WrappedAxis(self.omega)This is how every beamline has defined stage. If it is common and generic, it goes in dodal/devices/motors.py.
If it is beamline specific, it should be defined as dodal/devices/beamlines/iXX/my_device.py. For example i05, https://github.com/DiamondLightSource/dodal/blob/main/src/dodal/devices/beamlines/i05/i05_motors.py and i05_1 https://github.com/DiamondLightSource/dodal/blob/main/src/dodal/devices/beamlines/i05_1/i05_1_motors.py
If this is used mx wide, it should go in dodal/devices/my_mx_specific_device.py
There was a problem hiding this comment.
I think it should be a protocol 'cos I think we should be overriding omega completely, rather than creating a second wrapped axis. I think this helps downstream plans not worry so much about this logic and just use omega, making them more generic across MX
There was a problem hiding this comment.
Okay, I see. I was referring to the current implementation.
Yes a protocol could work. If this could wait for [StandardMovable](https://github.com/bluesky/ophyd-async/pull/1200), you could make the protocol
class XYZOmega(Protocol):
x: StandardMovable
y: StandardMovable
z: StandardMovable
omega: StandardMovableThen you could implement your own MovableLogic rather than use the MotorMovableLogic, then it doesn't matter what the implementation is for the plan.
| return value | ||
|
|
||
| @AsyncStatus.wrap | ||
| async def set(self, value: CombinedMove): |
There was a problem hiding this comment.
Should: I'm not sure I like that the co-ordinate system of this set is different to that of omega.set. I think it is possible to get the underlying motor object to always be moving based on phase.
| class WrappedAxis(StandardReadable): | ||
| 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 | ||
| ) | ||
| 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_offset_and_phase(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_offset_and_phase(offset_and_phase) | ||
| target_value = current_position.nearest_to_phase(value).unwrap() | ||
| await self._real_motor().set(target_value) | ||
|
|
||
| def distance(self, theta1_deg: float, theta2_deg: float) -> float: | ||
| """Obtain the shortest distance between theta2 and theta1 in degrees. | ||
| This will be the shortest distance in mod360 space (i.e. always <= 180 degrees). | ||
| """ | ||
| return AngleWithPhase.wrap(theta1_deg).phase_distance( | ||
| AngleWithPhase.wrap(theta2_deg) | ||
| ) |
There was a problem hiding this comment.
This isn't common maths. This is a device and should live with where you define your custom stage.
Change calculate_motion_profile to start at the neareset omega
585d794 to
a95aa88
Compare
Required for
Introduces a
mod_360_motorparameter to the init method forXYZOmegaStage; when set to True this replaces the standard omega motor with an instance ofModMotorwhich delegates motion to the underlying omega motor with the difference that the omega values are remapped to 0<=omega<360.When making an absolute move on the ModMotor:
Instructions to reviewer on how to test:
Checks for reviewer
dodal connect ${BEAMLINE}