Add i06-1 magnets - #2115
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #2115 +/- ##
==========================================
+ Coverage 99.16% 99.17% +0.01%
==========================================
Files 354 359 +5
Lines 13848 14211 +363
==========================================
+ Hits 13732 14094 +362
- Misses 116 117 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
…/dodal into add_i06_magnets
Relm-Arrowny
left a comment
There was a problem hiding this comment.
I gone through it roughly, I have a few questions
…LightSource/dodal into update_to_ophyd_async_0_20
Relm-Arrowny
left a comment
There was a problem hiding this comment.
The core logic for movement strategies and mode boundary enforcement looks great! I still feel we should make MagnetAxis a pure StandardReadable so we do not bypass the controller's safety logic and eliminates the need to inject mag_within_boundary callbacks into MagnetAxis.
I know you might not be fully convinced t about keeping MagnetAxis read-only and I probably did not explain it well. If you're busy or would prefer, I'm happy to take a stab at the branch to refactor MagnetAxis, and the controller to show you what I had in mind!
| # Do all decreasing of axes first as one step. This ensures we don't | ||
| # quench the magnet. |
There was a problem hiding this comment.
Should: Could we make a ticket and add to this comment to revisit this, the negative first moving restriction is not in the manual and we tried single step move and asked Dirk about it and we believe it should be safe to move everything at once.
| # Do all decreasing of axes first as one step. This ensures we don't | |
| # quench the magnet. | |
| # Do all decreasing of axes first as one step. This ensures we don't | |
| # quench the magnet. | |
| # To do optimise magnet movement #issue. |
|
|
||
|
|
||
| @dataclass | ||
| class UniaxialMovement(MovementStrategy): |
There was a problem hiding this comment.
could/maybe: There is no real reason to check the other inactive axis, since you can only get here if you are in the correct mode so we could just.
@dataclass
class UniaxialMovement(MovementStrategy):
mode: MagnetMode
limit: float
def check_within_limits(
self, current_readback: MagnetPosition, target: MagnetRequest
) -> None:
axis = self.mode.axis_alias
resolved_value = getattr(target.resolve_pos(current_readback), axis)
if resolved_value is not None and abs(resolved_value) > self.limit:
raise MagnetPositionError.axis_outside_limit(
self.mode, self.limit, resolved_value, axis
)
def move_steps(
self, current_readback: MagnetPosition, target: MagnetRequest
) -> list[MagnetRequest]:
axis = self.mode.axis_alias
return [MagnetRequest(**{axis: getattr(target, axis)})]There was a problem hiding this comment.
Same as above
| # Step 1: remove X component first | ||
| if target.x is not None and current_readback.x != 0: | ||
| steps.append(MagnetRequest(x=0)) | ||
|
|
||
| # Step 2: move Y | ||
| if target.y is not None and current_readback.y != target.y: | ||
| steps.append(MagnetRequest(y=target.y)) | ||
|
|
||
| # Step 3: move X to its final value | ||
| if target.x is not None and target.x != 0: | ||
| steps.append(MagnetRequest(x=target.x)) |
There was a problem hiding this comment.
Must: Missing checks to enforce positive quadrant boundaries . We should check x >= 0 and y >= 0 before allowing the move, we should also check H < 2.0T. If we do not do speed matching we must also do decrease first to stop it going beyond 2.0T during move.
| self.readback = epics_signal_r(float, prefix + "STS:RAMPRATE:TPM") | ||
| self.demand = epics_signal_rw(float, prefix + "SET:DMD:RAMPRATE:TPM") | ||
| self.ramp_limit = epics_signal_r(float, prefix + "LIM:RAMPRATE:TPM") | ||
| self.axis_limit = epics_signal_r(float, prefix + "LIM:FIELD:NOW") |
There was a problem hiding this comment.
Should: Axis_limit belong to MagnetAxis.
There was a problem hiding this comment.
In epics it belongs to the power supply and is reflected with the PV. My goal is to model how it appears in epics / hardware so it refeclts it as much as possible. We shouldn't be providing two completely different PV's to a class. We should create two classes that resemble the PV structure and then combine them together with a composite device of the two classes.
Also as it is not used anywhere in the code, I will just remove it. If I did add it back, I would change this to be a power supply class which has the ramp rate class inside and then the additional pv signals such as the limit
| @default_mock_class(DeviceMock) | ||
| # Don't want to sync readback and setpoint. IOC behaviour will move readback to demand | ||
| # once start_ramp is triggered. This logic lives in the SuperConductingMagnetController. | ||
| class MagnetAxis(StandardReadable, StandardMovable[float], Flyable, Preparable): |
There was a problem hiding this comment.
Should: I still think there is no good reason to make MagnetAxis moveable, as we should never allow anyone to move an axis bypass the controller.
If we can do this:
yield from bps.scan(
[detector],
scmc.cart,
MagnetRequest(x=0.0),
MagnetRequest(x=5.0),
num=100
)Then I think every uniaxisal move should go via MagnetCartesianCoordinates,
otherwise an uniaxial device is probably better:
class MagnetUniaxialAxis(
StandardReadable, StandardMovable[float], Flyable, Preparable
):
def __init__(self, controller: "SuperConductingMagnetController", name: str = ""):
self._controller = controller
@AsyncStatus.wrap
async def set(self, value: float):
mode = await self._controller.mode.get_value()
if mode not in (MagnetMode.UNIAXIAL_X, MagnetMode.UNIAXIAL_Y, MagnetMode.UNIAXIAL_Z):
raise RuntimeError("no")
axis = mode.axis_alias
await self._controller.set_within_boundary(MagnetRequest(**{axis: value}))| self.x = MagnetAxis( | ||
| prefix + "X:", MagnetMode.UNIAXIAL_X, ramp_rate.x, mag_within_boundary | ||
| ) | ||
| self.y = MagnetAxis( | ||
| prefix + "Y:", MagnetMode.UNIAXIAL_Y, ramp_rate.y, mag_within_boundary | ||
| ) | ||
| self.z = MagnetAxis( | ||
| prefix + "Z:", MagnetMode.UNIAXIAL_Z, ramp_rate.z, mag_within_boundary | ||
| ) |
There was a problem hiding this comment.
we should make these private and no longer need to pass mag_within_boundary into the axis if we accepte axis should just be device readable.
| async def prepare(self, value: FlyMagnetInfo): | ||
| self._fly_info = value | ||
| await self.ramp_rate_ref().set(value.ramp_rate) | ||
| await self.set(value.start_position) | ||
|
|
||
| @AsyncStatus.wrap | ||
| async def kickoff(self): | ||
| fly_info = error_if_none( | ||
| self._fly_info, | ||
| f"{self.name} must be prepared before attempting to kickoff.", | ||
| ) | ||
| self._fly_status = self.set(fly_info.end_position) | ||
| self._fly_info = None |
There was a problem hiding this comment.
Should: flyable should be move to the controller or it's subclass so mode is check before we try to fly we do not want to try to fly x when in y mode:
@AsyncStatus.wrap
async def prepare(self, value: FlyMagnetInfo):
mode = await self.mode.get_value()
check what is our current mode
do the prepare logic depend on mode.
Fixes #2120
Instructions to reviewer on how to test:
dodal connect i06-1Checks for reviewer
dodal connect ${BEAMLINE}