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
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>
Idea only, not implemented. Includes the 4-point DAP sine-fit stability
study and the decision to send submit=-1 as the skip signal.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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
Same gap as flomni.md (fixed previous commit): lamni.md had no mention of
stitching. Add the equivalent paragraph using lamni's own parameter names
(lamni_stitch_x/y, lamni_piezo_range_x/y) and cite tomo_scan_projection(),
which passes them straight through to scans.lamni_fermat_scan().
Also add an omny/AI_docs TODO note -- omny.md has the same gap and omny's
mechanism turns out to use identical parameter names to flomni's
(stitch_x/stitch_y/tomo_stitch_overlap), so the flomni.md paragraph should
carry over directly once written.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
flomni.md had no mention of stitching at all. Add a paragraph explaining
the tiled-Fermat-scan mechanism (flomni.py:3521-3527) and tomo_stitch_overlap,
matching the existing zero_deg_reference_at_each_subtomo callout's style.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Record the exposure-active-gated PCAP.gate variant discussed for reducing
idle-time data volume in raw_stream_mode (currently PCAP free-runs via
CLOCK1 regardless of detector state). Documentation only -- not implemented
or tested against hardware.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0132KBoxsovfcMNRGJhS1Pbw
Add an intuitive spacing table (x spacing stays at step, y spacing
becomes step/fermat_asymmetry) with a worked example, derived from
measuring actual generated positions (area-per-point vs. point count,
averaged over 20 randomized draws per ratio) rather than just stating
the formula. Mirrored into the OMNY port TODO so the same relationship
gets documented there once ported.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
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
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
Legacy spec code drew a fresh random offset per projection to shift the
Fermat lattice phase before FOV cropping (mitigates ptycho-tomo ring
artifacts). None of the current BEC Fermat spiral generators (flOMNI,
OMNY, LamNI) draw this offset -- all three are fully deterministic.
Plan only, not implemented; applies identically across all three setups.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lx3KffiFyyDMKT8vvPENUW
Introduces fermat_asymmetry, a ratio of x spacing to y spacing for the
Flomni Fermat-spiral scan: x spacing stays fixed at step while y spacing
scales by the ratio, with the requested field of view preserved (so the
total position count changes with the ratio). Covers the scan class,
global var/CLI, queuing, and the tomo_params Qt widget. Plan only, not
implemented -- written during a live beamtime experiment.
Also flags this for a future OMNY port (same Fermat-spiral algorithm),
explicitly excluding LamNI.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WW873PFzsY4N1r65aQgfFn
Once height-centering fixes the sample's vertical position during the
flomni alignment procedure, the selection box for the 0-180 deg rotation
steps should auto-snap to the FZP height instead of requiring manual
vertical placement each time. Plan only, not implemented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CpPtD7tCRwRZZnREhWcJi8
The Example: line was left out of sync with the parameter table fix
in 5141854 - argument order didn't match, and burst_at_each_point was
missing. Now mirrors the code docstring's example.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Captured 2026-09-15 directly from the omny-panda hardware via
GetState() (ophyd_devices/devices/panda_box/utility_scripts.py), one
per Design: omny.ini (Mode A) and omny_freerun.ini (Mode B). Mode B was
speed-tested successfully up to 20 kHz same day, though the saved
omny_freerun.ini itself reflects CLOCK1.PERIOD=0.0001 (10 kHz) -- flagged
in the setup doc as a discrepancy to double-check rather than assumed
away.
Setup doc updated with the exact save/load commands and a diff summary
of the two layouts' mode-specific differences (PCAP.GATE/TRIG source,
CLOCK1.ENABLE, capture types).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Both omny_panda and omny_panda_continuous ended up connected and active
simultaneously against the same physical PandA, even though only
omny_panda was enabled: true -- a device already connected apparently
doesn't disconnect just because a later config reload disables it.
Comment the inactive mode's block out entirely instead of relying on
enabled: false, so BEC never constructs the device object at all. Docs
updated to correct the earlier (wrong) guidance that toggling enabled
alone was sufficient to switch between modes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
Mode B (omny_panda_continuous) confirmed working end-to-end against real
PandA hardware on 2026-09-15: correct per-channel raw values, masked
gate_detector_active, and frame_counter-based completion all verified.
Switching back to Mode A (omny_panda) for A/B comparison testing.
Both device entries now stay uncommented in ptycho_flomni.yaml, toggled
via enabled: true/false, rather than commenting/uncommenting blocks --
confirmed a disabled device is an inert placeholder that BEC's device
server never connects to. Speed testing Mode B beyond the initial 100 Hz
is still TODO.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TsfqLcPWspjJ6vedMyCxco
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
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
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>
Add a temporary single-camera (ID 41, color) device config for manually
verifying the new exposure/auto-gain controls against real hardware
outside a live beamtime, and update the plan's status accordingly.
Reorders omny.md's parameter table to match the actual signature,
adds the previously undocumented readout_time parameter, and adds an
Examples block to OmnyFermatScan's docstring (mirroring the flomni fix).
Also rewords flomni.md's corridor_size default to read "3 um" for
consistency with omny, while noting it is auto-estimated when omitted.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Reorders parameters to match the actual signature, adds the
undocumented burst_at_each_point parameter, and corrects the
corridor_size default (None/auto-estimated, not a literal 3 um).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Port the flomni doc addition to omny -- both setups share the identical
tomo_alignment_fit mechanism (OMNYAlignmentMixin.get_alignment_offset
mirrors Flomni's), so the same command-line offset nudge applies verbatim.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
Add a snippet to the XrayEye alignment section showing how to patch the
constant x/y offset terms of tomo_alignment_fit directly on the command
line, for a quick few-micron correction (e.g. after moving foptz) without
recording a new fit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FFeiCHv3qUxxssmP9Qzhom
No code changes -- IDSCamera and the xrayeye widget are both used
during beamtimes. Revises the earlier plain-method design to
Cpt(Signal, kind=Kind.config) (mirroring live_mode_enabled) so the
xrayeye widget can read exposure/auto-exposure/auto-gain state via
its existing device_read_configuration subscription instead of
polling the device, and adds the corresponding GUI control knobs
(auto-exposure/auto-gain toggles, exposure spinbox) to the plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s92UHyMfov6Fb934pGqcU
No code changes -- IDSCamera is in production use during beamtimes.
Documents the intended USER_ACCESS additions (get/set_exposure_time,
set_auto_exposure_enabled, set_auto_gain_enabled) for a future session.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014s92UHyMfov6Fb934pGqcU
find_rotation_center_smear_experimental() rotated 45->0 for its post-shift
verification sweep, needlessly re-treading ground the preceding full smear
sweep already covered and adding a long extra blocking move that could
starve the GUI heartbeat past _gui_call_with_retry's budget, surfacing as
RuntimeError: GUI is not alive. Sweep straight back to 0 instead, and widen
the retry budget (8x1.5s -> 25x2.0s) so update_frame() can ride out long
blocking moves on slower hardware.
Also tidy a stray comment-block artifact in ptycho_lamni.yaml and update
lamni.md docs for the current xrayeye_rotation_center_calibration_* API.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Remove the manual fit-parameter reload docs: fit params already load
automatically, and the fallback path pointed at the retired
~/Data10/specES1 layout.
- Fix stale slits/slit0wh references (slits takes no argument; the
gap monitor is now slit1).
- Stop writing the plain-text xrayeye_alignmentvalues archive in
write_output() -- it was only for external fitting scripts and now
collides with xrayeye_alignmentvalues being a directory used for the
per-run HDF5 archives.
Add docs/developer/lamni_smear_architecture.md covering the three-tier
device/ipython-client/GUI architecture this feature exercises: the
camera's two PreviewSignal channels (image vs smear_preview) and why
composite pushes needed their own channel, XRayEye.set_live_view_signal
and exactly when the caller must switch it back, the sweep algorithm
(non-blocking mv() + ScanReport.status polling + max-projection), and
the bw-generate-cli regeneration step required whenever a plugin
widget's USER_ACCESS changes (the cause of this session's confusing
AttributeError even after a full client/GUI restart).
Also expand the user-facing description in lamni.md with a note on the
live-updating composite display and a cross-reference to the new
developer page.
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>
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>
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>
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>
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>