Commit Graph
211 Commits
Author SHA1 Message Date
x01dc f08c3ef7b7 Merge remote-tracking branch 'origin/fixes/flomni4' into feat/flomni_tomo_from_angles_file
CI for csaxs_bec / test (push) Successful in 3m7s
# Conflicts:
#	csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py
#	csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py
2026-10-01 16:19:45 +02:00
x01dcandClaude Sonnet 5 2a91f94c06 feat(flomni): add manual_shift_x fine-adjustment parameter
CI for csaxs_bec / test (push) Successful in 2m59s
Mirrors the existing manual_shift_y: a persistent, GUI/CLI-settable x
offset applied to the scan center in tomo_scan_projection() and
tomo_acquire_at_angle(), included in tomo-queue snapshots, and reset to
0 at the start of every x-ray-eye alignment run (and on account-change
defaults) so a leftover fine adjustment never biases the next sample's
alignment tomogram. Bounded to +/-10um in the GUI and CLI, narrower
than manual_shift_y's +/-50um range.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
2026-10-01 12:58:34 +02:00
x12saandClaude Sonnet 5.5 b3830dc3e5 feat(flomni): option to run all type-1 sub-tomos forward (0->180)
CI for csaxs_bec / test (push) Successful in 3m5s
Add tomo_type1_always_forward (180 deg range only, default off). Every
sub-tomo sweeps 0->180 with an unchanged angle set, so the 0 deg
reference (zero_deg_reference_at_each_subtomo) is taken before all 8
sub-tomos instead of every second one.

Exposed in tomo_parameters(), queue params and the tomo params widget;
resume/reacquire use the same plan. Tests and docs updated.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-01 12:33:49 +02:00
x01dcandClaude Sonnet 5 d119bd2ce8 fix(flomni): widen tomo_type-4 angle range to stage overtravel; fix GUI clipping
CI for csaxs_bec / test (push) Successful in 3m4s
Angle files now accept -2 to 362 degrees (the fsamroy rotation stage's
physical overtravel range) instead of the nominal 0-360 sweep. This
required widening both the parser's bounds (angle_file.py) and
_tomo_scan_at_angle()'s own gate (a new _min_scan_angle(), alongside the
existing _max_scan_angle()) -- without the latter, an angle just outside
[0, 360] would have parsed fine but then been silently skipped during
acquisition with no error, which is worse than rejecting it upfront.

Also fixes the type-4 GUI section's "Content:" label clipping its second
wrapped line: QFormLayout sizes that row from the label's sizeHint() at
construction (holding the short "---" placeholder) and doesn't reliably
recompute it when setText() later swaps in longer, word-wrapped content.
_set_angle_file_summary() now recomputes the label's minimum height from
heightForWidth() after every update.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
2026-10-01 11:49:49 +02:00
x01dcandClaude Sonnet 5 0332f1c681 fix(flomni): reset tomo progress before pre-flight gates, not after
CI for csaxs_bec / test (push) Successful in 3m18s
tomo_scan() only reset the progress dict (projection/subtomo/angle/
tomo_start_time/heartbeat/...) inside the "new scan" block, which runs
after the eye/optics-in and filter-out-of-beam gates. If either gate
declined (e.g. the setup isn't in measurement condition, which disabling
rt feedback can trigger) or the tomo_type-4 angle-file check raised,
tomo_scan() returned/raised having never touched progress, leaving the
GUI/webpage showing projection/angle/start-time from whatever scan last
completed -- arbitrarily old, and easy to mistake for a scan still
running.

Hoists the "is this a new scan" check to the top of tomo_scan() and
calls progress.reset() there, before any gate runs; the heavier new-scan
actions (sample DB registration, PDF report, collect_empty_frames(), the
0-deg reference shot) stay exactly where they were, after the gates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
2026-10-01 11:22:45 +02:00
x01dcandClaude Sonnet 5 67884c0c3f feat(flomni): tomo_type 4 -- live-appendable angle files, per-line overrides
CI for csaxs_bec / test (push) Successful in 2m59s
Angle files can now be grown while a scan is running: tomo_scan() polls
and re-reads the file whenever it catches up to the current end,
picking up newly appended lines automatically instead of requiring
tomo_scan_resume(). A file is "complete" only once it ends with an
explicit END sentinel line -- without one the scan waits indefinitely
at EOF rather than stopping, so a static/legacy angle file needs END
appended or a scan against it will hang at the end.

Each line may also carry optional step=/exptime=/frames= key=value
tokens overriding the Fermat step size, exposure time, and burst frame
count for that projection and (sticky) every one after it, falling
back to the live tomo_parameters() configuration whenever not set.
Threaded through as kwargs that default to None and are omitted when
empty, so every existing tomo_type 1/2/3 call site and test stub is
unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
2026-10-01 11:01:16 +02:00
x01dcandClaude Opus 5.5 9373fe307a fix(flomni): no 0-deg damage-estimation shots for tomo_type 4
CI for csaxs_bec / test (push) Successful in 3m4s
Sub-tomogram numbers in a user angle file are just labels, so inserting
extra 0-deg projections at odd sub-tomogram transitions is surprising.
tomo_type 4 now ignores golden_projections_at_0_deg_for_damage_estimation;
the CLI no longer prompts for it and the widget hides it for type 4.
The unconditional 0-deg reference shot at the start of a new tomogram
is unchanged; further 0-deg shots can be put in the file directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:57:11 +02:00
x01dcandClaude Opus 5.5 35ac94cfd5 feat(flomni): tomo_type 4 -- angles read from a file
CI for csaxs_bec / test (push) Successful in 3m7s
New angular sampling mode: tomo_angle_file points to a plain-text file
with one angle (0-360 deg) per line, optional 2nd column = sub-tomogram
number, '#' comments. Only the path is stored; the file is re-read at
every tomo_scan() start, resume and queued-job run, so an edited file
always takes effect on the next (re)start. The parsed angle list is
logged to scilog at scan start.

- OMNY_shared/angle_file.py: shared pure parser + summary, incl.
  warning for 180-deg-opposite/duplicate angles
