Detector Z-rail alignment to the beam
Walk the Optique Peter detector along its 1 m Z stage with a small square X-ray aperture defined by the upstream slits, watch the centroid drift, and use the detector optical table beneath the Z stage to rotate the rail back parallel to the beam.
This procedure removes the linear misalignment between the rail axis and the beam axis. The non-linear residual after this procedure is the intrinsic straightness of the PRO225SL-1000 rail (~9.5 µm horizontal and vertical straightness per the datasheet) — that floor is not correctable from the table.
For the hardware reference (table virtual axes, motor map, PV prefixes), see Beamline components.
Name
detector_z_rail_alignment
Source
Implementation: procedures/detector_z_rail_alignment.py
Release notes: 2bm-procedures CHANGELOG
Devices
Beamline components: PropagationDistance —
2bmbAERO:m1(Aerotech PRO225SL-1000, 1 m travel).Beamline components: Detector optical table — synApps
table.dbcomposite2bmb:table3; corrective DoFs.AX(pitch about lab-X, corrects vertical slope) and.AY(yaw about lab-Y, corrects horizontal slope). Underlying jacks (per item_020.rst) —2bmb:m9 / m10 / m11 / m12 / m13 / m14; motion-done detected by ANDing all six.DMOVfields.Not yet a cora Asset — registering one (cora
TableFamily) is the open trigger this procedure creates.Beamline components: MCTOptics — read only.
2bm:MCTOptics:CameraSelectandLensSelect(the setpoint mbbo records) are read at start to derive the camera areaDetector prefix and the lens magnification. The procedure does not modify either. The lookup is keyed by the mbbo enum index (returned by a plaincagetwithoutas_string=True) so it survives IOC-version differences in the display strings (e.g."Camera 1"vs"Camera Selected 1").Index
CameraSelect
cam_prefix
Camera
0
Camera 12bmSP1:FLIR Oryx 5MP
1
Camera 22bmSP2:FLIR Oryx 31MP
Index
LensSelect
Magnification
0
Lens11.1×
1
Lens25.0×
2
Lens310.0×
Magnifications are hard-coded in
detector_z_rail_alignment.py(LENS_MAGNIFICATIONS_BY_INDEX); update there if the installed objectives change.Beamline components: Scintillator_LuAG — passive (no command surface).
Beamline components: B-station slits —
2bma:m9/m10(Y pair) and2bma:m11/m12(X pair) for shaping the small square aperture. Operator-set before the run; not modified by the procedure.A-shutter (front-end) — operator opens before the run; not toggled by the procedure.
Preconditions
In v0.0.1 the operator is responsible for establishing each of the states below before launching. As the satisfying procedures land, cora’s dependency graph will be able to auto-resolve them.
State |
Predicate (informal) |
Satisfied by |
|---|---|---|
|
|
Enable beamline for beam ( |
|
A-station slits open with |
Set A-station slit aperture ( |
|
Mirror M1 and DMM driven to the energy-dependent positions
in the |
Set energy to preselect ( |
|
|
Move flag into beam (mode-dependent) ( |
|
|
Open B-shutter (P6-50 Safety Shutter) ( |
|
B-station slits at |
Set B-station slits for alignment ( |
|
The relevant sample-stack axis (mount-dependent) is at its out-of-beam position. |
Move sample out of beam ( |
|
MCTOptics lens at 1.1× (slot 0); detector optical table
|
Configure microscope for alignment ( |
FES shutter is open |
|
Enable beamline for beam ( |
Z stage in safety band |
|
operator (manual move) or
Configure microscope for alignment ( |
MCTOptics IOC reachable |
|
operator (start MCTOptics IOC if not running) |
PSS interlocks satisfied |
|
operator (floor procedure) |
The machine-readable form of this table lives in
procedures/detector_z_rail_alignment.py as the module-level
PRECONDITIONS list. It is currently data only — the
procedure does NOT runtime-check these. Cora can ingest the list
once the schema lands.
Operating envelope (v0.0.1 “build trust” phase)
Z safety band
[200, 500]mm — enforced at__init__; the motor’s own.HLM/.LLMare not modified.Per-motion confirmation gate — before every table move (calibration perturb / restore, iteration correction) the procedure prints a plan block (PV, current value, target value, delta, units) and waits for
yorNon stdin.Naborts cleanly viaOperatorAbort; thetry / finallythen runs the restore path. Z measurement moves stay within the safety band and only sample the alignment (don’t change it), so they are announced but NOT gated by default. Pass--gate-zto gate them too.Snapshot + restore — at entry the procedure captures the full camera state of the active camera (
Acquire,AcquireTime,NumImages,ImageMode,TriggerMode,TriggerSource,TriggerOverlap,ExposureMode,ArrayCallbacks), the Z stage RBV, and the table soft axes2bmb:table3.AY/.AX. On every exit path the camera state and Z position are restored. The table AY/AX are restored only on these exits:OperatorAbort(operator answered N at a gate).Exception (any RuntimeError, including the divergence guard).
max-iterations exhausted and the best
|tilt|seen across iterations was no better than the starting state.
On clean convergence, the optimised AY/AX stay in place as the procedure’s deliberate output. On max-iterations with a net improvement (the common case when the threshold can’t be reached because of noise), the procedure moves the table back to the iteration that gave the best
|tilt|(“best-state commit”) and leaves it there — the operator still gets the improvement that did happen, instead of losing it to baseline restore. The log clearly states which path was taken.The restore path prints its plan but is not gated (it must run even on a panic exit); pass
--confirm-restoreto gate it.Caveat: the table restore writes to the
2bmb:table3.AY/.AXsoft PVs; the synAppstable.dbkinematic does not always perfectly invert a perturb-and-back cycle, so the underlying jacks may end up a fraction of a microradian off their pre-procedure RBVs (jack hysteresis). True per-jack restore would require snapshotting and writing the six jack positions directly; not implemented in v0.0.1.Operator-managed surfaces — MCTOptics camera/lens selection, B-station slit apertures, and the FES shutter are NOT touched. The procedure reads what the operator has set and adapts.
Parameters
Name |
Type |
Unit |
Description |
|---|---|---|---|
|
number |
mm |
Upstream Z anchor for the two-point measurement.
Default: 200. Must be in |
|
number |
mm |
Downstream Z anchor. Default: 500 (300 mm lever arm).
Must be in |
|
number > 0 |
µrad |
Test step in |
|
number > 0 |
s |
Per-frame exposure. Default: 0.2 (gives a clean bright spot on the 1.1× lens at typical 2-BM-B flux). Increase if the centroid signal is weak. |
|
number > 0 (or auto) |
µrad |
Residual linear slope at or below which the procedure stops
iterating. Default: auto-computed from the detected lens
+ binning + dz + a fixed rail-straightness floor (~10 µrad
for the PRO225SL over a 300 mm dz), multiplied by
|
|
> 1 |
× |
Multiplier applied to the noise floor when auto-computing the convergence threshold. Default: 1.5. Larger values converge sooner (less precision); smaller values closer to 1.0 push toward the physical floor but may not always reach it given residual centroid jitter. |
|
> 0 |
pixels |
Assumed standard deviation of the centroid fit, used only by the auto-threshold calculation. Default: 1.0 (typical for COM on a clean spot above threshold). Increase if the spot is faint / noisy; decrease if a sub-pixel-stable Gaussian fit is in use. |
|
integer ≥ 1 |
— |
Safety cap. Default: 5. |
|
0 < d ≤ 1 |
— |
Multiplier on the iteration’s computed correction. 1.0 = full correction, 0.5 (default) = half. Damping < 1 keeps us in the linear range across iterations when the sensitivity matrix is imperfect or table cross-coupling exceeds what a 2×2 linear model captures. |
|
> 1 |
× |
Abort if |
|
> 0 |
µrad |
Hard clip on the per-iteration correction magnitude for each table axis. Default: 200. When the calibrated sensitivity matrix M is ill-conditioned (table has weak authority over one slope direction), M⁻¹ can compute very large corrections; the clip keeps each iteration within the linear range near the calibration point. Convergence happens over more iterations rather than one big move. |
|
|
— |
Selects the centroid implementation in
|
|
0 < x < 1 |
— |
( |
|
int > 4 |
pixels |
( |
|
> 0 |
σ |
( |
|
int ≥ 1 |
— |
Acquire and average N frames per centroid measurement.
Centroid shot-noise drops as |
|
number > 0 |
µm |
Camera sensor pixel pitch, pre-binning. Default: 3.45
(Oryx 5MP and 31MP both have 3.45 µm sensor pixels).
At |
|
flag |
— |
Also gate Z measurement moves on y/N. Default off: Z moves stay within the safety band and only sample alignment, so they’re announced but not gated. Table moves are ALWAYS gated regardless. |
|
flag |
— |
Auto-confirm every motion prompt. Off by default (interactive); use for headless / scripted runs only. |
|
flag |
— |
Gate the restore path on |
|
flag |
— |
Print every planned motion and skip; never moves any motor. Camera reads + centroid fits still happen. |
Note
Centroid algorithm. In v0.0.1 the centroid algorithm
changed from intensity-weighted COM (center_of_mass,
fraction-of-max threshold) to a background-thresholded
geometric centroid (centroid_above_background,
σ-above-background threshold). Driven by 2-BM-B field testing:
the DMM beam has strong horizontal multilayer-stripe modulation
that biases an intensity-weighted COM toward whichever stripe
happens to be brightest, instead of the geometric centre of the
illuminated square aperture. The new algorithm gives every
above-threshold pixel an equal vote, so the centroid tracks the
geometric centre of the illuminated area regardless of internal
structure. Median+MAD on corner samples makes the threshold
robust to bright features spilling into a corner.
Steps
# |
Action |
PV / call |
|---|---|---|
1 |
Detect operator-set configuration. Read
|
|
2 |
Snapshot pre-procedure state. Capture the active
camera’s full state and the Z stage RBV. Stored in a
|
|
3 |
Record table baseline. Read |
|
4 |
Calibrate the slope-sensitivity matrix M. For each
table axis (AY, AX): measure baseline slope, perturb axis
by | Δslope_X | | M_AY_X M_AX_X | | ΔAY |
| | = | | | |
| Δslope_Y | | M_AY_Y M_AX_Y | | ΔAX |
where slope is in µm/mm and Δaxis in µrad. (a) Move Z to (b) [gated] Perturb (c) [gated] Perturb (d) Sanity-check This replaces the old centroid-shift-at-z-far “Jacobian” formulation, which measured the wrong physical quantity: uniform centroid shifts at fixed Z cancel between z_near/z_far and leave slope unchanged. The slope sensitivity is what actually drives convergence. |
|
5 |
Iterative correction. For (a) Acquire frames at (b) Convert to angular misalignment (c) Divergence guard: if (d) If both (e) [gated] Compute corrective ΔAY, ΔAX by solving
|
|
6 |
Restore. Run by the |
|
Postconditions
|tilt_X|and|tilt_Y|over the[z_near, z_far]lever arm are both belowconvergence_threshold(success), or the iteration limit was hit and a warning logged.2bmb:table3.AYand.AXare at the converged values; their new positions are logged. (Procedure deliberately does not restore these — they’re the output.)PropagationDistanceis back at its pre-procedure RBV (restored from snapshot).All snapshotted camera state is back to its pre-procedure values: if the camera was running Continuous on entry, it is running Continuous on exit; ImageMode / TriggerMode / TriggerSource / TriggerOverlap / ExposureMode / NumImages / AcquireTime / ArrayCallbacks are all restored.
FES shutter state is unchanged (procedure does not toggle it).
The centroid-vs-Z log and iteration history are persisted via the cora Procedure record (when
--no-cora-logis not set).
Failure modes
Symptom |
Recovery |
|---|---|
|
Procedure exits cleanly via |
|
Adjust |
|
Verify MCTOptics IOC is up (host |
No signal at |
Slits closed too tight, beam off-centre, or shutter shut. Verify BLEPS status (no Fault latched); open B-station slits to a known-good 5 × 5 mm; re-open FES; re-check. |
Square aperture exits the camera field of view as Z is moved. |
The initial misalignment is too large for the current
FOV. Reduce |
Convergence not reached after |
The restore path returns the snapshot; the iteration history is logged. Almost always means the Jacobian sign discovery in iteration 0 was wrong (slits drifted, centroid algorithm misfit) — re-run with a brighter aperture, or a Gaussian fit instead of COM. |
|
The motion call raises |
Table move appears to complete but jacks did not actually
drive ( |
The synApps |
Operator walkthrough
This procedure is intentionally written so an operator can verify it step-by-step on the MEDM screens:
Lens / camera select —
mctOpticsoperator screen (LensSelect / CameraSelect). Set BEFORE launching the procedure; the procedure only reads.Slits —
2slit.adlfor the B-station horizontal + vertical screens (see Beamline components for the label-flip caveat on the horizontal blades). Set BEFORE launching.Z stage —
2bmbAEROmotor screen form1. Watch this as each[gated]Z move executes.Optical table corrections —
table_full.adlfor2bmb:table3(use the Translate column forX / Y / Zand the Rotate column forAX / AY / AZ; the composites back-drive the underlying corner motors at2bmb:m9–m14). Watch the AY / AX text-entry fields update as each[gated]table move executes.Centroid — the simplest live read is the camera live view plus a thresholded ROI in the areaDetector ROI plugin; the procedure logs centroid-µm and tilt-µrad to stdout for each iteration.
The y/N gate at the terminal IS the operator’s safety check —
read the plan block before answering y. If anything looks
wrong (PV name, sign, magnitude), answer N and the procedure
exits cleanly via the snapshot restore.
Field-test results (v0.0.1, 2026-06-14)
First end-to-end convergent run on 2-BM-B:
- Camera:
FLIR Oryx 31MP at
2bmSP2:via MCTOptics- Lens:
1.1× (slot 0)
- Z safety band:
200–500 mm
- Calibration step:
100 µrad
- Damping:
0.5
- Auto-set convergence threshold:
31.4 µrad
- Sensitivity-matrix condition number:
1.1 (essentially diagonal — AY ↔ slope_X, AX ↔ slope_Y, no cross-coupling)
Convergence trajectory:
iter |
|
|
|
reduction vs prev |
|---|---|---|---|---|
1 |
−169 µrad |
+397 µrad |
431 µrad |
— |
2 |
−85 |
+203 |
220 |
0.51× |
3 |
−46 |
+97 |
107 |
0.49× |
4 |
−20 |
+49 |
53 |
0.50× |
5 |
−10 |
+24 |
26 |
0.49× ← converged |
Each iteration cut |tilt| almost exactly in half, matching
the damping=0.5 prediction to better than 1%. The final
residual (26 µrad) sits at the PRO225SL rail’s intrinsic
straightness floor (~10–20 µrad over a 300 mm sub-range) — the
procedure cannot drive |tilt| below this regardless of
iteration count. Final table pose:
AY = −0.0092 deg, AX = −0.0211 deg.
Run details and the architectural / bug history that got here are in 2bm-procedures CHANGELOG.
Notes
The PRO225SL-1000 datasheet quotes ±9.5 µm horizontal / vertical straightness over the full 1 m travel — that is the floor of the non-linear residual once this procedure has removed the linear tilt. Operators should not chase residuals below that envelope.
The detector optical table is described in detail in Beamline components (Detector optical table block; SRI geometry, M0X=``m13``, M0Y=``m14``, M1Y=``m12``, M2X=``m10``, M2Y=``m9``, M2Z=``m11``; virtual record at
2bmb:table3).Sign convention for
table3.AX/.AYvs centroid drift is discovered at iteration 0 of step 4 — do not hard- code a sign in the implementation, derive it from the calibration Jacobian.Camera state hygiene — the procedure changes
TriggerMode,ImageMode,NumImages, and possiblyAcquireTimeon the active camera during the run.acquire_image()inprocedures/_shared/epics.pyforcesTriggerMode=OffandImageMode=Singlebefore every frame so a stale external-trigger configuration (e.g.TriggerSource=Line2for PSO-triggered tomoscan Runs) cannot make the call hang. All of these are restored from snapshot at exit.This procedure is not the same as cora’s stubbed
resolution_alignment. The two touch different Assets entirely:resolution_alignmentoptimises lens focus via the MCTOptics per-lens focus values (saved by the IOC per camera + lens combination), while this procedure walks thePropagationDistancerail stage (2bmbAERO:m1) to fit and correct rail-to-beam angular alignment. The earlier shared-Asset framing in this page reflected the now-corrected misconception that2bmbAERO:m1was a lens-focus motor; it is in fact the sample-to-detector Z (propagation) stage.Open trigger this procedure creates: register a
DetectorTableAsset (coraTableFamily) incora/docs/deployments/2-bm/assets.md, then add adetector_z_rail_alignmententry to that deployment’sprocedures.mdreferencing this page.