- Reword the extended-loop accept/iterate prompt: the old either/or
phrasing made a "yes" answer ambiguous about which half it affirmed.
- Freeze a single frame (and close the shutter/live-view per
keep_shutter_open) before requesting the rotation-centre click, same
as every other click site in this file -- previously the shutter and
5 Hz live view stayed on for the whole time the operator was
deciding where to click, exposing the sample for no benefit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lamni_move_to_scan_center's shift_x/shift_y are absolute offsets from the
currently-configured lsamx_center/lsamy_center (unchanged until the operator
confirms at the end of the procedure), not incremental deltas from wherever
the stage currently sits. The extended-sample iteration loop was passing
only the latest click's delta each time, so each new iteration partially
undid the previous one's correction instead of adding to it. Fixed by
accumulating the total shift across iterations and always applying the
running total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
rotation_center_calibration_start(sample_type="isolated") didn't match the
xrayeye_* naming convention used by the other X-ray eye entry points, and
the sample_type string enum was easy to forget (both the parameter name
and its accepted values). Split into two entry points instead:
lamni.xrayeye_rotation_center_calibration_isolated(keep_shutter_open=False)
lamni.xrayeye_rotation_center_calibration_extended(keep_shutter_open=False)
Both delegate to a shared _xrayeye_rotation_center_calibration() private
helper (single copy of the KeyboardInterrupt cleanup). Updated the user
docs to match; XrayEyeAlign.find_rotation_center()'s own sample_type
parameter is unchanged since it's internal shared implementation, not
what was being objected to.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three refinements found while testing find_rotation_center() against the
simulated LamNI:
- Drop the trailing tomo_rotate(0) at the end of find_rotation_center():
both the isolated path (explicit rotate-back before applying the
correction) and the extended path (verification sweep ends at 0) already
leave lsamrot at 0 by then, so it was a pure duplicate move.
- Simplify _live_sweep() to rotate directly to each waypoint instead of
stepping through artificial step_deg-sized sub-moves with settle pauses
in between -- a single tomo_rotate() already produces continuous physical
rotation, so the stepping added nothing but jerkier motion and extra
queue overhead.
- Add _ensure_at_configured_center(), called at the start of
find_rotation_center() before lfzp_in(). lamni_move_to_scan_center's
interferometer-drift safety check (LamNIFermatScan.py) assumes lsamx/
lsamy start near their configured center and silently skips the entire
corrective move if that drift exceeds 150um -- exactly the situation
find_rotation_center() is meant to handle, since its purpose is
recalibrating a potentially-stale center. Confirmed this was a real gap:
a run where the stage hadn't started at its configured center produced a
correct absolute target (verified via the interferometer reading) but a
confusingly small apparent motor delta.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Both align() and find_rotation_center() moved the FZP in and immediately
asked the operator to click its centre, but only confirmed OSA was out
afterwards (via loptics_out(), which runs after that click). If OSA was
left in from a previous scan setup, it would still be in the beam path
during the FZP reference click. Add an explicit losa_out() call right
before lfzp_in() in both procedures; it's a cheap, skip-if-already-out
no-op otherwise.
Found while testing find_rotation_center() against the simulated LamNI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lsamx_center/lsamy_center have to be re-measured by hand for nearly every
new sample (the rotation stage is tilted on top of lsamx/lsamy, so the
effective axis position shifts with sample thickness/mounting). Add
XrayEyeAlign.find_rotation_center()/lamni.rotation_center_calibration_start()
to automate this using the existing X-ray eye GUI and lamni_move_to_scan_center
interferometer-feedback move:
- "isolated" sample: click particle centre at 0 and 180 deg, use the
midpoint (angle-tilt-independent) as the rotation axis position.
- "extended" sample: live 0->180->0 sweep for visual identification, single
click, then a verification sweep and an accept/iterate prompt.
Both write the result to lsamx/lsamy's "center" userParameter after
confirmation. Heavily verbose logging throughout since this is fundamentally
an interactive, hardware-in-the-loop procedure that can't be fully exercised
by automated tests.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
renderInstrument() checked setup.temperatures for truthiness, but an
empty JS object is truthy, so LamNI's deliberately-empty temperatures
dict (it has no monitoring device) still rendered an empty
"Temperatures" heading/table. flomni only worked by coincidence since
its _TEMP_MAP always has entries. Adds a hasTemps check
(Object.keys(...).length>0), mirroring the existing hasParams idiom
one line above for the same card.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
HttpUploader now accepts a single URL or a list, fanning every upload
(dir/changed/single-file/cleanup) out to each configured server with an
independent per-URL mtime cache, so a newly added mirror backfills on
its own and a failing one doesn't block the others. Session-auth
endpoints (session_query/set_password) still derive from the primary
URL only.
flomni.py/lamni.py now mirror to https://omny-test.psi.ch/upload.php
alongside production, marked TEMP for easy removal once that server
is evaluated. Adds OMNY_shared/AI_docs/WEBPAGE_UPLOAD_MECHANISM.md
consolidating the upload mechanism (the prior doc predates this file
and still called LamNI a stub).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lets testing/debugging sessions running under a mismatched BEC account
still exercise the real upload path on demand -- e.g. to confirm uploads
work from the allowed subnet, or to confirm the remote server's IP
whitelist rejects uploads from outside it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EpsStatusGenerator was given its own upload_url when embedded in
WebpageGeneratorBase, spinning up a second HttpUploader that redundantly
re-scanned and re-POSTed the same shared output directory the base
generator's own uploader already covers every cycle. It now always embeds
with upload_url=None. Also adds .svg to HttpUploader's suffix whitelist so
eps_synoptic.svg is actually picked up (matches a corresponding fix already
applied server-side in upload.php/auth_gate.php).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Lands the standalone EPS status prototype (OMNY_shared/eps/) as tracked
files and wires it into WebpageGeneratorBase so it starts alongside the
experiment status page, sharing one output dir/port. Demo-vs-real data is
now auto-detected from the host's subnet (129.129.122.x = on the X12SA
beamline) instead of requiring an explicit --demo flag, independent of the
existing account-mismatch local-only fallback. Cross-links the two page
families via a new EPS nav button in status.html's header (the EPS pages
already linked back). Also moves HttpUploader/LocalHttpServer into a
shared web_common.py, picking up an SSRF fix (allow_redirects=False) that
the inline copy lacked.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
test_lamni_account_change_reset.py and test_lamni_tomo_angles.py set
builtins.__dict__["bec"] directly with no cleanup, so a leaked fake
account ("e22222") from an earlier test file was still in place when
test_x_ray_eye_align.py later constructed a real LamNI(), tripping
_maybe_reset_params_on_account_change()'s interactive yesno() prompt
and failing under pytest's captured stdin. Use monkeypatch.setitem so
pytest reverts the global after each test, matching the pattern already
used in test_lamni_tomo_alignment_scan.py.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a worked example to both lamni.md and flomni.md showing a hook
loaded from a file that temporarily bumps tomo_countingtime and
tomo_shellstep every 5th projection within sub-tomogram 2 (tomo_type 1),
restoring both in a finally block -- demonstrating that a hook can
change any scan parameter, not just move devices like the existing
polarizer example. Notes that progress["subtomo"]/["subtomo_projection"]
work the same way for tomo_types 2/3, just with an open-ended
sub-tomogram count there instead of type 1's fixed 8.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed this script no longer works and needs a rewrite. Commented
out the subprocess.run() call in write_pdf_report(), mirroring Flomni
which already has the identical call disabled the same way. omny.py's
own copy is left untouched (out of scope for this migration).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flomni.add_sample_database() and LamNI.add_sample_database() were
byte-for-byte identical -- both just forward to
self.tomo_id_manager.register(), a TomoIDManager from
omny/omny_general_tools.py that each setup already constructs
independently in its own __init__. Moved the method into
TomoQueueMixin (OMNY_shared), the base class both already inherit,
same "moved code, not changed logic" precedent as the tomo-queue
backend and webpage generator. omny.py's own separate, older
add_sample_database() (different URL, no e-account fallback) is
untouched.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flomni's ring-progress center label already showed "Est. finish" and
the active at-each-angle hook name; LamNI's equivalent
(_lamnigui_update_progress()) was missing both. Ported the same
formatting/display logic over so the two setups' progress displays
match.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
write_pdf_report() didn't mention the active at_each_angle_hook at all,
unlike Flomni's equivalent which already appends the hook's description
and source to both the PDF and its scilog entry. Ported that pattern
over using the same _describe_active_hook()/_active_hook_source() shared
helpers lamni already uses elsewhere (end-of-scan summary,
tomo_parameters() printout) -- no new logic needed, just the missing
call site.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
LamNI's tomo_scan() wrote a separate scilog entry ("Starting subtomo:
N...") every time the sub-tomogram number changed -- 8 extra entries
per tomo_type-1 scan, and one per sub-tomogram traversed for types 2/3
-- leftover behavior carried over from omny.py during the flomni->lamni
port. Flomni itself never does this: it only writes at scan start (PDF
report) and scan end (timing summary). Removed the three
_write_subtomo_to_scilog() call sites and the now-unused method so
LamNI's scilog behavior matches Flomni's.
Also fixed a stale test (test_tomo_queue_reacquire_rejects_when_another_job_is_incomplete)
that encoded the old, buggy tomo_queue_reacquire() invariant (rejecting
on ANY other job being incomplete/running) rather than the fixed one
from the prior commit (only an EARLIER job is a real conflict) --
split it into two tests covering both directions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
_format_projections() computed type 3's count with type 1's fixed
"N*8 sub-tomograms" formula, ignoring golden_max_number_of_projections
-- the value that actually bounds a type-3 run (lamni.py/flomni.py's
type-3 scan loop has no sub-tomogram cap, it stops only once
ii > golden_max_number_of_projections, same as type 2). This produced
an ~8x-inflated "Projections" column (e.g. 240 instead of 30) for any
type-3 job. Merged type 3 into type 2's branch since both share the
same uncapped/golden_max_number_of_projections model.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The single-resumable-job invariant check scanned all other jobs, but
jobs after job_index get force-reset to pending anyway -- so a later
incomplete/running job was never a real conflict, only an earlier one
is (since earlier jobs are left untouched).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
register_at_each_angle_hook() stores a function object by reference, so
editing a hook's def and forgetting to re-register left the old version
running silently -- easy to miss mid-beamtime. Detect it via
func.__globals__ (a redefined top-level def, or a reloaded module,
mutates the same namespace dict in place) and auto-adopt the new
definition with a printed note instead of failing or staying stale.
Centralizes the shared lookup/error logic (previously duplicated in
lamni.py and flomni.py) into TomoQueueMixin._resolve_at_each_angle_hook().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The tool is called BEC_ptycho_align now, not SPEC_ptycho_align.m as
the previous docs/comments said -- fixes the references introduced in
the previous commit (12b2538) in lamni.py's docstrings/print message
and the user docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replaces the awkward documented workaround (configure a full tomo_type
1 setup with 96 projections, then launch just sub_tomo_scan(1, 0)) with
a dedicated command, ported from flomni's tomo_alignment_scan(): adjust
tomo_parameters() (FOV/step/counting time), then call
lamni.tomo_alignment_scan() directly -- no tomo_type/sub-tomogram
bookkeeping involved, matching flomni's clean two-step workflow.
Runs 12 points evenly spaced across the full 360 degrees (lamni has no
180-degree symmetry the way flomni does, so unlike flomni's 5-point/
180-degree scan, this covers the full circle -- point count matches
what the old workaround's docs defaulted to, endpoint=False since
360==0 degrees). Aborts if x-ray-eye alignment hasn't been done yet
(tomo_fit_xray_eye unset), mirroring flomni's equivalent guard.
write_alignment_scan_numbers() writes the same 4-line scan-number/
angle/offset log flomni's version does, to
~/data/raw/logs/ptychotomoalign_scannum.txt for SPEC_ptycho_align.m,
also printed at the console (flomni's own console-print equivalent is
dead/commented-out code; lamni's actually prints).
Scope note: flomni's version conditionally skips its eye-out/optics-in
transition when already in measurement condition with feedback
running, to avoid an unneeded interferometer reset -- lamni has no
equivalent helpers for that check, so this calls leye_out()
unconditionally instead. Left as a possible follow-up, not in scope
here.
Item 6 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md. Documented in
docs/user/ptychography/lamni.md's "Fine alignment" section.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's ffzp_info() OSA collision-clearance section (compares
live losaz against nominal "in" position and a collision-boundary
constant, warns if closer to collision than nominal or reports
clearance at IN) -- same formula shape and sign convention as flomni
(motion directions confirmed identical between the two setups).
lamni's collision-boundary constant has never been measured, so it's
read from loptz's device config (userParameter.collision_offset) via
a new _get_user_param_optional() helper -- unlike the existing
_get_user_param_safe(), it returns None instead of raising when the
parameter is undefined (as it currently is for every lamni device
config, including the simulated one), since absence here means
"not yet commissioned" rather than a config error. lfzp_info() prints
a clear warning and skips the numeric estimate entirely until someone
adds the real measured value to the config -- deliberately not a
CLI-settable/global-var parameter, since this is deployment-level
hardware calibration.
Item 7 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's ffzp_in()/fosa_in() "skip the move (and, for the FZP,
the expensive feedback-disable + reset-enable cycle) if already there"
optimization to lamni's lfzp_in()/losa_in(). Also adds the same
skip-if-already-there check to losa_out(), which has no flomni
equivalent to mirror (fosa_out() always moves unconditionally there)
-- added as a lamni-side enhancement per discussion.
Checked every existing caller of lfzp_in()/loptics_in() before making
this change, since a skip changes behavior for callers that used to
get an unconditional reset. Found one real risk:
x_ray_eye_align.py's "Step 0: FZP centre" (the start of a fresh
alignment run) had its manual feedback disable/enable calls already
commented out, meaning it relied entirely on lfzp_in()'s old
unconditional reset to establish a correct interferometer zero.
flomni's equivalent call site doesn't have this problem because
flomni's alignment flow does its own explicit
feedback_enable_with_reset() independently beforehand -- lamni's
doesn't. Fixed by passing force_feedback_reset=True at that one call
site so it keeps its current always-reset behavior; no other lamni
caller needed this.
Item 4 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's _maybe_reset_params_on_account_change()/
_set_default_tomo_params() to lamni: at LamNI.__init__, if the active
BEC account differs from the one defaults were last applied for
(persisted via defaults_applied_for_account), interactively offer to
reset tomo params to defaults -- preventing a new experiment silently
inheriting the previous one's tuned FOV/stitch/etc. Same account is a
silent no-op, so restarting a client mid-experiment never disturbs
tuned parameters.
_set_default_tomo_params() is a direct port adapted to lamni's own
param names (tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y
instead of fovx/fovy/stitch_x/y, no tomo_angle_range/
single_point_random_shift_max -- lamni has neither concept). Per-
sample alignment state is deliberately not touched, matching flomni.
Item 2 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's per-projection/tomogram JSONL timing log
(~/data/raw/logs/timing_statistics/*.jsonl, feeding a future scan-time
prediction model) and the scilog_last_ptycho_scans(n) user command to
lamni. Direct port adapted only for lamni's own param names (no
fovx/fovy/stitch_x/stitch_y/single_point_instead_of_fermat_scan --
uses tomo_circfov/lamni_stitch_x/y/lamni_piezo_range_x/y instead, no
single-point concept at all).
_log_projection_timing() is only called on a clean (non-error_caught)
completion of _tomo_scan_at_angle()'s retry loop, so an
AlarmBase/TimeoutError attempt (no reliable duration measurement)
doesn't pollute the log.
Item 1 of csaxs_bec/bec_ipython_client/plugins/LamNI/AI_docs/
FLOMNI_LAMNI_FEATURE_GAPS_2026-07.md.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up sweep after this session's ports (timing overview, scan
interruption handling, tooltip filtering, constant shift, hook
display, zero-deg reference). Found and verified 9 items: a missing
per-projection/tomogram timing log + scilog_last_ptycho_scans()
command, no reset-tomo-params-on-account-change offer, no
collect_empty_frames() flat-field acquisition, no skip-if-already-in
optimization on lfzp_in(), no hard-stop button wired into lamni's
GUI, tomo_alignment_scan() with no clear lamni equivalent (lamni has
a different lookup-table correction scheme instead -- needs
discussion, not necessarily a gap), no OSA-collision-clearance
warning in lfzp_info(), a large confirmed-hardware-specific block
(sample-transfer/gripper/laser-tracker methods, not portable), and a
webpage-display opportunity that's symmetric on both setups rather
than a one-sided gap. No code changes -- findings only, for review.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extend commit range to c3877ec, add item 7 (zero_deg_reference_at_each_subtomo
for lamni tomo_type 1), update expected test count to 423, and add a
manual-check line for the new feature.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Ports flomni's zero_deg_reference_at_each_subtomo to lamni's "8 equally
spaced sub-tomograms" mode: an extra dedicated projection at exactly 0
degrees before every sub-tomogram where the rotation naturally passes
back through 0, plus one final shot once the tomogram completes, for
tracking radiation damage over a long acquisition. lamni's tomo_type
2/3 already had the equivalent (golden_projections_at_0_deg_for_damage_estimation,
byte-for-byte identical to flomni's) -- only tomo_type 1 was missing.
_subtomo_starts_near_zero() uses subtomo_number % 2 (every odd
sub-tomogram) rather than flomni's 360-mode subtomo_number % 4 == 1 --
confirmed against lamni's actual rotation behavior rather than derived
from the position-array math, which doesn't cleanly split odd/even.
The GUI (tomo_params.py) needed no new UI code: _build_type1_section()
already gated this control on a per-setup has_zero_deg_reference flag,
so enabling it for lamni was just flipping that flag plus adding the
param name/default, mirroring flomni's entries exactly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Brief AI_docs note covering commits 6c3d603..8a7a350: what changed and
what to test (automated + manual/simulated-session) to confirm the
branch is in good shape before relying on it at the beamline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FlomniFermatScan/LamNIFermatScan already refuse to run with fewer than
20 positions (_check_min_positions(), raises ScanAbortion), but only
once the scan actually starts -- too late for an unattended tomo-queue
run, where the job just aborts mid-run and pauses the queue. Surface
the same prediction earlier instead, as a non-blocking warning:
- Extracted the position-generating methods on both scan classes
(get_flomni_fermat_spiral_pos, and lamni's chain --
_lamni_compute_scan_center, _lamni_compute_stitch_center,
_compute_total_shift, _lamni_check_pos_in_fov_range_and_circ_fov,
get_lamni_fermat_spiral_pos) into pure @staticmethods, so the exact
same algorithm the scan server runs can also be called from
client-side code without a live scan session. Behavior-preserving --
verified against the existing exact-position/instruction assertion
tests for both classes. Lifted the hardcoded "20" into a _MIN_POSITIONS
class attribute on each, so client code references the same threshold.
- lamni.py/flomni.py: new _expected_fermat_position_count() calls the
real scan-class algorithm with the live tomo parameters and prints a
warning line in tomo_parameters() when below the minimum.
- tomo_params.py: a new live-updating "Estimated Fermat scan points"
field (mirroring the existing achievable-step preview's styling),
wired to the fov/step/stitch/piezo-range fields, flagged orange below
the minimum -- never blocks Submit/Add-to-queue.
lamni's circular-FOV crop is angle/stitch-dependent, so the estimate is
representative (center tile, angle 0 for the CLI; the live stitch tile
for the GUI, which has those fields right there) rather than an exact
per-projection guarantee -- documented as such in both places.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add both to the tomo-queue command reference in flomni.md and
lamni.md: tomo_queue_resume() as an alias for tomo_queue_execute(),
and tomo_queue_reacquire(job_index, projection_number) for reopening
a job (including an already-"done" one) at an earlier projection and
cascading later jobs to pending, mirroring the code added in the
previous commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add tomo_queue_reacquire(job_index, projection_number) to the shared
TomoQueueMixin: reopens any queue job (even an already-"done" one) at
an earlier projection than its own recorded progress, and resets every
job queued after it -- tomo and command jobs alike -- to "pending", so
a following tomo_queue_resume() (a new alias for tomo_queue_execute(),
added for naming symmetry with tomo_scan_resume()) re-runs everything
from there forward in order. Reuses the existing tomo_scan_resume()
machinery by writing the requested resume point into the shared
progress global var, rather than adding new parameters to
tomo_scan()/tomo_queue_execute(). Guards against clobbering another
job's progress if one is already "incomplete"/"running" elsewhere in
the queue.
A flat projection number is used as the resume-point unit for all
three tomo_types, including type 1 (8 equally-spaced sub-tomograms),
via a new per-setup _resolve_type1_projection() that maps it to
(subtomo_start, start_angle). flomni's version reuses its existing
_subtomo_angle_plan(); lamni's inline angle math in sub_tomo_scan() was
extracted into an analogous _subtomo_angle_plan() static method first
(behavior-preserving -- verified against the existing angle-math test
suite) so both setups share the same pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
flomni's tomo_parameters() CLI printout already showed the active
at_each_angle_hook; lamni's didn't, making it easy to have a custom
per-projection hook active without noticing from the parameter dump.
Added the identical guarded print, reusing the shared
TomoQueueMixin._describe_active_hook() helper both classes already
inherit. Also switched lamni's tomo_scan() end-of-scan summary to use
that same helper instead of the raw attribute, matching flomni's
equivalent block and correctly flagging a stale/unregistered hook name.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Correction 1, correction 2, and the x-ray-eye correction were already
printed for every projection, but the manual/constant shift
(manual_shift_x/manual_shift_y) applied in the same offset math wasn't
-- added on both setups, right after the existing correction calls in
lamni's tomo_scan_projection() and flomni's tomo_scan_projection() /
tomo_acquire_at_angle().
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Hovering a queue entry showed every snapshotted param unconditionally,
including golden-ratio settings on a type-1 (non-golden) job -- and
vice versa, type-1-only fields (angle range, zero-deg reference) on a
golden-ratio job. Reuses _update_type_visibility()'s existing
per-tomo_type section/field gating (already used for the live edit
form) to filter _job_tooltip()'s params list the same way, for both
flomni and lamni jobs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Port two flomni tomo_scan() behaviors lamni was missing: an end-of-scan
timing overview (total time, time excluding gaps, time lost to gaps,
sample/hook info) printed and written to scilog, and automatic repeat
of an entire projection when the beamline interlock trips mid-scan.
The latter needed two pieces together: enabling
bec.builtin_actors.scan_interlock at tomo_scan() start (previously
never enabled for lamni), and wrapping _tomo_scan_at_angle in
@scan_repeat so the resulting ScanRestart (or any other transient
exception) redoes the whole projection instead of aborting the scan.
A new LamNIError marks the one non-retryable case (unregistered
at_each_angle_hook), replacing a plain ValueError there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>