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).
TomoIDManager.register() used to skip OMNY registration entirely for
any non-e-account (e.g. test accounts), always returning the tomo ID
0 fallback. Register against a new TEST_OMNY_URL (omny-test.psi.ch)
instead for those accounts, so testing still gets a real, incrementing
tomo ID matching the counter the samples-folder PDF upload reads from
on that same host -- accepting a mismatched eaccount in that test
database. FALLBACK_TOMO_ID is now reserved for genuine failures
(server unreachable), not merely a non-standard account name.
Nothing previously enforced the intended sequence: rotation-center
calibration -> X-ray-eye alignment -> fine alignment scan -> real
tomogram. Add three soft gates -- xrayeye_alignment_start() (no center
found yet), tomo_alignment_scan() (X-ray-eye fit missing/invalidated,
softened from a hard abort), and tomo_scan() (no fine alignment
loaded) -- each printing a specific warning and prompting to continue,
via a new shared _confirm_sequence_override() helper. Not a hard
block: each method also gets force: bool = False to skip the check
entirely for cases that legitimately don't need it (e.g. a large FOV
that doesn't need fine alignment).
Re-running rotation-center calibration after X-ray-eye alignment (or
fine alignment) was already done left the stale fit/correction in
place, silently calibrated around the old center. Record
lamni_center_found_at and cascade-invalidate via the existing
reset_xray_eye_correction()/reset_correction() whenever a new center
is actually applied, in both find_rotation_center() and
find_rotation_center_smear_experimental().
tomo_scan()'s new-scan branch called write_pdf_report() once inside
the "real e-account" if-branch and again unconditionally right after,
so every scan start under a real account wrote the PDF, sent the
scilog entry, and (as of the previous commit) uploaded to the samples
folder twice. Keep only the unconditional call, matching LamNI's
equivalent structure.
Replaces the old, already-broken upload_last_pon.sh shell-out (and
assorted other dead/abandoned upload attempts left commented out in
flomni.py) with a direct HTTP upload: POSTs the just-written PDF,
base64-encoded, to omny-test.psi.ch/samples/upload.php with
filename="new.pdf" so the server assigns the number itself from its
own counter file -- the same counter newmeasurement.php
(TomoIDManager.register(), called via add_sample_database() just
before write_pdf_report()) already incremented and returned as
self.tomo_id. Runs in a background thread so a slow/unreachable host
never delays the calling tomo_scan(), and cross-checks the response
against the expected filename to flag a possible race with a
concurrent registration.
Added to TomoQueueMixin (OMNY_shared) rather than duplicated in both
LamNI and Flomni, following add_sample_database()'s existing pattern
of shared, setup-agnostic tomo-id/database logic.
tomo_alignment_scan() had no GUI feedback at all -- just console
prints -- unlike the real tomogram, which already drives a
RingProgressBar via lamnigui_show_progress(). Add the same kind of
single-ring display for the 12-angle alignment loop
(lamnigui_show_alignment_progress()/_lamnigui_update_alignment_progress()),
backed by its own alignment_scan_progress global var rather than
tomo_progress, so anything watching tomo_progress for the real
tomogram (heartbeat/idle-time tracking) never sees alignment-scan
writes mixed in.
lamni_compute_additional_correction_xeye_mu() always printed its
result. write_alignment_scan_numbers() called it once per angle just
to grab the x-offset for the log file, which dumped all 12 corrections
up front -- immediately followed by the exact same values reprinted
one-by-one as tomo_alignment_scan() actually runs each angle. Add a
verbose flag (default True) and pass verbose=False from the lookahead
call.
The estimated Fermat-scan point count was only ever flagged with an
orange label -- Submit and Add-to-queue would both happily accept a
configuration that's guaranteed to abort when actually run. Enforce
the same minimum in _validate(), shared by both actions, so it's now a
hard block instead of a cosmetic warning.
The shared XRayEye widget already supports a sample_name field, but
LamNI's x_ray_eye_align.py never set it (unlike FlOMNI's align(),
which does) -- so it was always blank. Add _sync_sample_name(): pushes
lamni.sample_name into the GUI everywhere align()/find_rotation_center*
run, and additionally prompts for it (Enter keeps the current value,
same as tomo_parameters()) at the rotation-center steps, since those
are often the first alignment action for a new sample.
IDSCamera's live-mode loop had a hardcoded 0.2s sleep (nominal 5 Hz
cap) between frame grabs. Expose it as live_mode_poll_interval_s
(default 0.2, unchanged for existing cameras) and lower it for LamNI's
cam_xeye (0.02s) so exposure/acquisition time, not this artificial
sleep, becomes the real limit on smear-sweep capture rate.
RtLamniReadbackSignal._socket_get() printed the same raw socket
response twice in a row with nothing modifying it in between --
leftover debug output, not by design.
feedback_enable_with_reset() silently rotates lsamrot back to 0 as part
of the interferometer reset; when the stage is far from 0 this can take
a while with no feedback in the client session. A plain print() there
only reaches the device-server console, not the client, so use
connector.send_client_info() instead.
- 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.
Both prompts appear while lsamrot happens to be at 0 deg at that point in
the procedure, but the click itself isn't specific to 0 deg -- just the
current position.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
loptics_out() already drives lsamrot to 0 via
RtLamniController.feedback_enable_with_reset() when rtx is enabled, so the
explicit tomo_rotate(0) that followed in find_rotation_center() and
find_rotation_center_smear_experimental() was reissuing a move that had
mostly already happened -- align() already skips this same step (see its
commented-out disable/rotate/re-enable at the equivalent spot).
_tomo_rotate_if_needed() keeps the still-needed pieces: it still corrects
small residual drift from feedback briefly running before
_disable_rt_feedback(), and still performs the move when rtx is disabled
(loptics_out() then never touches lsamrot at all).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Default was 0.5 (LamniGalilMotor's built-in fallback); this is the
tolerance used both by the normal move-success check and by the new
auto-retry logic. Applied to both the real deployment and the
simulated deployment config.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A safety thread on the LAMNI Galil controller supervises air pressure and
capacitive-sensor feedback for the rotation stage; a spurious trip drops
allaxref and sets allaxrer/caperr, failing every subsequent lsamrot move
until manually cleared with allaxrer=1/caperr=0/allaxref=1.
LamniGalilMotor.move() now resets those latches and retries once, scoped
to lsamrot only via LamniGalilController.reset_axis_errors() -- other
axes on the shared controller keep today's behavior unchanged, and a
persisting fault still surfaces as a failure after the single retry.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
update_frame() (reached via lamni.leye_in() -> xrayeye_update_frame())
used self.gui (lamni.xeyegui) directly, but that's None until the widget
has been opened by lamnigui_show_xeyealign() -- which align() and
find_rotation_center() call first, but update_frame() never did. If it's
the first X-ray-eye call of a session, this raised
AttributeError: 'NoneType' object has no attribute 'on_live_view_enabled'.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Neither rate was previously measured anywhere, making it hard to tell a
real acquisition bottleneck from a display/overhead one.
- IDSCamera.get_live_fps() (new USER_ACCESS entry): rolling-buffer
measurement of the live-mode push rate in _live_mode_loop(), readable as
dev.cam_xeye.get_live_fps(). SimIDSCamera inherits it unchanged, so it
works against the simulated deployment too.
- XrayEyeAlign._smear_sweep() now tracks timestamps of the first and most
recent distinct frame it captures, storing the computed rate as
self.last_smear_fps and printing it in the sweep-done summary line.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Three follow-ups from real-hardware testing of the smear aid:
1. Switching the "Smear preview channel" toggle off then back on showed
the live camera feed, not the previous composite. Root cause:
Image.image()'s connect_slot subscription only delivers stream
entries published *after* connecting (bec_lib's default
register(from_start=False, newest_only=False) bookmarks the
then-current last entry and only forwards newer ones) -- so nothing
re-renders on reconnect until the next sweep pushes a new frame.
Added a permanent, independent subscription (newest_only=True) that
keeps the last smear_preview payload cached regardless of which
channel is currently displayed; set_live_view_signal() now force-
renders that cache via main_image.set_data() when switching back to
"smear_preview", instead of waiting for a push that may never come.
Naturally "resets" once a new sweep starts pushing fresh composites.
2. The new switch row wasn't column-aligned with the existing
shutter/camera-running row (each used an independent QHBoxLayout
sized by its own labels' widths). Merged both rows into one
QGridLayout with right-aligned label columns, so all four toggles
line up regardless of label text length. Shutter now pairs with
Smear integrating, Camera running with Smear preview channel.
3. find_rotation_center_smear_experimental() now archives each run to
a timestamped HDF5 file (~/data/raw/logs/xrayeye_smear_calibration/),
mirroring align()'s existing _save_alignment_data() pattern: the FZP
reference, one composite image + one clicked centre per iteration,
and the final result -- collecting data towards eventually
automating center detection from the composite offline.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A Ctrl-C interrupting an in-flight RPC call (e.g. mid
set_live_view_signal) could leave the XRayEye widget stuck showing the
smear-preview channel afterward -- and since lamnigui_show_xeyealign()
reuses the existing xeyegui widget across calls rather than recreating
it, that stale state would silently carry into the *next* alignment
run too (including production align()/find_rotation_center(), since
on_live_view_enabled() now respects whichever channel is selected).
Fix: lamnigui_show_xeyealign() unconditionally resets both
set_live_view_signal("image") and set_smear_active(False) every time
it's called -- both are cheap no-ops if already in the default state,
and this is the single shared entry point for every alignment
procedure, so one fix covers all of them.
Also add two GUI-facing controls to XRayEye, in a new row below the
shutter/camera-running toggles:
- "Smear preview channel" -- an interactive toggle mirroring
set_live_view_signal(), so the operator has a GUI-only recovery path
independent of the ipython client (e.g. if it's hung/disconnected).
- "Smear integrating" -- a read-only status indicator (disabled
ToggleSwitch) reflecting whether _smear_sweep() is currently
accumulating a composite, driven entirely by the automation.
set_live_view_signal() keeps the interactive toggle's visual state in
sync regardless of who changed the channel (automation or operator) --
single source of truth. _smear_sweep() brackets accumulation with
set_smear_active(True)/(False), turning it off as soon as the sweep
ends (independent of the channel switch, which stays on the composite
until the operator's click is collected). Both new XRayEye methods
required regenerating csaxs_bec/bec_widgets/widgets/client.py via
bw-generate-cli --target csaxs_bec.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
csaxs_bec/bec_widgets/widgets/client.py is a checked-in generated file
(via `bw-generate-cli --target csaxs_bec`) containing the client-side
RPC stub for each plugin widget's USER_ACCESS surface -- BECGuiClient
resolves widget classes by name against this exact file, not via a
dynamic import of the real widget class, so it must be regenerated
whenever a plugin widget's USER_ACCESS changes.
Picks up XRayEye.set_live_view_signal (added for the LamNI smear
calibration aid) plus other plugin widgets that had drifted out of
sync already (SAXSWidget.move_to_selected_row, a SampleStorageWidget/
TomoParamsWidget docstring update) -- unrelated to this session's work
but caught by the same regeneration pass.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The simulated speckle pattern rotated exactly around image center --
the same point the FZP crosshair sits at -- so the calibration was
trivially already correct in sim, never exercising the actual
find-and-correct-a-miscalibration path.
Add an opt-in axis_offset_range_px/axis_offset_seed: when set, the
"true" rotation axis the speckle field orbits is offset from image
center by a random amount (fresh each device construction unless a
seed is given, so each sim session gets a new miscalibration to find).
Wired into LamNI's cam_xeye sim config (sim_axis_offset_range_px: 40,
no seed -- random each session); default (0.0) preserves prior
behavior exactly for any other SimIDSCamera user.
Also drop the synthetic crosshair lines baked into
make_speckle_test_pattern's pixel data -- a real camera image has no
such marking; the GUI already draws its own crosshair overlay
(TargetCrosshair) on top of the live image.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "camera running" toggle (IDSCamera.live_mode_enabled) was flapping
on/off during the smear sweep, and on real hardware that's genuinely
slow (starting/stopping the live acquisition thread has real driver
overhead, unlike the simulator). Root cause: composite pushes shared
the same device_preview channel the live thread continuously publishes
to, so live_mode_enabled had to be toggled off around every push to
keep the composite from being instantly overwritten.
Give the composite its own channel instead:
- IDSCamera gains a smear_preview PreviewSignal (no rotation_90/
transpose configured -- composite data is already display-oriented)
and push_smear_preview(), mirroring push_preview_image() but fully
decoupled from the live channel.
- XRayEye gains set_live_view_signal() to switch which camera signal
the live Image view displays, defaulting to the normal channel for
every fresh GUI instance; on_live_view_enabled() now uses whichever
signal is currently selected instead of a hardcoded constant.
- _smear_sweep() switches the GUI to "smear_preview" for the sweep and
pushes composite updates there -- live_mode_enabled is never touched
at all (just ensured on at the start, exactly like _live_sweep()).
The GUI is deliberately left on the composite after a successful
sweep until the caller has collected the operator's click, then
switched back to "image"; on KeyboardInterrupt it's switched back
immediately.
This removes the resume_live/hold_display_s toggle-avoidance mechanism
added in def9bf4, which is no longer needed with a dedicated channel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Real-HW-adjacent (sim) testing surfaced three issues with the smear
calibration aid:
1. The composite reverted to the plain rotating live view right before
the click prompt. find_rotation_center_smear_experimental() called
update_frame() after _smear_sweep(), which grabs a fresh raw live
frame -- overwriting the composite it just built. Also,
_push_smear_composite always resumed live mode, so even without that
bug the composite would only stay visible for hold_display_s.
Fixed: _push_smear_composite gained resume_live=False for the final
push; _smear_sweep now takes keep_shutter_open and closes the
shutter itself at the end, so the composite (not a fresh grab) stays
frozen on screen for the click. The stray update_frame() call is
removed.
2. The composite appeared rotated relative to the live view.
get_last_image() frames already have num_rotation_90/transpose
applied (PreviewSignal.put() applies it to whatever it stores), so
composites built from those frames got the transform applied AGAIN
by push_preview_image()'s own self.image.put() call. Fixed:
push_preview_image() now undoes the transform first (reverse order,
since transpose is applied last forward) so the net effect is a
single transform, matching a live frame.
3. Feature request: the sim camera's rotation-coupled pattern was a
single eccentric gaussian blob -- a better analog for the isolated/
point-particle case than the extended/textured case this feature
actually targets. Added generate_speckle_grains/
make_speckle_test_pattern (sim_cameras.py): a fixed, seeded field of
small gaussian "grains" at a range of radii, rotating rigidly
together as the live angle changes -- producing several concentric
arcs in the composite instead of one, much closer to a real textured
sample. New sim_speckle_count/sim_speckle_seed config, defaults
requiring no yaml changes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The simulated cam_xeye (SimIDSCamera/_SimIDSBackend) served a single
static Gaussian blob, cached at construction -- rotating lsamrot in a
simulated session produced no visible motion, so the smear/composite
rotation-center calibration aid had nothing to smear into an arc.
Add an opt-in sim_rotation_coupling config (mirrors the existing
sim_coarse_coupling idiom in sim_lamni.py's measured_positions()): when
set, _SimIDSBackend regenerates a fresh frame on every get_image_data()
call with an eccentric blob (make_rotating_test_pattern) tracking the
coupled axis' live simulated Galil position instead of serving the
cached static frame. Wired into LamNI's cam_xeye sim config only
(host/port/axis matching lsamrot's own sim config); flomni's cam_xeye
and any other SimIDSCamera user are unaffected since the new params
default to None (byte-identical output to before, confirmed by test).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
dev.cam_xeye.image.put(composite) failed on real hardware --
PreviewSignal is a BECMessageSignal subclass, and BEC hardcodes
rpc_access=False for that signal_info, so the client-side device proxy
never exposes `image` as an attribute at all (confirmed: this is a
structural BEC constraint, not specific to this device -- no existing
RPC surface, device or widget, offers a raw-array setter either).
Add IDSCamera.push_preview_image(data), a small device-server-side
method (alongside the existing get_last_image()) that does
self.image.put(data) from within the device-server process, where
PreviewSignal is a normal ophyd attribute. _push_smear_composite now
calls dev.cam_xeye.push_preview_image(composite) instead of writing to
the signal directly.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds an EXPERIMENTAL, operator-only alternative to the extended-sample
rotation-center click: instead of judging the center from a single
instant of live rotation, accumulate a max-projection composite while
lsamrot rotates continuously through a (tunable, default 360 deg)
sweep. Off-axis features smear into circular arcs; the common center
of curvature is the rotation axis, an easier target to click than one
live frame. No automatic circle fitting -- purely a visual aid feeding
the same _collect_click mechanism the production feature already uses.
Rotation is issued non-blocking via scans.mv() (new local wrapper,
sibling of this file's existing blocking umv()) and frames are grabbed
via get_last_image() in a single-threaded loop polling ScanReport.status
-- the same thing ScanReport.wait() does internally -- so no background
thread or device-server change is needed to grab frames while the motor
is actively moving. The composite is pushed to the GUI through the
camera's existing PreviewSignal (dev.cam_xeye.image), duty-cycling
live_mode_enabled off/on around each push so the background 5 Hz live
thread can't immediately overwrite it.
New entry point: lamni.xrayeye_rotation_center_calibration_smear_experimental().
Purely additive -- does not modify find_rotation_center()/_live_sweep()
or the two production entry points.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>