From 6a8e417fa02baf66d9b7c44c417fc50c49051d7a Mon Sep 17 00:00:00 2001 From: eir17846 Date: Fri, 20 Feb 2026 14:55:23 +0000 Subject: [PATCH 1/5] first attempt --- .../beamlines/i05/configuration/__init__.py | 0 .../beamlines/i05/configuration/constants.py | 13 +++ .../beamlines/i05/plans/__init__.py | 0 .../beamlines/i05/plans/m1m3_alignment.py | 92 +++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 src/sm_bluesky/beamlines/i05/configuration/__init__.py create mode 100644 src/sm_bluesky/beamlines/i05/configuration/constants.py create mode 100644 src/sm_bluesky/beamlines/i05/plans/__init__.py create mode 100644 src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py diff --git a/src/sm_bluesky/beamlines/i05/configuration/__init__.py b/src/sm_bluesky/beamlines/i05/configuration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sm_bluesky/beamlines/i05/configuration/constants.py b/src/sm_bluesky/beamlines/i05/configuration/constants.py new file mode 100644 index 00000000..5c8b5c2c --- /dev/null +++ b/src/sm_bluesky/beamlines/i05/configuration/constants.py @@ -0,0 +1,13 @@ +import numpy as np + +M3MJ6_X_OFFSET_1600 = np.array([6.2625, -0.010313, 0.0], dtype=np.float64) +M3MJ6_X_OFFSET_800 = np.array([6.2017, -0.010049, 0.0], dtype=np.float64) + +M3MJ6_PITCH_OFFSET_1600 = np.array( + [4528.4, 0.63741, -1.9103e-4, 5.7109e-8], + dtype=np.float64, +) +M3MJ6_PITCH_OFFSET_800 = np.array( + [4460.8, 0.66544, -0.00017244, 4.4545e-8], + dtype=np.float64, +) diff --git a/src/sm_bluesky/beamlines/i05/plans/__init__.py b/src/sm_bluesky/beamlines/i05/plans/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py new file mode 100644 index 00000000..55dfcdef --- /dev/null +++ b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py @@ -0,0 +1,92 @@ +from bluesky.plans import grid_scan +from bluesky.utils import MsgGenerator +from dodal.beamlines.i05 import devices as i05_devices +from dodal.common import inject +from dodal.device_manager import DeviceManager +from dodal.devices.beamlines.i05_shared import PolynomCompoundMotors +from dodal.devices.common_mirror import XYZPiezoSwitchingMirror +from dodal.devices.motors import Motor, XYZPitchYawRollStage +from ophyd_async.core import StandardReadable + +from sm_bluesky.beamlines.i05.configuration.constants import ( + M3MJ6_PITCH_OFFSET_800, + M3MJ6_PITCH_OFFSET_1600, + M3MJ6_X_OFFSET_800, + M3MJ6_X_OFFSET_1600, +) + +# Define alignment devices and factory functions to create them. +alignment_devices = DeviceManager() + + +@alignment_devices.factory() +def m1es_pitch_800( + m1_collimating_mirror: XYZPitchYawRollStage, + m3mj6_switching_mirror: XYZPiezoSwitchingMirror, +) -> PolynomCompoundMotors: + return PolynomCompoundMotors( + m1_collimating_mirror.pitch, + { + m3mj6_switching_mirror.x: M3MJ6_X_OFFSET_800, + m3mj6_switching_mirror.pitch: M3MJ6_PITCH_OFFSET_800, + }, + ) + + +@alignment_devices.factory() +def m1es_pitch_1600( + m1_collimating_mirror: XYZPitchYawRollStage, + m3mj6_switching_mirror: XYZPiezoSwitchingMirror, +) -> PolynomCompoundMotors: + return PolynomCompoundMotors( + m1_collimating_mirror.pitch, + { + m3mj6_switching_mirror.x: M3MJ6_X_OFFSET_1600, + m3mj6_switching_mirror.pitch: M3MJ6_PITCH_OFFSET_1600, + }, + ) + + +i05_devices.include(alignment_devices) + + +def align_mirrors_800( + m1_start: float, + m1_end: float, + m1_num: int, + m3_start: float, + m3_end: float, + m3_num: int, + det: StandardReadable = inject("dj7current_new"), + m1_pitch: PolynomCompoundMotors = inject("m1es_pitch_800"), + m3_pitch: Motor = inject("m3mj6_switching_mirror.pitch"), +) -> MsgGenerator: + """ + Plan to align the M1 and M3 mirrors by scanning the pitch of the mirror and moving + to the fitted position. + + Parameters + ---------- + det: StandardReadable, + Detector to be use for alignment. + motor: PolynomCompoundMotors + The compound motor that controls the pitch of the mirror. + start: float, + The starting position for the scan. + end: float, + The ending position for the scan. + num: int = 10, + The number of steps in the scan. + """ + + yield from grid_scan( + [det], + m1_pitch, + m1_start, + m1_end, + m1_num, + m3_pitch, + m3_start, + m3_end, + m3_num, + ) From b0fae5915f30d6faa426efcfbf43deba1e1f7e30 Mon Sep 17 00:00:00 2001 From: eir17846 Date: Fri, 20 Feb 2026 15:30:43 +0000 Subject: [PATCH 2/5] pgm detect grating --- .../beamlines/i05/plans/m1m3_alignment.py | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py index 55dfcdef..ce1db8e6 100644 --- a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py +++ b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py @@ -3,10 +3,11 @@ from dodal.beamlines.i05 import devices as i05_devices from dodal.common import inject from dodal.device_manager import DeviceManager -from dodal.devices.beamlines.i05_shared import PolynomCompoundMotors +from dodal.devices.beamlines.i05_shared import Grating, PolynomCompoundMotors from dodal.devices.common_mirror import XYZPiezoSwitchingMirror -from dodal.devices.motors import Motor, XYZPitchYawRollStage -from ophyd_async.core import StandardReadable +from dodal.devices.motors import XYZPitchYawRollStage +from dodal.devices.pgm import PlaneGratingMonochromator +from ophyd_async.core import StandardReadable, StrictEnum from sm_bluesky.beamlines.i05.configuration.constants import ( M3MJ6_PITCH_OFFSET_800, @@ -50,7 +51,7 @@ def m1es_pitch_1600( i05_devices.include(alignment_devices) -def align_mirrors_800( +def map_m1_m3_mirrors_800( m1_start: float, m1_end: float, m1_num: int, @@ -58,35 +59,32 @@ def align_mirrors_800( m3_end: float, m3_num: int, det: StandardReadable = inject("dj7current_new"), - m1_pitch: PolynomCompoundMotors = inject("m1es_pitch_800"), - m3_pitch: Motor = inject("m3mj6_switching_mirror.pitch"), + m3: XYZPiezoSwitchingMirror = inject("m3mj6_switching_mirror"), ) -> MsgGenerator: """ - Plan to align the M1 and M3 mirrors by scanning the pitch of the mirror and moving - to the fitted position. - - Parameters - ---------- - det: StandardReadable, - Detector to be use for alignment. - motor: PolynomCompoundMotors - The compound motor that controls the pitch of the mirror. - start: float, - The starting position for the scan. - end: float, - The ending position for the scan. - num: int = 10, - The number of steps in the scan. + Plan to find optimal alignment of the M1.pitch and M3MJ6.pitch mirrors """ - + if get_pgm_grating() == Grating.PT_800: + m1_pitch = inject("m1es_pitch_800") + elif get_pgm_grating() == Grating.C_1600: + m1_pitch = inject("m1es_pitch_1600") + else: + raise ValueError("Unsupported grating for M1-M3 alignment.") yield from grid_scan( [det], m1_pitch, m1_start, m1_end, m1_num, - m3_pitch, + m3.pitch, m3_start, m3_end, m3_num, ) + + +async def get_pgm_grating( + pgm: PlaneGratingMonochromator = inject("pgm"), +) -> StrictEnum: + reading = await pgm.grating.get_value() + return reading From d8942a255ebef12fcdb9176b6d5a836f25100683 Mon Sep 17 00:00:00 2001 From: eir17846 Date: Fri, 20 Feb 2026 15:55:48 +0000 Subject: [PATCH 3/5] rename --- src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py index ce1db8e6..2cc2c364 100644 --- a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py +++ b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py @@ -51,7 +51,7 @@ def m1es_pitch_1600( i05_devices.include(alignment_devices) -def map_m1_m3_mirrors_800( +def map_m1_m3_mirrors( m1_start: float, m1_end: float, m1_num: int, From 89048be4c2cad496b6e706ec140ce4a55858f025 Mon Sep 17 00:00:00 2001 From: eir17846 Date: Mon, 23 Feb 2026 11:38:35 +0000 Subject: [PATCH 4/5] add docs --- .../beamlines/i05/configuration/constants.py | 8 +-- .../beamlines/i05/plans/m1m3_alignment.py | 49 ++++++++++++++----- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/sm_bluesky/beamlines/i05/configuration/constants.py b/src/sm_bluesky/beamlines/i05/configuration/constants.py index 5c8b5c2c..ab212133 100644 --- a/src/sm_bluesky/beamlines/i05/configuration/constants.py +++ b/src/sm_bluesky/beamlines/i05/configuration/constants.py @@ -1,13 +1,13 @@ import numpy as np -M3MJ6_X_OFFSET_1600 = np.array([6.2625, -0.010313, 0.0], dtype=np.float64) -M3MJ6_X_OFFSET_800 = np.array([6.2017, -0.010049, 0.0], dtype=np.float64) +M3MJ6_X_POLY_1600 = np.array([6.2625, -0.010313, 0.0], dtype=np.float64) +M3MJ6_X_POLY_800 = np.array([6.2017, -0.010049, 0.0], dtype=np.float64) -M3MJ6_PITCH_OFFSET_1600 = np.array( +M3MJ6_PITCH_POLY_1600 = np.array( [4528.4, 0.63741, -1.9103e-4, 5.7109e-8], dtype=np.float64, ) -M3MJ6_PITCH_OFFSET_800 = np.array( +M3MJ6_PITCH_POLY_800 = np.array( [4460.8, 0.66544, -0.00017244, 4.4545e-8], dtype=np.float64, ) diff --git a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py index 2cc2c364..ffaf749f 100644 --- a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py +++ b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py @@ -1,5 +1,5 @@ from bluesky.plans import grid_scan -from bluesky.utils import MsgGenerator +from bluesky.utils import MsgGenerator, plan from dodal.beamlines.i05 import devices as i05_devices from dodal.common import inject from dodal.device_manager import DeviceManager @@ -10,40 +10,56 @@ from ophyd_async.core import StandardReadable, StrictEnum from sm_bluesky.beamlines.i05.configuration.constants import ( - M3MJ6_PITCH_OFFSET_800, - M3MJ6_PITCH_OFFSET_1600, - M3MJ6_X_OFFSET_800, - M3MJ6_X_OFFSET_1600, + M3MJ6_X_POLY_800, + M3MJ6_X_POLY_1600, ) +# At this point in the alignment, we already have nice parameters for the dependence of +# m3mj6_x, but we want to establish the dependence of m3mj6_pitch vs m1es_pitch. +# The GDA scan command would be something like: +# scan m1es_pitch -200 1600 75 m3mj6_pitch 4750 5350 8 waittime 0.1 dj7current_new +# We then need to fit the peak at each value of m1es_pitch, and from the peak positions +# we fit a third order polynomial. With m1es_pitch then well established, we +# subsequently do the focusing scan using the gas cell. +# +# This scan actually takes over an hour, because the step motion of the hexapod +# (m3mj6_pitch) is slow. In principle we could try to do it in a "fly" mode +# with bluesky, which could be a significant speed up. + # Define alignment devices and factory functions to create them. alignment_devices = DeviceManager() +# This is a temp polynomial compound motor that maps the m1es_pitch to the already +# defined m3mj6_x for finding m3 pitch dependence on m1es_pitch. Once we have the +# m3mj6_pitch dependence on m1es_pitch, we can then define the real polynomial +# compound motor for m3mj6_pitch and forget this temp one @alignment_devices.factory() -def m1es_pitch_800( +def m1_m3_x_800( m1_collimating_mirror: XYZPitchYawRollStage, m3mj6_switching_mirror: XYZPiezoSwitchingMirror, ) -> PolynomCompoundMotors: return PolynomCompoundMotors( m1_collimating_mirror.pitch, { - m3mj6_switching_mirror.x: M3MJ6_X_OFFSET_800, - m3mj6_switching_mirror.pitch: M3MJ6_PITCH_OFFSET_800, + m3mj6_switching_mirror.x: M3MJ6_X_POLY_800, }, ) +# This is a temp polynomial compound motor that maps the m1es_pitch to the already +# defined m3mj6_x for finding m3 pitch dependence on m1es_pitch. Once we have the +# m3mj6_pitch dependence on m1es_pitch, we can then define the real polynomial +# compound motor for m3mj6_pitch and forget this temp one @alignment_devices.factory() -def m1es_pitch_1600( +def m1_m3_x_1600( m1_collimating_mirror: XYZPitchYawRollStage, m3mj6_switching_mirror: XYZPiezoSwitchingMirror, ) -> PolynomCompoundMotors: return PolynomCompoundMotors( m1_collimating_mirror.pitch, { - m3mj6_switching_mirror.x: M3MJ6_X_OFFSET_1600, - m3mj6_switching_mirror.pitch: M3MJ6_PITCH_OFFSET_1600, + m3mj6_switching_mirror.x: M3MJ6_X_POLY_1600, }, ) @@ -51,6 +67,13 @@ def m1es_pitch_1600( i05_devices.include(alignment_devices) +# This will produce 2D map of m1es_pitch vs m3mj6_pitch, with the intensity of +# dj7current_new as the value. We then fit the peak at each value of m1es_pitch, +# and from the peak positions we fit a third order polynomial to find the dependence +# of m3mj6_pitch on m1es_pitch. This will amend M3MJ6_PITCH_POLY_800/1600. +# With m1es_pitch then well established, +# we subsequently do the focusing scan using the gas cell. +@plan def map_m1_m3_mirrors( m1_start: float, m1_end: float, @@ -65,9 +88,9 @@ def map_m1_m3_mirrors( Plan to find optimal alignment of the M1.pitch and M3MJ6.pitch mirrors """ if get_pgm_grating() == Grating.PT_800: - m1_pitch = inject("m1es_pitch_800") + m1_pitch = inject("m1_m3_x_800") elif get_pgm_grating() == Grating.C_1600: - m1_pitch = inject("m1es_pitch_1600") + m1_pitch = inject("m1_m3_x_1600") else: raise ValueError("Unsupported grating for M1-M3 alignment.") yield from grid_scan( From 25de75b8d858de63a93f167ac6d23089d5c49a65 Mon Sep 17 00:00:00 2001 From: eir17846 Date: Mon, 23 Feb 2026 11:52:04 +0000 Subject: [PATCH 5/5] add final compound motors m1_m3 --- .../beamlines/i05/plans/m1m3_alignment.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py index ffaf749f..824ed8ff 100644 --- a/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py +++ b/src/sm_bluesky/beamlines/i05/plans/m1m3_alignment.py @@ -10,6 +10,8 @@ from ophyd_async.core import StandardReadable, StrictEnum from sm_bluesky.beamlines.i05.configuration.constants import ( + M3MJ6_PITCH_POLY_800, + M3MJ6_PITCH_POLY_1600, M3MJ6_X_POLY_800, M3MJ6_X_POLY_1600, ) @@ -64,6 +66,38 @@ def m1_m3_x_1600( ) +# This is a final polynomial compound motor that maps the m1es_pitch to the +# m3mj6_x for and m3 pitch. +@alignment_devices.factory() +def m1_m3_x_pitch_800( + m1_collimating_mirror: XYZPitchYawRollStage, + m3mj6_switching_mirror: XYZPiezoSwitchingMirror, +) -> PolynomCompoundMotors: + return PolynomCompoundMotors( + m1_collimating_mirror.pitch, + { + m3mj6_switching_mirror.x: M3MJ6_X_POLY_800, + m3mj6_switching_mirror.pitch: M3MJ6_PITCH_POLY_800, + }, + ) + + +# This is a final polynomial compound motor that maps the m1es_pitch to the +# m3mj6_x for and m3 pitch. +@alignment_devices.factory() +def m1_m3_x_pitch_1600( + m1_collimating_mirror: XYZPitchYawRollStage, + m3mj6_switching_mirror: XYZPiezoSwitchingMirror, +) -> PolynomCompoundMotors: + return PolynomCompoundMotors( + m1_collimating_mirror.pitch, + { + m3mj6_switching_mirror.x: M3MJ6_X_POLY_1600, + m3mj6_switching_mirror.pitch: M3MJ6_PITCH_POLY_1600, + }, + ) + + i05_devices.include(alignment_devices)