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>
The k=2..8 alignment steps always rotated via a halfway "approach"
angle before the 45 deg target, to let the operator watch the sample
sweep through in live view. That's only useful when keep_shutter_open
is True; with the shutter closed nobody sees the intermediate move, so
just rotate straight to the target angle in that case.
Also bump the OMNY/FlOMNI post-fshopen() freeze-frame delay in
update_frame() from 0.5s to 1s to match LamNI's, for consistent
behavior between the two plugins.
pygobject ships no PyPI wheels, so pip install always compiles it from
source, which requires the OS gobject-introspection dev package at
install time. As a mandatory dependency this broke plain `pip install
csaxs_bec` on CI and on Read the Docs, neither of which has that
package installed. Move it to the `allied_vision` extra so only hosts
that actually run the camera (which already need Aravis + the OS
package present) opt in via `pip install csaxs_bec[allied_vision]`.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The live_mode_poll_interval_s: 0.02 override (50 Hz) made the GUI
unusably laggy. Remove it so cam_xeye falls back to the class
default of 0.2 s (5 Hz).
This PV belonged to the old EPICS/LabView X-ray-eye acquisition system
that both LamNI and OMNY alignment have already replaced with the
BEC-native cam_xeye (IDSCamera) device and GUI-driven workflow. The PV
no longer has a listener, so the write only produced a "cannot connect"
warning at the start of LamNI's tomo_alignment_scan() (via leye_out())
and OMNY's oeye_out() -- remove both, plus the now-dead epics_put
imports left behind in lamni_optics_mixin.py, omny_optics_mixin.py, and
flomni_optics_mixin.py, and a leftover print() in flomni's
x_ray_eye_align.py that referenced the same PV without ever writing it.
Adds a free-text "fzp_details" userParameter (default "manufacturing
notes here") to the loptx/foptx/ofzpx optics-stage devices for LamNI,
flomni and OMNY, alongside the existing fzp_diameter/
fzp_outermost_zone_width parameters, and prints it in each beamline's
tomography-start PDF report.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
move_and_finish()'s spurious-trip retry (added in 1f85106) couldn't tell a
genuine Galil safety-thread trip apart from a user/scan-initiated stop()
(e.g. Ctrl+C): an aborted move looked like an incomplete move either way,
so the abort got silently undone by resetting the error latches and
re-issuing the original move to the same target.
Track stop() requests on a per-motor threading.Event, cleared at the start
of each move() and checked (twice, since reset_axis_errors() itself takes
a few round-trips) before the retry branch fires, so an aborted move is
now reported as a failure instead of being retried.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Camera.__init__ raised ImportError unconditionally whenever the Aravis
GObject-Introspection library wasn't importable, even with connect=False --
contradicting the module's own "lazy initialization" docstring and
breaking every unit test in CI, since Aravis is a system library (not
pip-installable) that CI never has installed. The test fixture builds a
real AlliedVisionAravisCamera(connect=False) and only mocks camera.cam
afterward, so the raise fired before mocking ever got a chance to run.
Move the guard into on_connect(), mirroring IDSCamera's working pattern
(its equivalent check lives in the object only built by on_connect(),
never at construction time). Construction is now independent of whether
Aravis is installed; on_connect() still raises the same clear error the
moment something actually tries to talk to hardware.
allied_vision_test.yaml was only ever a scratch config for exercising the
camera during bring-up; the same commented allied_vision_cam example
already lives in bl_detectors.yaml, so keep that one and add the matching
commented block to ptycho_omny.yaml instead of a separate file.
Port get_live_fps()/live_mode_poll_interval_s from IDSCamera (added on
main): a rolling deque of the last 10 live-mode push timestamps, exposed
as a measured frame rate rather than assuming the nominal poll interval
is actually achieved -- verified against real hardware, where the
Alvium G1-507m's actual acquisition/readout time brings the achievable
rate to ~3 fps despite the 0.2s (5 Hz) nominal poll interval.
Mirrors the same three unit tests added for IDSCamera's version.
Update the placeholder camera_id in allied_vision_test.yaml/bl_detectors.yaml
to the Aravis device ID discovered on the actual Alvium G1-507m hardware,
and set live_mode back to False (explicit, not deleted) so the camera stays
idle until start_live_mode() is called -- matching every real IDS camera
config, instead of auto-starting its live-mode polling thread as soon as
the device connects.
Pin pygobject<3.52 in pyproject.toml: 3.52+ requires girepository-2.0
(GLib >=2.80/gobject-introspection >=1.80), which this RHEL9 host's system
packages don't provide (only the older girepository-1.0 via
gobject-introspection 1.68) -- unpinned pip install fails at build time.
Document both the camera_id lookup and the pin in the README.
Several tomo_scan()/tomo_alignment_scan()/find_rotation_center() tests
started failing (CI) after recent commits on this branch introduced new
input()-backed confirmation/sample-name prompts, an
alignment_scan_progress GUI proxy, and tomo_queue_execute()'s
interactive=False kwarg -- none of which the existing test fixtures or
assertions were updated for. Mock/bypass the new prompts and GUI calls,
add the missing _alignment_scan_progress_proxy to the bare-object test
fixture, and update two assertions (tomo_queue interactive kwarg,
account-less sample registration) to match the intended new behavior.
simulated_lamni.yaml/simulated_flomni.yaml define their own separate
loptx/foptx (not shared with ptycho_lamni.yaml/ptycho_flomni.yaml) --
missed these when adding fzp_diameter/fzp_outermost_zone_width/
detector_distance earlier, so a simulated-only session wouldn't have
had them. Same values (170 micron, 60 nm, -1).
Downloaded from https://intranet.psi.ch/themes/custom/design/logo.svg
(no gradients/filters/embedded fonts -- confirmed fpdf2 renders it
directly, no PNG conversion needed) and stored as psi_logo.svg in
OMNY_shared. Added TomoQueueMixin._add_psi_footer(), which overrides
the fpdf instance's footer() callback (PDFWriter/BECPDF live in
bec_lib, a separate repo with no public API for this) so the logo
repeats on every page -- unlike the header logo, which is drawn once
inline, a report can span multiple pages (e.g. a long
at_each_angle_hook source dump). Also drops the microseconds from the
existing "BEC, <timestamp>" footer text while reimplementing it, per
request ("ms precision is not needed"). Verified end-to-end with
bec_lib's real PDFWriter: a 300-line hook source forced a 7-page
report, with the logo correctly repeating on every page.
fzp_diameter_um/fzp_zone_width_nm were read inside the same try block
as the focal-distance calculation, which also needs energy_kev. When
the photon energy read fails (as it does in at least one simulated
session -- confirmed live: dev.ccm_energy unavailable), the exception
wiped out the diameter/zone-width display too, even though those come
from config (loptx/foptx userParameter) and were read just fine.
Split into two independent try blocks: diameter/zone-width no longer
depend on a working energy read, only the focal distance itself does.
Adds FZP diameter, outermost zone width, focal distance,
focus-to-sample distance, and sample-to-detector distance to
write_pdf_report(). Diameter/zone-width/detector-distance come from
the userParameter entries just added to loptx/foptx; focal distance
is computed with the same formula lfzp_info()/ffzp_info() already use
(diameter * zone_width / wavelength); focus-to-sample distance reuses
their existing live z-stage-based calculation rather than storing a
separate value. Replaces this exact old SPEC workflow
(_tomo_other_parameters interactively asking for these same numbers
every time) with values read automatically from config + live
hardware.
Also fixes FlOMNI's "Current photon energy: To be implemented"
placeholder (never wired up) -- needed a real value for the new focal
distance calculation anyway, and dev.ccm_energy is the same device
ffzp_info() already reads.
focal_distance was computed in meters (consistent with the rest of the
function, e.g. beam_size's own internal *1000 conversions), but the
table row printed it directly as "X.XX mm" with no conversion -- off
by 1000x (e.g. showing "0.05 mm" instead of "51.34 mm"). FlOMNI's and
OMNY's equivalent ffzp_info()/ofzp_info() already convert correctly;
fix just the display here to match, without touching the
meters-based internal variable beam_size still depends on.
Adds fzp_diameter (170 micron), fzp_outermost_zone_width (60 nm), and
detector_distance (-1, unknown for now) as userParameter entries on
loptx/foptx. LamNI's values match FlOMNI's already-documented active
FZP (per its "#170 micron, 60 nm" comment) -- confirmed with the user
that LamNI currently uses the same optic. Feeds the new PDF report
fields in the next commit.
OMNYTools, PtychoReconstructor, and TomoIDManager were sitting under
the omny plugin's own directory, but are imported by LamNI, FlOMNI,
and cSAXS too -- none of the three classes reference anything
OMNY-specific (device names are always passed in as parameters).
OMNY_shared already exists for exactly this kind of cross-setup code
(tomo_queue_mixin.py, web_common.py, webpage_generator_base.py), so
move it there and update all five import sites. Pure relocation, no
behavior change.
Mirrors LamNI.write_pdf_report()'s same fix: replace the ASCII-art
header with the actual flOMNI.png logo (already correctly resolved
for the scilog attachment, just never used in the PDF itself),
embedded via PDFWriter's underlying fpdf object. Also left-justify
values instead of right-justifying them in a wide fixed field, to
remove the big ragged gap after short labels. Drops a leftover debug
print(logo_file).
Replace the ASCII-art header with the actual LamNI.png logo, embedded
via PDFWriter's underlying fpdf object (bec_lib's PDFWriter has no
public image API). Also fix the label/value formatting: values were
right-justified in a wide fixed field, leaving a big ragged gap after
short labels -- left-justify both instead for a clean, tight
"label: value" layout.
Also fixes a real bug found along the way: the scilog attachment
referenced "LamNI_logo.png", a file that has never existed (the actual
file is LamNI.png) -- the resulting FileNotFoundError was swallowed by
write_pdf_report()'s generic try/except, so the scilog message has
been silently failing to send every time. Now uses the one correct,
shared logo_path for both the PDF and the scilog attachment.
TomoIDManager.register() called wget via subprocess with shell=True,
interpolating sample_name/eaccount/etc. unescaped into the command
string -- broke outright wherever wget isn't installed (as hit while
testing: "wget: command not found", silently falling back to tomo ID
0), and was a latent shell-injection risk. Use requests.get() with a
params dict instead: no external binary dependency, proper URL
encoding, and the same self-signed-cert/SSRF handling already used by
the samples PDF upload. Drops the now-unused OMNYToolsError class and
TMP_FILE/subprocess, which only existed to support the wget call.
Mirrors LamNI.tomo_scan()'s same fix: remove the outer
"bec.active_account != ''" short-circuit so an empty/test account still
goes through add_sample_database() -> TomoIDManager.register() (which
now handles test-host registration itself) instead of hard-coding
tomo_id=0. Also accept an unused interactive kwarg for signature
compatibility with LamNI.tomo_scan(), since tomo_queue_execute() (shared
between both setups) calls tomo_scan(interactive=False) uniformly.
tomo_queue_execute() runs queued scans unattended, potentially for
hours -- tomo_scan()'s fine-alignment prompt would block forever there
with nobody watching. Add interactive: bool = True; when False (queue
use), the check no longer prompts or aborts -- it prints a bold red
warning, waits 10s, and always proceeds.
Also remove the outer "bec.active_account != ''" short-circuit that
hard-coded tomo_id=0 for an empty account without even attempting
registration -- always call add_sample_database() now and let
TomoIDManager.register() (test-host registration, previous commit)
decide the right outcome instead of pre-empting it here.
tomo_id == FALLBACK_TOMO_ID (0) means add_sample_database() never
actually registered a measurement server-side, so there's no reserved
"new.pdf" slot -- uploading anyway would silently claim whatever
number countersaver.txt currently holds, i.e. an unrelated real
measurement's slot. Skip with a clear message instead. Also widen the
truncated response text in the upload warning/log messages (120 -> 400
chars), which had cut off the file+line a real PHP error would show,
making a live parse-error response undiagnosable from the client log
alone (as happened while testing this).