- flomni: tomo_type 4 branch in tomo_scan()/tomo_scan_resume(), bad
  file fails before any acquisition, angles >180 not gated by
  tomo_angle_range, CLI prompt/display, PDF report
- tomo queue: path snapshotted per job; setter does not validate so
  stale paths never break non-type-4 jobs; reacquire bounds by file
- TomoParams widget: type 4 (flomni only) with Browse... file dialog,
  live content preview, validation blocking Submit/Add to queue
- webpage: show angle file in param tables

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 14:46:56 +02:00
x12saandClaude Sonnet 5.5 7cb553414a fix(tests): give leye_out shutter test a csaxs and client stub
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m56s
leye_out() now reads self.csaxs and self.client (slit/filter handling), which
the bare LamNIOpticsMixin subclass in the test did not define.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 b80c6417d2 fix(lamni): go-live audit tier 2 -- wrong numbers and missing flomni ports
- tomo_scan_projection() now adds the projection to the reconstruction queue
  itself (like flomni), so custom at_each_angle hooks and direct calls are
  reconstructed; _at_each_angle()/MagLamNI no longer do it (no double entry).
  tomo_alignment_scan() opts out (_reconstruct=False) to keep its previous
  behaviour -- flomni's alignment projections are queued, LamNI's never were.
- write_pdf_report(): branch on tomo_type (a golden-ratio scan reported a stale
  type-1 count and a hardcoded '8 sub-tomograms'), e-account from
  bec.active_account, tolerate a missing detector_distance.
- tomo_parameters_cli(): range guards (stitch 0-50, frames 1-100, >= 8
  projections, type-3 >= 1, golden max 0-1e6, bunch size 20-100).
- Webpage: projection count for tomo type 1 used flomni's 180-degree
  sub-tomograms (half the real count); TOMO_TYPES gets a base_angle. Also show
  the piezo range, manual shift and 0-degree reference rows.
- Tomo-params GUI: manual shift is mm end to end (label/range said um, +-1000);
  golden bunch size floor 20 for LamNI (setter/CLI already required it, so a
  smaller GUI value broke tomo_queue_execute). flomni fields untouched.
- lfzp_info() and the queue 'move Energy' entry used the legacy 'mokev' device
  (only in device_configs/legacy); now ccm_energy.
- Special-angle UnboundLocalError fixed in flomni too (list consumed only after
  a successful projection, so retries still see the special angle).
- Tomo type 3: exact integer sub-tomogram numbering (float truncation mislabelled
  the boundary projection for ~35 values of n < 400).
- XrayEyeAlign.align(): always reset the GUI (DAP forwarding, crosshair) on any
  failure; xrayeye_update_frame() warns when beamline states are invalid.
- Stale text / debug print; gap doc updated with the current status.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 657e192b8c fix(lamni): go-live audit tier 1 -- lost projections, shutter, endless-scan GUI, scilog
Found by auditing LamNI against the battle-tested flomni:

- _tomo_scan_at_angle swallowed TimeoutError alarms: the projection was never
  retried, its failed scan numbers were logged as a success and it never
  reached reconstruction. Now the queue is reset and the alarm re-raised so
  @scan_repeat re-acquires it. The special-angle list is consumed only after
  success (a retry must still see it) and num_repeats is always defined.
- tomo_alignment_scan had the same swallow; it now retries (bounded) and
  rewrites ptychotomoalign_scannum.txt with the scan numbers actually acquired.
- leye_out() opened the fast shutter instead of closing it (regression from
  4967474 'removed fsh commands').
- Progress GUI: an endless golden-ratio scan (golden_max_number_of_projections
  == 0) divided by zero on the first projection, and any GUI/RPC failure
  propagated into the scan (retried 10x by @scan_repeat, then aborted). Now
  guarded and wrapped like flomni's dea43ed.
- raise_window() disabled as in flomni (it can hide the GUI window).
- write_to_scilog and the PDF report entry used the removed bec.logbook API;
  they now use bec.messaging.scilog like flomni, never raise, and include the
  at-each-angle hook source in the end-of-scan/timing entries.
- extra_tomo: num_repeats unbound for non-special angles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 27cdaa197c feat(lamni,flomni): tomo_parameters() opens the GUI window, CLI is tomo_parameters_cli()
- shared TomoQueueMixin.tomo_parameters() opens OMNY_TomoParams in its own
  window (lamni_tomo_params / flomni_tomo_params); repeat calls do not open
  a second one; a missing or failing GUI raises the setup's error and points
  to tomo_parameters_cli() instead of silently falling back
- rename the prompt-based versions to tomo_parameters_cli()
- docs: describe both, and use the CLI in the queue examples that rely on
  the command blocking until the values are entered
- add tests with a fake GUI

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gUjUT2xCNpnBQkThn3CqP
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 7753dfff38 fix(lamni): port flomni's tomo_reconstruct scan-list fallback and folder resync
- fall back to the last completed scan number when _current_scan_list is
  missing or stale (direct calls outside tomo_scan_projection)
- re-read ptycho_reconstruct_foldername on every write so changes from
  another client session are picked up
- add tests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gUjUT2xCNpnBQkThn3CqP
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 b066b526c1 fix(lamni): scan defaults, sim feedback interlock, fttrz draft
- lamni/omny fermat scan: default exp_time 0.1, drop unused readout_time
  (devices already fall back to their own configured readout time)
- sim_lamni: refuse the sd scan start and abort a running scan while the
  interferometer feedback loop is off; add tests
- simulated_lamni.yaml: lsamrot sim_velocity 48 (4x hw)
- lamni_optics_mixin / ptycho_lamni.yaml: commented-out draft for moving the
  fttrz flight tube in leye_out/leye_in, pending commissioned positions

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016gUjUT2xCNpnBQkThn3CqP
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 15c720e89e fix(lamni): fully reset tomo progress when a new tomo scan starts
tomo_scan() only reset the timing/ETA fields, so after a finished scan the
progress GUI kept showing the old subtomo, angle and projection == total
(rings at 100 %) while the initial 0-degree reference shot ran, with an ETA
computed from the old projection count against the new start time.

