-
Notifications
You must be signed in to change notification settings - Fork 12
Add i06-1 magnets #2115
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
Open
oliwenmandiamond
wants to merge
35
commits into
main
Choose a base branch
from
add_i06_magnets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Add i06-1 magnets #2115
Changes from all commits
Commits
Show all changes
35 commits
Select commit
Hold shift + click to select a range
f209651
Create Superconducting magnet device for i06-1
oliwenmandiamond b3f9037
correct pvs
oliwenmandiamond e004d75
Update to user own coordinate device
oliwenmandiamond a4812df
Complete implementation
oliwenmandiamond 56a754d
Add to do comments
oliwenmandiamond d4f3dc7
Add SuperconductingMagnetController
oliwenmandiamond 310be3b
Merge branch 'main' into add_i06_magnets
oliwenmandiamond 71b1cc0
Add RampMagnetController
oliwenmandiamond ac72244
Move ramp controllers to own file
oliwenmandiamond 770d400
Add read methods for spherical coordinates
oliwenmandiamond 706c39c
Rename to ramp controllers to MagnetAxisRampRateController
oliwenmandiamond 3487313
Add ramp_controller tests
oliwenmandiamond 1deca95
Add tests for superconducting_magnet
oliwenmandiamond bc877e9
Merge branch 'main' into add_i06_magnets
oliwenmandiamond 0f3829c
Merge branch 'main' into add_i06_magnets
oliwenmandiamond 712ca91
Add individual spherical axis tests
oliwenmandiamond c2dc441
Merge branch 'add_i06_magnets' of ssh://github.com/DiamondLightSource…
oliwenmandiamond 7da935c
Increased timeout to 600 seconds, no longer a signal
oliwenmandiamond 27389fb
Add order of set test
oliwenmandiamond c45047f
Add missing code coverage
oliwenmandiamond de9bc5c
Andd Flyable and Prepare methods and tests
oliwenmandiamond 1c359d5
Add doc strings to classes
oliwenmandiamond e526270
Refactor magnets to have moment strategies, add better mock behaviour
oliwenmandiamond 7a55ad1
Remove commented out code
oliwenmandiamond 917718d
Fix test
oliwenmandiamond 7bb955b
Update doc strings and add logging
oliwenmandiamond d0282c6
Remove unneeded comment
oliwenmandiamond 55446eb
Add better mocking behaviour
oliwenmandiamond 424d5a3
Update SuperConductingMagnet to SuperConductingMagnetController
oliwenmandiamond c12f482
Add test for no movement strategy
oliwenmandiamond d11a7ee
Improve typing
oliwenmandiamond 665095f
Add check_value logic for the ramp controllers
oliwenmandiamond f8b281e
Rename lakeshore devices to use cooling/heating
oliwenmandiamond 928f0ad
Add missing code coverage
oliwenmandiamond 7317776
Merge branch 'main' into add_i06_magnets
oliwenmandiamond File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| from .movement import MagnetPosition, MagnetPositionError, MagnetSphericalPosition | ||
| from .ramp_controller import ( | ||
| MagnetAxisRampRateController, | ||
| MagnetThreeAxesRampRateController, | ||
| ) | ||
| from .superconducting_magnet import ( | ||
| FlyMagnetInfo, | ||
| MagnetAxis, | ||
| MagnetLimitStatus, | ||
| MagnetModes, | ||
| MagnetRampStatus, | ||
| SuperConductingMagnetController, | ||
| ) | ||
|
|
||
| __all__ = [ | ||
| "MagnetPosition", | ||
| "MagnetPositionError", | ||
| "MagnetSphericalPosition", | ||
| "MagnetAxisRampRateController", | ||
| "MagnetThreeAxesRampRateController", | ||
| "FlyMagnetInfo", | ||
| "MagnetAxis", | ||
| "MagnetLimitStatus", | ||
| "MagnetModes", | ||
| "MagnetRampStatus", | ||
| "SuperConductingMagnetController", | ||
| ] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| from ophyd_async.core import StrictEnum | ||
|
|
||
|
|
||
| class MagnetModes(StrictEnum): | ||
| UNIAXIAL_X = "UNIAXIAL_X" | ||
| UNIAXIAL_Y = "UNIAXIAL_Y" | ||
| UNIAXIAL_Z = "UNIAXIAL_Z" | ||
| SPHERICAL = "SPHERICAL" | ||
| PLANAR_XZ = "PLANAR_XZ" | ||
| QUADRANT_XY = "QUADRANT_XY" | ||
| CUBIC = "CUBIC" | ||
| UNDEFINED = "UNDEFINED" | ||
|
|
||
|
|
||
| class MagnetRampStatus(StrictEnum): | ||
| RAMP_MADE = "RAMP MADE" | ||
| RAMPING = "RAMPING" | ||
|
|
||
|
|
||
| class MagnetLimitStatus(StrictEnum): | ||
| OK = "OK" | ||
| VIOLTATION = "VIOLATION" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,187 @@ | ||
| import math | ||
| from abc import ABC, abstractmethod | ||
| from dataclasses import dataclass | ||
|
|
||
|
|
||
| def read_theta(x: float, z: float) -> float: | ||
| theta = math.degrees(math.atan2(-x, z)) | ||
| return theta % 360 | ||
|
|
||
|
|
||
| def read_rho(x: float, y: float, z: float) -> float: | ||
| return math.hypot(x, y, z) | ||
|
|
||
|
|
||
| def read_phi(x: float, y: float, z: float) -> float: | ||
| return math.degrees(math.atan2(math.hypot(x, z), y)) | ||
|
|
||
|
|
||
| @dataclass(kw_only=True) | ||
| class MagnetPosition: | ||
| x: float | ||
| y: float | ||
| z: float | ||
|
|
||
| def to_spherical(self) -> "MagnetSphericalPosition": | ||
| return MagnetSphericalPosition( | ||
| rho=read_rho(self.x, self.y, self.z), | ||
| theta=read_theta(self.x, self.z), | ||
| phi=read_phi(self.x, self.y, self.z), | ||
| ) | ||
|
|
||
|
|
||
| @dataclass(kw_only=True) | ||
| class MagnetSphericalPosition: | ||
| rho: float | ||
| theta: float # degrees | ||
| phi: float # degrees | ||
|
|
||
| def to_cartesian(self) -> MagnetPosition: | ||
| theta = math.radians(self.theta) | ||
| phi = math.radians(self.phi) | ||
|
|
||
| return MagnetPosition( | ||
| x=-self.rho * math.sin(phi) * math.sin(theta), | ||
| y=self.rho * math.cos(phi), | ||
| z=self.rho * math.sin(phi) * math.cos(theta), | ||
| ) | ||
|
|
||
|
|
||
| @dataclass(frozen=True, kw_only=True) | ||
| class MagnetStep: | ||
| x: float | None = None | ||
| y: float | None = None | ||
| z: float | None = None | ||
|
|
||
|
|
||
| class MagnetPositionError(Exception): | ||
| pass | ||
|
|
||
|
|
||
| class MovementStrategy(ABC): | ||
| @abstractmethod | ||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| """Return a sequence of safe moves.""" | ||
|
|
||
|
|
||
| class SphericalMovement(MovementStrategy): | ||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| steps: list[MagnetStep] = [] | ||
|
|
||
| decreases = [ | ||
| ("z", current.z, target.z), | ||
| ("x", current.x, target.x), | ||
| ("y", current.y, target.y), | ||
| ] | ||
|
|
||
| for axis, old, new in decreases: | ||
| if abs(new) < abs(old): | ||
| kwargs = {axis: new} | ||
| steps.append(MagnetStep(**kwargs)) | ||
|
|
||
| if any(abs(new) > abs(old) for _, old, new in decreases): | ||
| steps.append(MagnetStep(x=target.x, y=target.y, z=target.z)) | ||
|
|
||
| return steps | ||
|
|
||
|
|
||
| class CubicMovement(MovementStrategy): | ||
| LIMIT = 1.5 | ||
|
|
||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| for axis in (target.x, target.y, target.z): | ||
| if abs(axis) > self.LIMIT: | ||
| raise MagnetPositionError( | ||
| f"Target {target} outside cubic operating region limit of {self.LIMIT}" | ||
| ) | ||
|
|
||
| return [MagnetStep(x=target.x, y=target.y, z=target.z)] | ||
|
|
||
|
|
||
| class PlanarXZMovement(MovementStrategy): | ||
| LIMIT = 2.0 | ||
|
|
||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| if target.y != 0: | ||
| raise MagnetPositionError( | ||
| f"Y must remain zero in XZ mode. Requested value was {target.y}" | ||
| ) | ||
|
|
||
| hypot = math.hypot(target.x, target.z) | ||
| if hypot > self.LIMIT: | ||
| raise MagnetPositionError( | ||
| f"Outside XZ operating region. math.hypot(x={target.x}, y={target.y}) = {hypot}. Limit is {self.LIMIT}" | ||
| ) | ||
|
|
||
| return [MagnetStep(x=target.x, z=target.z)] | ||
|
|
||
|
|
||
| @dataclass | ||
| class UniaxialMovement(MovementStrategy): | ||
| axis: str | ||
| limit: float | ||
|
|
||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| coords = {"x": target.x, "y": target.y, "z": target.z} | ||
| for axis, value in coords.items(): | ||
| if axis == self.axis: | ||
| abs_value = abs(value) | ||
| if abs_value > self.limit: | ||
| raise MagnetPositionError( | ||
| f"{axis} value of {abs_value} exceeds limit of {self.limit}" | ||
| ) | ||
| elif value != 0: | ||
| raise MagnetPositionError( | ||
| f"{axis} must remain zero in {self.axis.upper()} mode. Requested value was {value} " | ||
| ) | ||
|
|
||
| return [MagnetStep(**{self.axis: coords[self.axis]})] | ||
|
|
||
|
|
||
| class QuadrantXYMovement(MovementStrategy): | ||
| """Start from zero field in all axes. Appy 2.o T in + y direction. Move field | ||
| direction through positive quadrant to 2.0 T in the +x direction. At all times | ||
| | B | <= 2.0 T. Ramp field back to zero in x direction. | ||
| """ | ||
|
|
||
| LIMIT = 2.0 | ||
|
|
||
| def moves( | ||
| self, current: MagnetPosition, target: MagnetPosition | ||
| ) -> list[MagnetStep]: | ||
| if target.z != 0: | ||
| raise MagnetPositionError( | ||
| f"Z must remain zero in QUADRANT_XY mode. You provided {target.z}" | ||
| ) | ||
|
|
||
| hypot = math.hypot(target.x, target.y) | ||
| if hypot > self.LIMIT: | ||
| raise MagnetPositionError( | ||
| f"Target math.hypot(x={target.x}, y={target.y}) {hypot} is outside the XY operating region limit of {self.LIMIT}." | ||
| ) | ||
|
|
||
| steps: list[MagnetStep] = [] | ||
|
|
||
| # Step 1: remove X component first | ||
| if current.x != 0: | ||
| steps.append(MagnetStep(x=0)) | ||
|
|
||
| # Step 2: move Y | ||
| if current.y != target.y: | ||
| steps.append(MagnetStep(y=target.y)) | ||
|
|
||
| # Step 3: move X to its final value | ||
| if target.x != 0: | ||
| steps.append(MagnetStep(x=target.x)) | ||
|
|
||
| return steps |
76 changes: 76 additions & 0 deletions
76
src/dodal/devices/beamlines/i06_1/magnets/ramp_controller.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| from dataclasses import dataclass | ||
| from functools import cached_property | ||
|
|
||
| from ophyd_async.core import ( | ||
| InstantMovableMock, | ||
| MovableLogic, | ||
| SignalR, | ||
| StandardMovable, | ||
| StandardReadable, | ||
| StandardReadableFormat, | ||
| TimeoutCalculator, | ||
| default_mock_class, | ||
| set_mock_value, | ||
| ) | ||
| from ophyd_async.epics.core import epics_signal_rw | ||
|
|
||
|
|
||
| @dataclass | ||
| class RampRateMovableLogic(MovableLogic[float]): | ||
| limit: SignalR[float] | ||
|
|
||
| async def check_move(self, new_position: float) -> None: | ||
| limit = await self.limit.get_value() | ||
| if new_position > limit: | ||
| raise ValueError( | ||
| f"Requested ramp rate {new_position} exceeds the maximum limit of {limit} for device {self.readback.name}." | ||
| ) | ||
|
|
||
| async def move(self, new_position: float, timeout: TimeoutCalculator): | ||
| await self.setpoint.set(new_position, timeout=timeout()) | ||
|
|
||
|
|
||
| class MockMagnetAxisRampRateController(InstantMovableMock): | ||
| async def connect(self, device: StandardMovable): | ||
| await super().connect(device) | ||
| # Extend to set a sensible default value for the limit. | ||
| set_mock_value(device.limit, 2) # type: ignore | ||
|
|
||
|
|
||
| @default_mock_class(MockMagnetAxisRampRateController) | ||
| class MagnetAxisRampRateController(StandardMovable[float], StandardReadable): | ||
| """Controls the ramp rate of a single superconducting magnet axis. | ||
|
|
||
| Exposes the readback ramp rate, demand ramp rate and the maximum | ||
| permitted ramp rate for one magnet axis. | ||
| """ | ||
|
|
||
| def __init__(self, prefix: str, name: str = ""): | ||
| with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL): | ||
|
oliwenmandiamond marked this conversation as resolved.
|
||
| self.readback = epics_signal_rw(float, prefix + "STS:RAMPRATE:TPM") | ||
| self.demand = epics_signal_rw(float, prefix + "SET:DMD:RAMPRATE:TPM") | ||
| self.limit = epics_signal_rw(float, prefix + "LIM:RAMPRATE:TPM") | ||
| super().__init__(name) | ||
|
|
||
| @cached_property | ||
| def movable_logic(self) -> RampRateMovableLogic: | ||
| return RampRateMovableLogic( | ||
| readback=self.readback, setpoint=self.demand, limit=self.limit | ||
| ) | ||
|
|
||
|
|
||
| class MagnetThreeAxesRampRateController(StandardReadable): | ||
| """Groups the ramp rate controllers for the x, y and z magnet axes. | ||
|
|
||
| This device is passed to :class:`SuperConductingMagnetController` so that each | ||
| :class:`MagnetAxis` can configure its own ramp rate during preparation for | ||
| fly scans. | ||
| """ | ||
|
|
||
| def __init__(self, prefix: str, name: str = ""): | ||
| with self.add_children_as_readables(): | ||
| self.x = MagnetAxisRampRateController(prefix + "-01:") | ||
| self.y = MagnetAxisRampRateController(prefix + "-02:") | ||
| self.z = MagnetAxisRampRateController(prefix + "-03:") | ||
|
|
||
| super().__init__(name) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.