-
Notifications
You must be signed in to change notification settings - Fork 12
Add i05 sample stage #1873
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?
Add i05 sample stage #1873
Changes from all commits
462ece5
3756de7
0fc5aec
cbd692a
a107355
0eda406
df80ee8
031e4dd
c5f3441
99dd78a
09fb2a5
e9afad0
7c3f2d4
044178e
3b09018
19d8baf
fdfa9d5
28fd04c
a9d07f1
578e200
cf02211
143ce97
8c68c3f
3fdbcc9
267d70b
0715af9
46b7eb3
31327ad
1a105e1
f914865
7f15174
2bb24a9
cde0f4c
a33e813
41b0bad
a603432
d848d24
a1a1599
a59f535
b31c793
2897073
a28493b
db997ba
fbc91d4
2060152
b2c29d7
26ff07a
da01cb3
8398fb4
c9ec869
0689c23
ec7b7b5
d7db203
ec853de
b715606
8f180b9
8789887
e13e2a2
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 |
|---|---|---|
| @@ -1,19 +1,31 @@ | ||
| from dodal.beamlines.i05_shared import devices as i05_shared_devices | ||
| from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline | ||
| from dodal.device_manager import DeviceManager | ||
| from dodal.devices.beamlines.i05 import I05Goniometer | ||
| from dodal.devices.temperture_controller import Lakeshore336 | ||
| from dodal.log import set_beamline as set_log_beamline | ||
| from dodal.utils import BeamlinePrefix, get_beamline_name | ||
|
|
||
| devices = DeviceManager() | ||
| devices.include(i05_shared_devices) | ||
|
|
||
| BL = get_beamline_name("i05") | ||
| PREFIX = BeamlinePrefix(BL) | ||
| set_log_beamline(BL) | ||
| set_utils_beamline(BL) | ||
|
|
||
| devices = DeviceManager() | ||
| devices.include(i05_shared_devices) | ||
|
|
||
|
|
||
| @devices.factory() | ||
| def sample_temperature_controller() -> Lakeshore336: | ||
| return Lakeshore336(prefix=f"{PREFIX.beamline_prefix}-EA-TCTRL-02:") | ||
|
|
||
|
|
||
| @devices.factory() | ||
| def sa() -> I05Goniometer: | ||
| """Sample Manipulator.""" | ||
| return I05Goniometer( | ||
| f"{PREFIX.beamline_prefix}-EA-SM-01:", | ||
| x_infix="SAX", | ||
| y_infix="SAY", | ||
| z_infix="SAZ", | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| from dodal.devices.beamlines.i05.enums import Grating | ||
| from .enums import Grating | ||
| from .i05_motors import I05Goniometer | ||
|
|
||
| __all__ = ["Grating"] | ||
| __all__ = ["Grating", "I05Goniometer"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,69 @@ | ||||||||||||||||||||
| from dodal.devices.beamlines.i05_shared.rotation_signals import ( | ||||||||||||||||||||
| create_rotational_ij_component_signals, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| from dodal.devices.motors import ( | ||||||||||||||||||||
| _AZIMUTH, | ||||||||||||||||||||
| _POLAR, | ||||||||||||||||||||
| _TILT, | ||||||||||||||||||||
| _X, | ||||||||||||||||||||
| _Y, | ||||||||||||||||||||
| _Z, | ||||||||||||||||||||
| XYZPolarAzimuthTiltStage, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| class I05Goniometer(XYZPolarAzimuthTiltStage): | ||||||||||||||||||||
| """Six-axis stage with a standard xyz stage and three axis of rotation: polar, | ||||||||||||||||||||
|
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.
Suggested change
|
||||||||||||||||||||
| azimuth, and tilt. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| In addition, it defines two virtual translational axes, `perp` and `long`, which | ||||||||||||||||||||
| form a rotated Cartesian frame within the x-y plane. | ||||||||||||||||||||
|
|
||||||||||||||||||||
| - `long`: Translation along the longitudinal direction of the rotated in-plane | ||||||||||||||||||||
| coordinate frame defined by ``rotation_angle_deg``. | ||||||||||||||||||||
|
Comment on lines
+22
to
+23
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.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| - `perp`: Translation perpendicular to `long` within the x-y plane. | ||||||||||||||||||||
|
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.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| The `perp` and `long` axes are derived from the underlying x and y motors using a | ||||||||||||||||||||
| fixed rotation angle (default 50 degrees). From the user's point of view, these | ||||||||||||||||||||
| behave as ordinary orthogonal Cartesian translation axes aligned with physically | ||||||||||||||||||||
| meaningful directions on the sample, while internally coordinating motion of the x | ||||||||||||||||||||
| and y motors. | ||||||||||||||||||||
|
Comment on lines
+27
to
+31
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.
Suggested change
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Unlike sample-frame axes that rotate with a live rotation motor, these axes are | ||||||||||||||||||||
| defined at a constant orientation set by `rotation_angle_deg`. | ||||||||||||||||||||
|
Comment on lines
+33
to
+34
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.
Suggested change
|
||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def __init__( | ||||||||||||||||||||
| self, | ||||||||||||||||||||
| prefix: str, | ||||||||||||||||||||
| x_infix: str = _X, | ||||||||||||||||||||
| y_infix: str = _Y, | ||||||||||||||||||||
| z_infix: str = _Z, | ||||||||||||||||||||
| polar_infix: str = _POLAR, | ||||||||||||||||||||
| azimuth_infix: str = _AZIMUTH, | ||||||||||||||||||||
| tilt_infix: str = _TILT, | ||||||||||||||||||||
| rotation_angle_deg: float = 50.0, | ||||||||||||||||||||
| name: str = "", | ||||||||||||||||||||
| ): | ||||||||||||||||||||
| self.rotation_angle_deg = rotation_angle_deg | ||||||||||||||||||||
|
|
||||||||||||||||||||
| super().__init__( | ||||||||||||||||||||
| prefix, | ||||||||||||||||||||
| name, | ||||||||||||||||||||
| x_infix=x_infix, | ||||||||||||||||||||
| y_infix=y_infix, | ||||||||||||||||||||
| z_infix=z_infix, | ||||||||||||||||||||
| polar_infix=polar_infix, | ||||||||||||||||||||
| azimuth_infix=azimuth_infix, | ||||||||||||||||||||
| tilt_infix=tilt_infix, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| with self.add_children_as_readables(): | ||||||||||||||||||||
| self.perp, self.long = create_rotational_ij_component_signals( | ||||||||||||||||||||
| i_read=self.x.user_readback, | ||||||||||||||||||||
| i_write=self.x, | ||||||||||||||||||||
| j_read=self.y.user_readback, | ||||||||||||||||||||
| j_write=self.y, | ||||||||||||||||||||
| angle_deg=self.rotation_angle_deg, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .i05_1_motors import XYZPolarAzimuthDefocusStage | ||
|
|
||
| __all__ = ["XYZPolarAzimuthDefocusStage"] |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,75 @@ | ||||||||||||||||||||||
| from ophyd_async.epics.motor import Motor | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| from dodal.devices.beamlines.i05_shared.rotation_signals import ( | ||||||||||||||||||||||
| create_rotational_ij_component_signals, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| from dodal.devices.motors import XYZPolarAzimuthStage | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| class XYZPolarAzimuthDefocusStage(XYZPolarAzimuthStage): | ||||||||||||||||||||||
| """Six-axis stage with a standard xyz stage and three axis of rotation: polar, | ||||||||||||||||||||||
| azimuth, and defocus. | ||||||||||||||||||||||
|
Comment on lines
+10
to
+11
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.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This device exposes four virtual translational axes that are defined in frames | ||||||||||||||||||||||
| of reference attached to the sample: | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - `hor` and `vert`: | ||||||||||||||||||||||
| Horizontal and vertical translation axes in the sample frame. | ||||||||||||||||||||||
| These axes are derived from the lab-frame x and y motors and rotate | ||||||||||||||||||||||
| with the azimuth angle, so that motion along `hor` and `vert` | ||||||||||||||||||||||
| remains aligned with the sample regardless of its azimuthal rotation. | ||||||||||||||||||||||
|
Comment on lines
+17
to
+20
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.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| - `long` and `perp`: | ||||||||||||||||||||||
| Longitudinal and perpendicular translation axes in the tilted sample | ||||||||||||||||||||||
| frame. These axes are derived from the lab-frame z motor and the | ||||||||||||||||||||||
| sample-frame `hor` axis, and rotate with the polar angle. | ||||||||||||||||||||||
| Motion along `long` follows the sample's longitudinal direction, | ||||||||||||||||||||||
| while `perp` moves perpendicular to it within the polar rotation plane. | ||||||||||||||||||||||
|
Comment on lines
+23
to
+27
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.
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| All four virtual axes behave as ordinary orthogonal Cartesian translations | ||||||||||||||||||||||
| from the user's point of view, while internally coordinating motion of the | ||||||||||||||||||||||
| underlying motors to account for the current rotation angles. | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| This allows users to position the sample in physically meaningful, sample-aligned | ||||||||||||||||||||||
| coordinates without needing to manually compensate for azimuth or polar rotations. | ||||||||||||||||||||||
| """ | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| def __init__( | ||||||||||||||||||||||
| self, | ||||||||||||||||||||||
| prefix: str, | ||||||||||||||||||||||
| x_infix="SMX", | ||||||||||||||||||||||
| y_infix="SMY", | ||||||||||||||||||||||
| z_infix="SMZ", | ||||||||||||||||||||||
| polar_infix="POL", | ||||||||||||||||||||||
| azimuth_infix="AZM", | ||||||||||||||||||||||
| defocus_infix="SMDF", | ||||||||||||||||||||||
| name="", | ||||||||||||||||||||||
| ): | ||||||||||||||||||||||
| super().__init__( | ||||||||||||||||||||||
| prefix, | ||||||||||||||||||||||
| name, | ||||||||||||||||||||||
| x_infix=x_infix, | ||||||||||||||||||||||
| y_infix=y_infix, | ||||||||||||||||||||||
| z_infix=z_infix, | ||||||||||||||||||||||
| polar_infix=polar_infix, | ||||||||||||||||||||||
| azimuth_infix=azimuth_infix, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| with self.add_children_as_readables(): | ||||||||||||||||||||||
| self.defocus = Motor(prefix + defocus_infix) | ||||||||||||||||||||||
| self.hor, self.vert = create_rotational_ij_component_signals( | ||||||||||||||||||||||
| i_read=self.x.user_readback, | ||||||||||||||||||||||
| j_read=self.y.user_readback, | ||||||||||||||||||||||
| i_write=self.x, | ||||||||||||||||||||||
| j_write=self.y, | ||||||||||||||||||||||
| angle_deg=self.azimuth.user_readback, | ||||||||||||||||||||||
| clockwise_frame=True, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| self.long, self.perp = create_rotational_ij_component_signals( | ||||||||||||||||||||||
| i_read=self.z.user_readback, | ||||||||||||||||||||||
| i_write=self.z, | ||||||||||||||||||||||
| j_read=self.hor, | ||||||||||||||||||||||
| j_write=self.hor, | ||||||||||||||||||||||
| angle_deg=self.polar.user_readback, | ||||||||||||||||||||||
| clockwise_frame=False, | ||||||||||||||||||||||
| ) | ||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,89 @@ | ||||||
| import asyncio | ||||||
| from math import radians | ||||||
|
|
||||||
| from bluesky.protocols import Movable | ||||||
| from bluesky.utils import maybe_await | ||||||
| from ophyd_async.core import ( | ||||||
| SignalR, | ||||||
| SignalRW, | ||||||
| derived_signal_rw, | ||||||
| ) | ||||||
|
|
||||||
| from dodal.common.maths import rotate_clockwise, rotate_counter_clockwise | ||||||
|
|
||||||
|
|
||||||
| async def _get_angle_deg(angle_deg: SignalR[float] | float) -> float: | ||||||
| if isinstance(angle_deg, SignalR): | ||||||
| return await angle_deg.get_value() | ||||||
| return angle_deg | ||||||
|
|
||||||
|
|
||||||
| def create_rotational_ij_component_signals( | ||||||
| i_read: SignalR[float], | ||||||
| j_read: SignalR[float], | ||||||
| i_write: Movable[float], | ||||||
| j_write: Movable[float], | ||||||
|
Comment on lines
+22
to
+25
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. Why is there a separation of read and write if there could be just one SignalRW ? |
||||||
| angle_deg: float | SignalR[float], | ||||||
| clockwise_frame: bool = True, | ||||||
| ) -> tuple[SignalRW[float], SignalRW[float]]: | ||||||
| """Create virtual i/j signals representing a Cartesian coordinate frame | ||||||
|
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.
Suggested change
|
||||||
| that is rotated by a given angle relative to the underlying equipment axes. | ||||||
|
|
||||||
| The returned signals expose the position of the system in a *rotated frame | ||||||
| of reference* (e.g. the sample or stage frame), while transparently mapping | ||||||
| reads and writes onto the real i/j signals in the fixed equipment (lab) frame. | ||||||
|
|
||||||
| From the user's point of view, i and j behave like ordinary orthogonal | ||||||
| Cartesian axes attached to the rotating object. Internally, all reads apply | ||||||
| a rotation to the real motor positions, and all writes apply the inverse | ||||||
| rotation so that the requested motion is achieved in the rotated frame. | ||||||
|
|
||||||
| Args: | ||||||
| i_read (SignalR): SignalR representing the i motor readback. | ||||||
| j_read (SignalR): representing the j motor readback. | ||||||
| i_write (Movable): object for setting the i position. | ||||||
| j_write (Movable): object for setting the j position. | ||||||
| angle_deg (float | SignalR): Rotation angle in degrees. | ||||||
| clockwise_frame (boolean): If True, the rotated frame is defined using a | ||||||
| clockwise rotation; otherwise, a counter-clockwise rotation is used. | ||||||
|
|
||||||
| Returns: | ||||||
| tuple[SignalRW[float], SignalRW[float]] Two virtual read/write signals | ||||||
| corresponding to the rotated i and j components. | ||||||
| """ | ||||||
| rotate = rotate_clockwise if clockwise_frame else rotate_counter_clockwise | ||||||
| inverse_rotate = rotate_counter_clockwise if clockwise_frame else rotate_clockwise | ||||||
|
|
||||||
| async def _read_rotated() -> tuple[float, float, float]: | ||||||
| i, j, ang = await asyncio.gather( | ||||||
| i_read.get_value(), | ||||||
| j_read.get_value(), | ||||||
| _get_angle_deg(angle_deg), | ||||||
| ) | ||||||
| return (*rotate(radians(ang), i, j), ang) | ||||||
|
|
||||||
| async def _write_rotated(i_rot: float, j_rot: float, ang: float) -> None: | ||||||
| i_new, j_new = inverse_rotate(radians(ang), i_rot, j_rot) | ||||||
| await asyncio.gather( | ||||||
| maybe_await(i_write.set(i_new)), | ||||||
| maybe_await(j_write.set(j_new)), | ||||||
| ) | ||||||
|
|
||||||
| def _read_i(i: float, j: float, ang: float) -> float: | ||||||
| return rotate(radians(ang), i, j)[0] | ||||||
|
|
||||||
| async def _set_i(value: float) -> None: | ||||||
| i_rot, j_rot, ang = await _read_rotated() | ||||||
| await _write_rotated(value, j_rot, ang) | ||||||
|
|
||||||
| def _read_j(i: float, j: float, ang: float) -> float: | ||||||
| return rotate(radians(ang), i, j)[1] | ||||||
|
|
||||||
| async def _set_j(value: float) -> None: | ||||||
| i_rot, j_rot, ang = await _read_rotated() | ||||||
| await _write_rotated(i_rot, value, ang) | ||||||
|
|
||||||
| return ( | ||||||
| derived_signal_rw(_read_i, _set_i, i=i_read, j=j_read, ang=angle_deg), | ||||||
| derived_signal_rw(_read_j, _set_j, i=i_read, j=j_read, ang=angle_deg), | ||||||
| ) | ||||||
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.
I don't think you actually need 2 functions rotate_cw and rotate_ccw - you can rotate ccw using cw function by changing angle alpha either to (-1)alpha or to (2Pi-alpha)