New _reset_progress_for_new_scan() resets every field (incl. the right
totals for the tomo type) in one global-var update and refreshes the GUI.
Resumes are unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:07:46 +02:00
hollerandClaude Sonnet 5 fd4e8a2de1 fix(lamni): tomo_scan_resume resumes at the interrupted angle, not a shifted sweep
_subtomo_angle_plan() used an explicit start_angle as the origin of a new
360-degree sweep. Resuming subtomo 2 at 85 deg generated 435..85; the >360
artifacts were silently skipped by _tomo_scan_at_angle's range gate, so the
resume began at 355 deg, re-measured done angles, never reached the rest and
reported wrong progress indices.

start_angle is now the literal resume point on the sub-tomogram's own grid:
the plan returns the remaining tail of the normal sweep plus a subtomo_offset
used for progress numbering (mirrors Flomni). Off-grid angles raise ValueError.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:07:46 +02:00
x12saandClaude Sonnet 5 00ddc5f560 fix(eiger): resolve live preview data channel per detector, not "default" only
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m57s
The Eiger2 Si 500K's multi-threshold JFJ preview messages key the frame
under a threshold name (e.g. "threshold_1"), not "default" as eiger_1_5/
eiger_9 do -- confirmed by capturing the live JFJ ZMQ preview stream.
_preview_callback hardcoded "default", so it silently dropped every
frame for eiger2_si_500k (251+ "without data" errors in DeviceServer.log,
matching the empty BEC image widget). Now prefers "default" when present
and falls back to whichever single channel the broker actually sent.

Also document the JFJoch Web UI (http://sls-jfjoch-001.psi.ch:8080) and
the Eiger 2 Si 500K detector server address (129.129.122.159) in
bl_detectors.yaml's Eiger section.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:44:42 +02:00
x12saandClaude Sonnet 5 26da8ea66a feat(eiger): integrate Eiger 2 Si 500K and make NeXus writer detector-agnostic
Add the Eiger2Si500K device class (subclassing the shared JungfrauJoch Eiger
base) and make it the active detector in bl_detectors.yaml. The cSAXS NeXus
writer hardcoded "eiger_1_5" everywhere, so switching the active detector
silently dropped the frame-data links (entry_ptycho/data, data_soft, and the
NXdetector group) for any other Eiger model. Resolve the active detector via
a new "eiger" deviceTag instead, and look up per-model NXdetector metadata
(pixel size, sensor description/thickness, orientation) from a small table so
a future model can't reintroduce the same silent failure.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:44:42 +02:00
x12saandClaude Sonnet 5 bc094dd172 fix(tests): resolve flomni/lamni CLI source paths via module __file__
CI for csaxs_bec / test (pull_request) Successful in 2m57s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 1s
CI for csaxs_bec / test (push) Successful in 2m56s
test_flomni_cli_fovy_bounds_within_scan_bounds and
test_lamni_cli_piezo_range_bounds_within_scan_bounds opened the CLI
source files using a path hardcoded relative to the repo root, which
only works if pytest's cwd happens to equal the repo root. In CI the
job's cwd is one level above the checkout, so the same relative path
raised FileNotFoundError. Resolve the path from the already-imported
module's __file__ instead, which is independent of cwd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:19:45 +02:00
x12saandClaude Sonnet 5 55906334fc feat(panda): only arm PandA for flomni_fermat_scan
CI for csaxs_bec / test (push) Failing after 2m51s
Generic BEC staging (stage_all_devices/pre_scan_all_devices) has no
per-scan device opt-in -- every enabled device is staged/pre_scanned for
every scan type (grid_scan, line_scan, alignment moves, ...), not just the
ones that consume its data. deviceTags (ptycho_flomni) are never consulted
by that code path, they're purely a client-side grouping convenience. So
omny_panda_continuous was being Arm()ed and disarmed for every scan in a
session even though only flomni_fermat_scan reads its data -- unnecessary
hardware arm/disarm cycling, and it directly compounds the stale
frame_counter carryover bug documented in PANDA_POSITION_VALIDATION.md
sec 7 (more arm cycles means more abort->restage windows where that bug
can be triggered).

PandaBoxOMNY now takes an arm_scan_allowlist constructor kwarg (None by
default, arms for every scan -- unchanged behavior). When set, on_stage()
computes _should_arm_panda from scan_parameters.scan_name against the
list; on_pre_scan() skips sending Arm() entirely when it's False, and
on_complete() skips the wait-for-frame-count poll loop the same way, both
resolving immediately instead. The data-readout thread/TCP connection
startup in on_stage() is untouched, since it doesn't touch PandA's
hardware register state (COUNTER1) the way Arm() does.

ptycho_flomni.yaml's omny_panda_continuous now sets
arm_scan_allowlist: [flomni_fermat_scan].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 16:00:20 +02:00
x01dcandClaude Sonnet 5 3eb6e0355f fix(panda): drop stale frame_counter carryover in Mode B raw stream
CI for csaxs_bec / test (push) Failing after 2m51s
Diagnosed against a real scan (S06313): grouping raw interf_st_fzp_x/_y
samples by frame_counter + gate_detector_active reproduces rt_positions'
trusted average_x_st_fzp/_y to R² > 0.9999, confirming that method -- but one
point (167) was off by ~116 nm because PandA's COUNTER1.OUT is not cleared by
Arm(). The first raw samples of an acquisition can briefly carry a stale
frame_counter value left over from the previous acquisition's tail (observed:
36 samples reading a stale 167 before dropping to 0), which silently
contaminates whichever real point later in the same scan happens to share
that counter value.

PandaBoxOMNY now drops raw rows captured before it observes frame_counter ==
0 for the first time in each acquisition -- on every arm, including
on_pre_scan retries, since each Arm() reopens the same stale-value window --
and drops leftover buffered rows from an abandoned attempt at the same point.
Verified against S06313: point 167's residual drops from ~116 nm to ~0.1 nm
after the fix, in line with the rest of the scan.

