The DMM-through / CCM-active check now lives in _attenuation_allowed and
fil_trans calls it, so there is one copy rather than two drifting ones.
A guard that is duplicated is a guard that gets fixed in one place only.
Behaviour is unchanged: the same three PVs are read with the same
fallbacks, the same warning is printed, the same default-NO prompt is
raised through OMNYTools.yesno, the same "Safe fallback" of refusing
when no prompt is available applies, and fil_trans(1) still bypasses the
check entirely.
Split from the previous commit so that this half can be reverted on its
own if it misbehaves at the beamline, without taking fil_comb with it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
fil_trans selects by transmission, which is the wrong tool when a
particular foil is wanted: it optimises against the thickness table and
which combination wins is not something to rely on. The alternative was
four hand-copied stage coordinates, where the out positions differ per
unit (25.0 / 25.5 / 25.8 / 25.0) and getting one wrong leaves the stage
off any tabulated slot -- _fil_trans_report matches within 0.1, so the
combination then reports as unidentified.
fil_comb takes one slot number per unit, 1..6 with 1 = out, as the SPEC
fil_comb did, and looks the coordinates up. It reuses
_position_transmission, _print_combination and _execute_combination
unchanged, so it reports and moves exactly as fil_trans does, including
the dry run and the default-yes prompt.
Every slot is validated before the safety prompt is reached, so a typo
raises rather than putting a question about an unexecutable combination
in front of someone. Disabled slots -- Fe5 and the redundant opens --
raise too: _all_combinations skips them, so fil_comb must refuse them
rather than move somewhere fil_trans would never choose.
_attenuation_allowed is added here but not yet wired into fil_trans;
that is the next commit, so this one cannot change existing behaviour.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017kLGTqTyXzTT4swt3CfTkV
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.
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.
Adds a Print button to OMNY_SampleStorage that renders an A4 PDF
replica of the on-screen stage/gripper/magazine grid (via
bec_lib.pdf_writer.PDFWriter) and sends it to the local CUPS queue
WSLA_X12SA via `lp`. Checks printer availability with a quick local
`lpstat` query first, so a host without that queue gets a plain
"printer not available" message instead of a raw CUPS error; the
actual `lp` call still offers Retry/Ignore on failure, mirroring the
existing P-touch label-print fail-soft pattern.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BgQoxFiJRZi1FnmnF8epKh
make_flomni_for_golden_resume() builds a bare Flomni via object.__new__,
bypassing __init__, so it never got measured_log or a working
sample_get_measured_log_key -- both added by the measured-sample-tracking
commit for the P-touch label printer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Records whether a tomo scan was started/completed for the sample
currently on the stage (a new _MeasuredSampleLog, redis-backed via
BEC global vars, keyed by the packed name|owner identity so it
survives ftransfer_get_sample/put_sample moves between slots). When a
tray slot's occupant is cleared or replaced via
ftransfer_modify_storage() (CLI) or the sample-storage widget's
Clear/Change-name actions (GUI), and that sample was measured, offer
to print an account/date/samplename label via PTouchLabelPrinter.
Also surfaces measured status in ftransfer_show_all() and the
widget's slot cells, including the stage line.
GUI failures retry via QMessageBox Retry/Ignore (a modal dialog runs
its own Qt event loop, so this can't hang the process the way the
CLI's input()-based ensure_ready() would) instead of losing the label
on the first failed attempt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fills the named text objects of a pre-transferred P-touch Template and
triggers a print over raw TCP:9100, validated end-to-end against the
real printer at BRN94DDF8AAB8EC.psi.ch. Shared OMNY_shared utility, not
an ophyd device, per docs/developer/ptouch_label_printer_plan.md.
Not yet wired into flomni.py -- that's the next step.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fosa_in() moved fosax/fosay/fosaz straight to their configured "in"
positions with no collision check. ffzp_info() already computed OSA-to-pin
clearance but never gated the move on it. Add a shared
_osa_remaining_space() helper (also used to deduplicate ffzp_info()'s own
calculation) and raise FlomniOpticsError in fosa_in() when the computed
clearance at the target position is <= 0.
Root cause of the specific collision seen: stage init's _align_setup()
hardcoded foptz to 23 instead of its calibrated "in" value of 17
(ptycho_flomni.yaml), 6 mm off from what fosaz's own "in" calibration
assumes. Hardcode foptz to 17 instead, and lock foptz.limits to +-0.1 mm
around it in both set_limits() and _align_setup() so any future move away
from 17 requires deliberately widening the limits first. Also corrects
the stale "in: 23" in ptycho_flomni.yaml's foptz userParameter to 17.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
ftransfer_gripper_move() silently returned on "No" to the stage-out
confirmation, but its return was indistinguishable from success to its
callers (ftransfer_get_sample/ftransfer_put_sample), which then proceeded
to command the physical get/mount sequence on the controller -- moving
the gripper without the stage ever having moved out or the gripper being
positioned at the transfer coordinates. Now raises FlomniError instead,
which propagates out of both callers and stops the transfer before any
controller command is sent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reflects the single-URL omny.psi.ch config (myfritz/omny-test list retired)
and replaces the omny-test.psi.ch-specific .svg note with the known
permission and intermittent-WAF issues on omny.psi.ch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cuts over the webpage status-page mirror, TomoIDManager's sample-counter
registration, and the samples-folder PDF upload to omny.psi.ch exclusively,
replacing v1p0zyg2w9n2k9c1.myfritz.net and omny-test.psi.ch everywhere.
Note: omny.psi.ch currently has known server-side issues (a permission
error on /upload.php, and a WAF blocking /samples/* outright) tracked
separately with PSI admins -- this switch is expected to be broken until
those are fixed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add omny.psi.ch as a third upload target alongside production and the
omny-test.psi.ch trial mirror.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add DataViewer, ported from debye_bec's bec_widgets/widgets/data_viewer,
adapted to this repo's parent()-first __init__ convention required by
bw-generate-cli.
Drop the "Widget" suffix from SampleStorage, SlitControl, and TomoParams
for naming consistency with DataViewer, then further rename for OMNY
namespacing and Designer-list ordering:
- SampleStorage -> OMNY_SampleStorage
- TomoParams -> OMNY_TomoParams
- XRayEye -> OMNY_XRayEye (XRayEye2DControl untouched)
- ConsoleButtonsWidget -> z_ConsoleButtonsWidget (sorts last)
Update the live gui.<area>.new("...") string-based widget lookups in
flomni/LamNI gui_tools.py that construct XRayEye/ConsoleButtonsWidget by
name, so they keep resolving after the rename. Delete now-stale generated
plugin/register/pyproject trios and regenerate everything via
`bw-generate-cli --target csaxs_bec` (client.py, designer_plugins.py, and
fresh per-widget trios under the new snake_case names).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VZHQaRmthXmyfn2pxP3dF9
A job's params dict only has whatever _TOMO_SCAN_PARAM_NAMES held when it
was snapshotted by tomo_queue_add(); a job persisted before a key was
added (e.g. fovx/fovy, tomo_circfov) raised KeyError on the hard p['key']
lookup, making the whole queue un-inspectable instead of just missing a
field.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
self.dev.fsamroy/.osamroy on the scan server is a generic bec_lib
Positioner proxy rebuilt from serialized device info, not the real
ophyd instance. tolerance was only a plain __init__ attribute, so it
was dropped during serialization and flomni_rotation()/rotation()
crashed with AttributeError: 'Positioner' object has no attribute
'tolerance'. Register tolerance in USER_ACCESS so it serializes and
is reachable via RPC like controller already is.
_refresh_busy_banner() toggled the banner's visibility synchronously
on every poll tick and scan-queue push message, so brief blips in the
underlying queue status made it flicker on/off.
Show busy immediately, but hold the banner up for 5s of continuous
idle before clearing it, mirroring the debounce pattern already used
for XRayEye's queue-guarded toggles (_queue_idle_timer).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
find_rotation_center()'s y/n prompts now go through OMNYTools.yesno()
(omny_general_tools.py) instead of a raw input() in x_ray_eye_align.py,
but these tests were still patching input() on the old module -- so
the real input() got called under pytest's captured stdin and raised
"reading from stdin while output is captured".
Patch input() on omny_general_tools instead, where yesno() actually
calls it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Several confirmation prompts hand-rolled their own input() parsing
instead of using the established yesno() helper, including one
weaker variant (no default, no retry, case-sensitive) and a "Close
the shutter now?" prompt duplicated verbatim four times.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
XQ#NEWPAR dispatches asynchronously on controller thread 0 and returns as
soon as it starts, not once it finishes; the dispatch time varies with
controller load. drive_axis_to_limit/find_reference followed it with a
fixed sleep before starting XQ#FES/XQ#FRM (also thread 0), which has
already needed bumping twice (0.1->0.3, 0.3->0.2) and still raced on
lgalil: sending XQ#FES while thread 0 was still busy got a '?' reply
(Galil error 19, thread already running).
Replace the fixed sleep with _wait_for_thread_idle(0), reusing the
existing is_thread_active() primitive (already used the same way in
hard_abort_and_restore_positioning_mode) to poll until thread 0 is
actually free before dispatching the next routine.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
str.strip(prefix) strips individual characters, not a literal prefix, so
replies made entirely of characters in the prefix (e.g. ":CLS0,0" for
losax) collapsed to '' and crashed float('') in describe(). Other replies
could silently truncate to a wrong value instead. Since the prefix is
already validated by _message_starts_with(), removeprefix() is a safe
drop-in fix for all affected parsers in SmaractController.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status webpage's tomo-queue accordion auto-expanded a job's details
panel as soon as its status became "running", overriding whatever
open/closed state the user had selected. Panel state now depends only
on the existing DOM snapshot of user selections.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Ctrl+C during lsamrot/fsamroy/osamroy rotation leaves the cached
user_setpoint at the target even though the axis never finished moving
(stop() halts the motor mid-move). The next scan then wrongly concluded
"no rotation required" from setpoint equality alone, leaving LamNI's
air bearings unclamped (confirmed via galil show all) and the beamline
unable to scan until the setpoint was nudged manually.
lamni_rotation() now also requires
lgalil_is_air_off_and_orchestra_enabled() before skipping, since that
flag only goes true once the Galil #CENROT sequence (air-clamp plus
centering) has fully completed. flomni_rotation()/omny_rotation() use
a readback-vs-tolerance check instead, since those axes have no
air-bearing clamp step.
Along the way, fixed lgalil_is_air_off_and_orchestra_enabled() itself:
it did bool(socket_put_and_receive(...)) directly on the raw Galil
reply string, which is always truthy, so it could never report
anything but True. Confirmed via HW testing in LamNI.
previous_subtomo_number resets to -1 on every tomo_scan() call in the
golden-ratio branches (tomo_type 2/3), including a resume. That makes
the first loop iteration look like a genuine sub-tomogram transition,
firing the 0-deg reference shot before the actual resume angle even
though the rotation isn't passing through 0 deg at that moment.
Mirrors tomo_type 1's existing start_angle-is-None resume guard:
suppress the shot only for the first, possibly-spurious transition
right after a resume, then let later genuine transitions in the same
call fire normally. Same fix applied to both lamni.py and flomni.py,
which share byte-for-byte identical golden-ratio logic here.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Flomni's tomo_scan() warns and prompts if the X-ray eye isn't out and
the FZP/OSA optics aren't in before scanning; LamNI had no equivalent,
letting a tomogram start with the eye still in the beam or the optics
retracted. Ports the same check, wired into the existing
force/interactive gating so it never blocks on input() during
unattended/queued runs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
xrayeye_rotation_center_calibration_isolated/smear_experimental() reproducibly
crashed with "RuntimeError: GUI is not alive" when keep_shutter_open=True, right
after a long blocking device move (interferometer feedback reset, live rotation
sweep). Root cause: bec_widgets' GUI liveness check is a Redis heartbeat with a
10s TTL refreshed from the same Qt event loop that renders live-view frames;
with live view left on continuously, a long blocking move can starve that
heartbeat past its TTL even though the GUI process is still alive. Add
_gui_call_with_retry() and use it at the on_live_view_enabled(True) call sites
that follow these blocking waits, so the transient false negative is retried
instead of crashing the calibration.
Also reorder the sample-name prompt in find_rotation_center() and
find_rotation_center_smear_experimental() to run before the alignment GUI is
shown -- showing the GUI first steals OS focus, forcing the operator to click
back to the terminal to answer the prompt.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The scan already treats fov_circular==0 as "disabled" and defaults to
it, but the GUI spinbox floor of 0.1 made 0 unreachable. Lower the
floor to 0.0 and label the field so the meaning is explicit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The stage_x_rot/fovy and stage_y_rot/fovx comparisons look swapped,
but are correct: alpha bakes in a fixed ~90 deg mechanical offset
between the piezo axes and the angle=0 beam frame that fovx/fovy are
defined in, so stage_x_rot actually tracks the beam-frame y-extent.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Switch xbpm2x/xbpm2y/polrot/poly/scinx in bl_optics_hutch.yaml from the
legacy MCS1 controller to the new MCS2 controller (mcs2-00029056.psi.ch),
re-enabling polrot/poly with corrected axis assignments from the updated
wiring table to avoid colliding with xbpm2x/xbpm2y on the same channel.
- Remove mcs2_config_test.yaml now that MCS2 is live on real stages.
- Give Mcs2Controller.find_reference_mark an unused hold_time parameter so
shared call sites (smaract.py, flomni.py, omny.py, lamni_optics_mixin.py)
that pass MCS1-style (axis, direction, holdTime, autoZero) args work
unchanged against MCS2 devices too.
- Rename smaract_show_all/mcs2_show_all to a common show_all on both
controllers, and have each list describe() from every registered MCS1 and
MCS2 controller instead of only its own type.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfjGm8MQrhCV8VaC9Tf4qx
Adds a new driver for the SmarAct MCS2 controller (MCS2-C-0008, 9 channels),
parallel to the existing MCS1 (SCU) implementation. Uses the MCS2's raw
ASCII/SCPI interface over TCP (port 55551), which differs from MCS1 in units
(picometers), message termination (\r\n), and error handling (a polled error
queue instead of inline echoes).
- csaxs_bec/devices/mcs2: Mcs2Controller/Mcs2Motor and errors
- csaxs_bec/devices/sim/sim_mcs2.py: simulated backend for testing
- tests/tests_devices/test_mcs2.py: unit tests + sim end-to-end test
- device_configs: commented example stage in bl_optics_hutch.yaml, and a
standalone single-axis mcs2_config_test.yaml
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- restore omny_fermat_scan scan_name (was broken by _v4 rename)
- restore missing settling delay after LamNI feedback_disable
- make LamNI drift-correction moves consistently concurrent
- use self.actions.set for flomni rtx/rtz moves
- cap flomni corridor_size (explicit and auto-estimated) at 3um
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>