Write-up, including the diagnostic method and a possible PandA-layout-level
alternative fix, in docs/developer/panda_box_free_running_setup.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NPZ9gzN2rt3wgiMnTKyrug
2026-09-21 20:21:23 +02:00
x01dcandClaude Sonnet 5 337cdc45e9 fix(xrayeye): hide the Skip button until a client script opts in
CI for csaxs_bec / test (push) Failing after 2m57s
LamNI/omny never call enable_skip_button, so they only had a greyed-out
Skip button. Keep it hidden; the first enable_skip_button() call (flomni
does this at the start of the alignment) shows it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 16:10:09 +02:00
x01dcandClaude Sonnet 5 0ad6c5014d feat(flomni): Skip button on the xrayeye alignment to leave out one angle
CI for csaxs_bec / test (push) Failing after 2m12s
Flat samples are edge-on at 90 deg and cannot be centered there. The
XRayEye widget gets a Skip button (disabled unless the client enables it,
so LamNI/omny are unaffected) that signals omny_xray_gui.submit = -1.

XrayEyeAlign._align_impl leaves that step out of the fit: one of the five
angles may be skipped, at least 4 are needed. Skips of the FZP center, the
height marking, or a second angle are refused with a message and the run
continues. write_output() builds angles/offsets from the remaining steps
and raises if fewer than 4 are left; the h5 records the skipped angles.

Not yet tried on real hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 15:56:10 +02:00
x12saandClaude Sonnet 5 26df688512 fix(flomni): motion stop button stops all three Galil controllers
CI for csaxs_bec / test (push) Failing after 2m26s
The GUI "Motion Stop" button did not stop the stages when pressed during
a sample transfer:
- stop_all_axes was not in the Galil controllers' USER_ACCESS, so the
  foptx stop over RPC could never work (only logged in the GUI server).
- fsamroy's controller (port 8084) was never stopped.
- an unresolvable device in _stop_device_controller returned silently.

The widget now takes hard_stop_device_names (flomni: ftransy, foptx,
fsamroy; transfer controller first) and sends stop_all_axes() to each
controller from its own thread, so a hung RPC cannot delay the others.
Devices are resolved freshly at click time with retries, every outcome
is logged and shown in the widget's message label, and positioning mode
is restored on the first controller only after all stops were sent. If
stop_all_axes is not yet exposed by the device server, the raw
XQ#STOP,1 is sent via socket_put_and_receive. The legacy
hard_stop_device_name/extra_hard_stop_device_name kwargs still work.

Not yet tested on hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 13:44:06 +02:00
x12saandClaude Sonnet 5 19cfee3e79 feat(flomni): add randomize_offset scan argument to flomni_fermat_scan
CI for csaxs_bec / test (push) Failing after 2m12s
The per-call random sub-step offset of the Fermat lattice could not be
switched off from the scan (seed is only an argument of the static position
function, and a seed only makes the offset reproducible). Add an optional
randomize_offset: bool = True scan argument, passed through prepare_scan()
to get_flomni_fermat_spiral_pos(); False gives offset 0 (unshifted lattice).
Default keeps today's behaviour; tomo scans do not pass it.

Document it in the flomni user doc and add an omny AI_docs TODO describing
the port.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
2026-09-20 13:49:20 +02:00
x12saandClaude Sonnet 5 c66253709a perf(csaxs): read filter positions from cache in the fil_trans report
CI for csaxs_bec / test (push) Failing after 2m13s
_fil_trans_report is a status report, so use readback.get(cached=True) (last
published readback from redis) instead of one RPC round trip per filter axis.
A missing value still ends in the existing "readback unavailable" error. The
post-umv position check in fil_trans stays live on purpose.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
2026-09-20 12:01:17 +02:00
x12saandClaude Sonnet 5 028e31be41 perf(csaxs): read slit positions from cache in slits_show_all()
CI for csaxs_bec / test (push) Failing after 2m10s
device.read() defaults to cached=False, i.e. one RPC round trip to the device
server per device (24 for the six slits). Use cached=True, which reads the last
published readback from redis; a missing value still shows as '---'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
2026-09-20 11:55:26 +02:00
x12saandClaude Sonnet 5 bbede883b0 fix(panda): retry arming once after a forced reset instead of hanging in pre_scan
CI for csaxs_bec / test (push) Failing after 2m11s
PandaBoxOMNY inherited on_pre_scan, which waits forever for the PandA READY
event before sending Arm(). After rapid abort/retry cycles the readout thread
can be left parked, so READY never arrives and pre_scan_all_devices() hangs
with no error (flomni_fermat_scan 11998/12000).

on_pre_scan now runs a bounded arm task: wait pre_scan_timeout (default 5 s) for
READY; on failure force-reset the box, restart the readout thread (after the old
loop finished its cleanup) and try once more, printing and logging a warning with
diagnostics. If the second attempt fails too, the status fails with a
RuntimeError so the scan aborts. Arm() is sent from the task, not a status
callback, so the scan is only released after it went out. A stop is not retried.

Also records the outcome and the still-open ophyd_devices race in
AGENTS_mirko.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017szgTwuHG65YhjoBGiK2Gj
2026-09-20 11:47:04 +02:00
x01dcandClaude Sonnet 5 d01af690a9 fix(flomni,lamni): fovy/piezo-range bounds didn't match scan's strict "<"
CI for csaxs_bec / test (push) Failing after 2m12s
Real hardware failure: flomni.fovy=100.0 was accepted by the property, GUI,
and CLI (all capped at an inclusive 100.0), but FlomniFermatScan's own
ScanArgument requires fovy strictly less than 100 -- so the scan rejected it
at runtime with ScanInputValidationError, only surfacing once actually run.

- flomni.fovy property setter: now raises at >=100 (was >100), matching
  FlomniFermatScan's gt=0/lt=100. GUI/CLI max tightened 100.0 -> 99.9.
- lamni_piezo_range_x/y (become LamniFermatScan's fovx/fovy, gt=0/lt=80):
  found the same bug class at BOTH ends while writing the regression test --
  GUI/CLI min was an inclusive 0.0 (scan requires strictly >0) and max was
  an inclusive 80.0 (scan requires strictly <80). Tightened to 0.1-79.9 in
  both GUI and CLI. Property setter's large_range_scan bypass left
  untouched per earlier explicit instruction -- this fixes the GUI/CLI
  entry points, which is where the bound is actually enforced in practice.
- Added test_fov_bounds_match_scan_args.py: introspects the real
  ScanArgument gt/ge/lt/le from FlomniFermatScan/LamniFermatScan and asserts
  our configured GUI/CLI ranges are strictly inside them, so this class of
  bug (passes every check except the one that actually runs the scan)
  can't silently come back for these or future fields.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-19 21:30:22 +02:00
x01dcandClaude Sonnet 5 d77b5cede9 fix(lamni): tighten circfov/piezo-range GUI limits, enforce ranges in CLI
CI for csaxs_bec / test (push) Successful in 2m18s
- tomo_circfov: GUI upper bound 200.0 -> 500.0 um (floor kept at 0.0 so
  "0 = disabled" -- checked explicitly in lamni_fermat_scan.py:484 -- still
  works)
- lamni_piezo_range_x/y: GUI upper bound 200.0 -> 80.0 um, matching the
  "(max 80)" already printed in the CLI prompt. The property setter's
  large_range_scan bypass is left untouched (uncertain hardware rationale) --
  this only tightens the GUI/CLI entry points, which is where "<80" actually
  becomes enforced in practice, since both now validate before ever calling
  the (still-conditionally-permissive) property setter.
- LamNI._get_val() now supports min_/max_ (mirrors Flomni._get_val()
  exactly, added previously) -- LamNI's tomo_parameters() CLI wizard
  previously had zero range validation at all. Wired up for
  tomo_countingtime (0.001-100.0), tomo_shellstep (0.025-20.0),
  lamni_piezo_range_x/y (0.0-80.0), and tomo_circfov (0.0-500.0).
- tomo_stitch_overlap gets a brand new CLI prompt (0.0-10.0 um) -- it had no
  prompt at all before (property-only, same gap Flomni still has).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-19 21:16:28 +02:00
x01dcandClaude Sonnet 5 158dc800f0 fix(flomni): enforce GUI parameter ranges in tomo_parameters() CLI too
CI for csaxs_bec / test (push) Successful in 2m15s
_get_val() (the wizard's input prompt) previously did no range validation
at all -- only the Tomo Parameters GUI's spinboxes enforced limits, so the
CLI silently accepted any value of the right type (e.g. manual_shift_y=500
despite the GUI's ±50 µm range). _get_val() now takes optional min_/max_
and re-prompts until the value is in range; wired up for every
tomo_parameters() field that already has a GUI-defined range (tomo_countingtime,
tomo_shellstep, fermat_asymmetry, fovx, fovy, stitch_x/y, manual_shift_y,
frames_per_trigger, single_point_random_shift_max, total projections,
golden_ratio_bunch_size, golden_max_number_of_projections, projections/sub-tomo).
fermat_asymmetry reuses FlomniFermatScan's own MIN/MAX constants rather than
duplicating them.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-19 20:21:20 +02:00
x01dcandClaude Sonnet 5 0a12d4892f feat(flomni,lamni): unconditional 0-deg reference projection at scan start
CI for csaxs_bec / test (push) Successful in 2m15s
Take one real 0-degree projection at the very start of every new tomogram
(subtomo_number=1, included in reconstruction like any grid projection),
independent of zero_deg_reference_at_each_subtomo /
golden_projections_at_0_deg_for_damage_estimation -- gives an immediate
radiation-damage baseline even when those flags are off. Placed right after
Flomni's existing collect_empty_frames() step; LamNI gets the same 0-deg
shot but no flat-field step, since it has no way to retract the sample out
of the beam. Omny intentionally left as a follow-up (not implemented here).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-19 10:06:49 +02:00
x01dcandClaude Sonnet 5 cd838e4f73 fix(eps): enforce hard byte budget on eps_alarm_history.json uploads
CI for csaxs_bec / test (push) Successful in 2m22s
The 2026-09-11 fix capped alarm-text *count* per event (_ALARM_TEXTS_MAX),
but not text length, and _load() never re-caps an already-oversized event's
texts list -- only new appends are capped. Confirmed on x12sa: the file had
grown to 1,209,271 bytes, still tripping HTTP 413 on every upload cycle
(upload.php itself has no size check -- almost certainly nginx's default
client_max_body_size=1m). HistoryTracker.save() now shrinks a copy of the
payload to a ~250 KB budget (alarm texts, then ring trace, then oldest
events, then field length as a last resort) before writing/uploading, so
the file can no longer exceed the server's limit regardless of how the
underlying data grows. In-memory history is untouched.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-18 13:46:12 +02:00
x01dcandClaude Sonnet 5 bf6f89636a feat(flomni,lamni): clear tomo queue and reset progress on account change
CI for csaxs_bec / test (push) Successful in 2m19s
The account-change check already offered to reset scan parameters to
defaults for a new experiment; extend it to also clear the tomo queue and
reset acquisition progress, so a new account doesn't inherit leftover
queued jobs or in-progress state from the previous account.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
2026-09-18 09:19:41 +02:00
x01dcandClaude Sonnet 5 d237357012 fix(omny-shared): filters_out_of_beam tolerates a missing/None dev
CI for csaxs_bec / test (push) Successful in 2m31s
Surfaced live on a simulated flomni session: tomo_scan_projection()
crashed with "DeviceConfigError: Device filter_array_1_x does not
exist" -- the real beamline's four filter_array_*_x devices
(bl_endstation.yaml) aren't part of any endstation simulation config,
by design (out of scope for an isolated endstation sim, same as
frontend/optics-hutch/detector devices).

filters_out_of_beam() already tried to skip a missing device via
getattr(dev, axis_name, None), but BEC's device container raises
DeviceConfigError -- a plain Exception, not AttributeError -- for a
missing device, so getattr's default never applied. Wrap the lookup
in try/except instead, which also preserves the pre-existing (and
apparently relied-upon, per several LamNI tests) behavior of `dev`
itself being None -- the usual state of this codebase's
`dev = builtins.__dict__.get("dev")` idiom before a real session sets
it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
2026-09-17 14:22:29 +02:00
x01dcandClaude Sonnet 5 0690fbb391 feat(flomni): auto-lock ROI vertical center to FZP height in x-ray eye alignment
Implements docs/plans/xrayeye-vertical-lock.md: once the height-centering
step fixes the sample's vertical position against the FZP crosshair, every
selection box drawn for the remaining angle-alignment steps (1-5) no longer
needs manual vertical placement -- it snaps to the crosshair's y position on
draw and again on every drag/resize.

- OMNY_XRayEye: new _vertical_lock_enabled state, lock_vertical_center()/
  unlock_vertical_center() RPC methods, _snap_roi_vertical() helper wired
  into _style_new_roi() (both roiAdded and sigRegionChangeFinished). Snap is
  idempotent (no-op once already at the target y) so it can't loop back into
  itself via its own set_position() call.
- Also added a "ROI vertical lock" toggle in the control panel so an
  operator can release/re-engage the lock manually mid-run -- e.g. to freely
  draw/measure an unrelated box without aborting the alignment. Both the
  toggle and the RPC methods stay in sync (blockSignals pattern already used
  elsewhere in this file for on_live_view_enabled).
- x_ray_eye_align.py: lock_vertical_center() right after _height_centered
  flips to True; unlock_vertical_center() at the start of every fresh run
  (so a run never inherits a stale lock) and in align()'s finally block
  (so the lock never outlives a run, including on error/interruption).

FZP box (step 0) and the height-centering box itself remain fully free --
the lock isn't engaged yet at that point. LamNI/OMNY have their own
near-identical scripts but are explicitly out of scope (per the plan); the
widget change is inert by default.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
2026-09-17 14:22:20 +02:00
x01dcandClaude Sonnet 5 8cd9235e12 feat(flomni): add fermat_asymmetry (x/y spacing ratio) to Fermat scan
CI for csaxs_bec / test (push) Successful in 2m16s
Implements docs/plans/flomni-fermat-asymmetry.md as written: generate the
spiral against an inflated fovy half-width (length_axis2 * fermat_asymmetry),
then compress the accepted y-coordinate back down by the ratio before
storing -- keeps x spacing fixed at step, scales y spacing by the ratio,
preserves the requested FOV. Composes with the already-landed sub-step
randomization: the offset is subtracted at the raw (pre-compression) scale
on both axes, before the inflated-window check, so the offset's phase-shift
stays proportional to the local spacing on both axes.

Wired through the scan class (FlomniFermatScan), Flomni's global var +
queue/CLI plumbing (_TOMO_SCAN_PARAM_NAMES, scan_kwargs, tomo_parameters()
print/prompt, _set_default_tomo_params()), and the tomo_params Qt widget
(new field with a "?" help button, following the widget's existing
offsets-section help-button pattern).

Added tests verifying the inflate/compress math directly (against an
equivalent call with an inflated FOV and fermat_asymmetry=1.0, same seed)
rather than via an approximate spacing-ratio heuristic -- a first attempt
at the latter proved too noisy on a Fermat spiral to assert reliably.
Updated the flOMNI user docs and the OMNY port TODO (out of scope here,
explicitly excludes LamNI per the plan).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
2026-09-17 13:07:44 +02:00
x01dcandClaude Sonnet 5 5e26b86d03 feat(scans): randomize Fermat spiral sub-step offset (flOMNI/OMNY/LamNI)
CI for csaxs_bec / test (push) Successful in 2m14s
Restores the legacy spec behavior of drawing a fresh random offset
(uniform in [0, step)) per call and shifting the sampling lattice by
it before the FOV crop check -- decorrelates position-tied
reconstruction errors that otherwise accumulate into ring artifacts
across a ptycho-tomography series. Applied identically to all three
generators (get_flomni_fermat_spiral_pos, get_omny_fermat_spiral_pos,
get_lamni_fermat_spiral_pos), same scalar offset for x and y, matching
the historical spec. New `seed` parameter (default None = fresh
randomization) allows deterministic/reproducible calls.

The four call sites that predict Fermat point counts ahead of a real
scan (flomni.py/lamni.py's _expected_fermat_position_count(),
tomo_params.py's _compute_fermat_positions_flomni/_lamni) now pin
seed=0 so their own estimates stay reproducible across repeated calls;
their docstrings note the real scan's count can differ by a point or
two near the threshold due to the randomization. Updated the affected
tests to compare against a matching seed=0 instead of an independent
random draw.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
2026-09-17 12:30:13 +02:00
x12saandClaude Sonnet 5 1fc702b854 fix(scans): correct undersized fermat spiral iteration cap for elongated FOVs
get_flomni_fermat_spiral_pos/get_omny_fermat_spiral_pos/get_lamni_fermat_spiral_pos
sized their spiral loop's iteration cap (n_max) from the FOV's area alone,
but the spiral radius only grows as step*0.57*sqrt(ii). For an elongated
(non-square) FOV -- e.g. flomni's fovx up to 220 vs fovy capped at 100 --
this under-estimated the iterations needed, so the loop ran out before
reaching the long axis's outer region. The two explicitly-appended
bounding-box corner points masked this: they showed up regardless, while
genuine spiral fill points near the true edge silently went missing
(observed as the pattern being "cut" well short of the requested FOV).

Add a shared compute_fermat_spiral_n_max() helper that takes the max of
the old area-based estimate and a corner-radius-based estimate, so
square/near-square FOVs are numerically unaffected (verified
byte-for-byte identical) while elongated ones now reach the requested
edge (verified: flomni fovx=220,fovy=30 now reaches 109.98um vs the
82.78um it reached before, against a 110.0um target).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197yD3hTxwvrgmYr7aaYiCD
2026-09-16 11:30:46 +02:00
x12saandClaude Sonnet 5 f93e591227 feat(flomni): split omny_panda into two devices, fix free-running completion check
CI for csaxs_bec / test (push) Successful in 2m13s
Mode A (per-gate aggregate) and Mode B (continuous raw-sample) need
different PandA hardware layouts and can't run simultaneously, so
omny_panda is now two device config entries in ptycho_flomni.yaml:
omny_panda (legacy, kept commented as the restore reference) and
omny_panda_continuous (raw_stream_mode: true, enabled).

Also fixes on_complete for the continuous case. PCAP.CAPTURED can't be
used to detect end-of-acquisition on this layout: CLOCK1 drives
PCAP.gate/trig continuously, so the captured count never naturally
stops increasing while armed -- the previous settle-based wait would
have just run out its timeout. COUNTER1 (frame_counter) instead counts
completed exposures directly (triggered on the detector-trigger's
falling edge), so PandaBoxOMNY now waits for it to reach the exact
expected num_points * frames_per_trigger, mirroring the legacy
exact-count check instead of guessing when capture has "settled".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
2026-09-15 14:22:28 +02:00
x12saandClaude Sonnet 5 3012cd3154 fix(flomni): mask gate_detector_active to its own bit, record confirmed PandA wiring
TTLIN1 (detector trigger) drives both COUNTER1 (frame_counter) and the
PCAP.BITS0 gate-bit capture, confirmed 2026-09-15 against the omny-panda
hardware via TTLIN1.VAL.CAPTURE_WORD?/OFFSET? (PCAP.BITS0, bit 0).

PCAP.BITS0 is a shared 32-bit word -- other captured bit_out signals may
occupy other bit positions of it, so the raw word is not itself a clean
0/1. PandaBoxOMNY now masks gate_detector_active down to bit 0 before
publishing it, instead of trusting the whole word.

Also records the confirmed free-running setup in the developer docs: all
four encoders plus both FMC_IN channels switched to raw Value capture,
CLOCK1 driving continuous PCAP gate/trig, and the initial 100 Hz test
rate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
2026-09-15 14:13:22 +02:00
x01dcandClaude Sonnet 5 82ecab4c21 feat(flomni): add continuous free-running raw-sample mode to PandaBoxOMNY
CI for csaxs_bec / test (push) Successful in 2m14s
Adds an opt-in raw_stream_mode to omny_panda alongside the existing
per-gate averaged burst acquisition, so a burst window can instead be
captured as continuous raw samples plus a detector-gate bit and a
monotonic frame counter for completeness checking. Client-side
coalescing bounds the message rate to the BEC message bus regardless of
PandA's own network-frame batching, and completion uses a settle-based
check since an exact expected sample count isn't predictable for
continuous capture. Defaults to off, preserving today's behavior
exactly; hardware wiring/layout and the raw data schema are documented
under docs/developer/ pending PandA hardware access to configure and
validate against.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 13:44:07 +02:00
x12saandClaude Sonnet 5 3f719c8119 feat(ddg): add show_all() status command for the delay generators
CI for csaxs_bec / test (pull_request) Successful in 2m15s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m16s
Prints the current per-pair delay/width/reference/IO settings and the
trigger/burst configuration for DDG1/DDG2, following the PrettyTable
status pattern already used by the Galil controllers. Exposed via
USER_ACCESS as dev.ddg1.show_all() / dev.ddg2.show_all().

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 12:48:12 +02:00
x01dcandClaude Sonnet 5 8cf95196b7 fix(ids-cameras): snap pixel clock to a supported value; pulse auto-gain
Pixel clock: get_pixel_clock_range()'s (min, max, increment) describes
a linear range, but IS_PIXELCLOCK_CMD_SET doesn't actually accept
every value in it -- confirmed on hardware, 49/60/73 MHz all failed on
camera 41. Added Camera.get_pixel_clock_list() (IS_PIXELCLOCK_CMD_GET_
NUMBER + _GET_LIST, the SDK's authoritative source for what's settable)
and IDSCamera.get_pixel_clock_list(); IDSCamera.set_pixel_clock() now
snaps to the nearest supported value before writing, instead of
passing the raw requested value straight to the driver.

Auto gain: HW testing found continuous auto-gain has nothing further
to adjust once correctly exposed, and takes gain out of manual
control. on_connected() now pulses auto_gain_enabled on then off
(_AUTO_GAIN_SETTLE_S = 0.5s in between, for the already-running
continuous capture to feed it a few frames), instead of leaving it
enabled like auto_exposure_enabled -- still does the one-time gain
correction, but leaves gain fixed under manual control afterward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
2026-09-15 08:31:51 +02:00
x01dcandClaude Sonnet 5 9a999bed37 fix(ids-cameras): apply auto-exposure/gain defaults on connect; add pixel clock
auto_exposure_enabled/auto_gain_enabled default to True but were never
actually written to hardware at connect -- is_ResetToDefault() leaves
the sensor's auto-shutter/auto-gain off, so a fresh session ran
under-exposed until an operator happened to re-toggle the GUI switch
(which is what issued the enable call for the first time). Fixed by
round-tripping both through their subscribe callbacks in
on_connected(), same idiom as exposure_time.

Also adds pixel_clock/pixel_clock_min/pixel_clock_max Kind.config
signals, backed by the Camera.get_pixel_clock()/get_pixel_clock_range()/
set_pixel_clock() added earlier -- lowering the pixel clock raises the
achievable max exposure time, and changing it now re-derives
exposure_time_min/max from hardware automatically.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PYQTHuSxSaLCijvayoRkX5
2026-09-15 08:31:50 +02:00
x01dc 8c1c322393 fix(ids-cameras): fix auto-gain/auto-exposure SDK call, add exposure-range query
is_SetAutoParameter's pval1/pval2 are double* (8 bytes); set_auto_gain()/
set_auto_shutter() were passing a c_int (4 bytes), so the driver read
garbage past the buffer and rejected it -- this is what raised
UEyeException (surfaced as ophyd's "Subscription value callback
exception") when toggling auto gain on real hardware. Fixed to pass
c_double, matching the SDK's documented signature.

Also wrap the three hardware-write subscribe callbacks in try/except so
a driver failure logs clearly from IDSCamera instead of only via
ophyd's generic subscription-exception message.

Adds Camera.get_exposure_range()/get_pixel_clock()/
get_pixel_clock_range()/set_pixel_clock() (the max exposure time is
bounded by the current pixel clock) and matching IDSCamera
exposure_time_min/max config signals + USER_ACCESS wrappers, seeded on
connect, so the GUI can bound its exposure control to real hardware
limits instead of a placeholder range.
2026-09-15 08:31:49 +02:00
x01dc e3323e521e feat(ids-cameras): add manual exposure / auto-gain control signals
Adds exposure_time/auto_exposure_enabled/auto_gain_enabled as
Kind.config Signals on IDSCamera (mirroring live_mode_enabled), with
USER_ACCESS wrappers and hardware seeding of exposure_time on connect.
Extends SimIDSCamera's backend to match, and adds unit test coverage.

Per docs/plans/ids-camera-manual-exposure.md.
2026-09-15 08:31:48 +02:00
x12saandClaude Sonnet 5 cc93d563bf fix(tests): accept _internal kwarg in tomo_scan_projection test doubles
CI for csaxs_bec / test (pull_request) Successful in 2m6s
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m8s
7d64b49 added an _internal kwarg to LamNI.tomo_scan_projection() (used
by tomo_alignment_scan/_at_each_angle to skip the redundant per-angle
filter-out-of-beam check) but left three tests' fake/lambda
replacements without it, causing them to fail in CI with
"got an unexpected keyword argument '_internal'".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
2026-09-15 08:27:47 +02:00
menzel f74f09de7b fix(cont_grid): wait for the move to reach the motor before triggering
Read the Docs Deploy Trigger / trigger-rtd-webhook (push) Successful in 2s
CI for csaxs_bec / test (push) Successful in 2m3s
at_each_point issues the line move and then sleeps before firing the
burst. The sleep was acc_time alone, which covers the acceleration ramp
and nothing else -- while the move still has to cross the scan server ->
device server -> EPICS -> controller path first. acc_time shrinks with
the scan velocity; that path does not. On a slow scan the burst
therefore began well before the stage moved, and the first points of
every line piled up at the line start.

Measured from the position readback of five commissioning scans, as the
distance the stage was behind the trigger grid once the lag stopped
growing:

    scan   exp_time   v_cmd         lag       latency
     254     20 ms    0.5   mm/s   29.7 pts   0.594 s
     450     50 ms    0.1   mm/s   11.9 pts   0.595 s
     324     15 ms    0.667 mm/s   41.5 pts   0.623 s
     473    100 ms    0.065 mm/s    6.3 pts   0.630 s
     411     50 ms    0.1   mm/s   12.7 pts   0.635 s

Constant to +-3.5% across a 6.7x range of exposure and a 10x range of
velocity, on two axes. Scan 473 is sampled 74 times per line and shows
the shape plainly: the lag appears in the first interval and then holds
at 6.3-6.4 points for the rest of the line while the velocity sits at
exactly the commanded 0.065 mm/s. A start-up offset, not a velocity
error, and the stage itself is blameless.

Deliberately not solved by polling for motion. Observing the readback
costs a round trip of this same ~0.6 s, so it would trade a systematic
offset for a jitter of similar size -- and a constant offset displaces
every line equally, where a varying one shears the image line by line
and cannot be undone afterwards. The reproducibility is the asset here,
not the enemy.

The value lives on ddg1 next to the shutter delay, with a setter in
USER_ACCESS, because that is where cont_grid already fetches its trigger
timing. It is not a property of the delay generator and the docstring
says so. Setting it to 0 reproduces exactly the previous timing without
a redeploy, which is the intended way back if this makes things worse.

The real fix is to trigger the DDG from the motor
(scan_type: hardware_triggered), taking the round trip out of the timing
chain rather than compensating for it. This is the stopgap until then,
and it rests on the latency staying constant -- which is worth
re-checking with the same measurement whenever the deployment changes.
2026-09-09 12:30:16 +02:00
menzel 3ddd0d7957 fix(ddg1): give the shutter the time it actually needs to open
The first point of every line is under-exposed. Four commissioning scans
put a number on it, against the 2e-3 head start that was in place:

    scan   exp_time   first/near   lost
     324     15 ms       0.655     5.18 ms
     254     20 ms       0.732     5.36 ms
     411     50 ms       0.881     5.93 ms
     450     50 ms       0.879     6.04 ms

A fixed time, not a fixed fraction: it varies by 17% across a 3.3x range
of exposure while the fraction varies by 2.9x. The same deficit appears
on the integrated scattering, a different detector behind a different
gate, so the cause is upstream of both readout chains rather than in
either of them. 2e-3 allowed and ~5.6e-3 still lost means the shutter
needs about 7.6 ms, rounded up to 8 ms: the spread across the four scans
is 0.9 ms, so the third digit is not meaningful, and overshooting costs
only the difference in dead time at the start of each line.

The trigger scheme was already right -- the shutter fires on cd at t0
and the acquisition on ab is held back by _shutter_to_open_delay, with
the widths, burst_period and cont_grid's acc_time and premove all
derived from it. Only the value was wrong, and it was a literal in two
places, so setting one and not the other would have been silently undone
by keep_shutter_open_during_scan.

Lifts it to DEFAULT_SHUTTER_TO_OPEN_DELAY next to the other defaults,
with the measurement recorded, and adds set_shutter_to_open_delay to
USER_ACCESS so the value can be converged from the client instead of by
redeploying the device server. It is bounded, because a delay is paid on
every line and a fat-fingered value would stretch the scan rather than
fail.

Cost at the new value is 6 ms per line -- 0.26 s over scan 450 -- and
0.8 um of extra premove.

The existing stage test asserted the 2e-3 literal and now asserts the
constant. New tests cover the default, the bound, the USER_ACCESS entry,
that a set value actually reaches the ab channel while cd still fires at
t0, and that keep_shutter_open_during_scan discards a tuned value, which
is a sharp edge worth pinning rather than leaving to be rediscovered.
2026-09-09 12:30:16 +02:00