fix(lamni): correct X-ray eye alignment DAP forwarding, offset reference point, and pixel calibration
- Enable DAP fit parameter forwarding (self.gui.set_dap_params_forwarding), never called before -- without it the dap_params_update signal was never connected, so no fit data could ever reach fit_params_x/y regardless of how long anything waited. Disabled again once the fit is read (or on abort, via the existing KeyboardInterrupt cleanup in lamni.py). - Replace the fixed 5s sleep before reading the DAP fit with _wait_for_dap_fit(), which polls fit_params_x/y for the real converged shape (up to 30s) instead of guessing a duration. - Fix write_output()'s offset reference: was using one of the 8 angle points as its own zero-reference (first via an off-by-one at k=1, then at k=2), which forces that point's offset to exactly 0 by construction and biases the fit. Now references alignment_values[0] (the FZP centre, the only angle-independent point in the procedure), matching both FlOMNI's own reference point and the original SPEC macro (l_xrayeyealign.mac) exactly, including the asymmetric x/y sign convention. - pixel_calibration is now a property reading the cam_xeye device's pixel_calibration user parameter, matching FlOMNI's camera-sourced calibration, with the old hardcoded constant kept only as a fallback (PIXEL_CALIBRATION_DEFAULT) -- by design, to match FlOMNI's own pattern. - lamni_optics_mixin.py: lfzp_in() and loptics_out() now print before/after re-establishing interferometer feedback, previously silent. - device_configs: add the missing userParameter.pixel_calibration to cam_xeye in ptycho_lamni.yaml and simulated_omny/simulated_lamni.yaml (present on the FlOMNI side already; this is what the pixel_calibration property above actually reads at runtime).
This commit is contained in:
@@ -208,4 +208,4 @@ class DataDrivenLamNI(LamNI):
|
||||
|
||||
shapes = [data.shape for data in self.tomo_data.values()]
|
||||
if len(set(shapes)) > 1:
|
||||
raise ValueError(f"Tomo data file has entries of inconsistent lengths: {shapes}.")
|
||||
raise ValueError(f"Tomo data file has entries of inconsistent lengths: {shapes}.")
|
||||
|
||||
@@ -250,6 +250,10 @@ class LamNI(LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools):
|
||||
aligner.gui.hide_crosshair()
|
||||
except Exception as gui_exc: # pylint: disable=broad-except
|
||||
logger.warning(f"Failed to hide XRayEye alignment crosshair: {gui_exc}")
|
||||
try:
|
||||
aligner.gui.set_dap_params_forwarding(False)
|
||||
except Exception as gui_exc: # pylint: disable=broad-except
|
||||
logger.warning(f"Failed to disable XRayEye DAP parameter forwarding: {gui_exc}")
|
||||
raise exc
|
||||
|
||||
# ── Reset manual shifts if needed ─────────────────────────────
|
||||
@@ -1219,4 +1223,4 @@ class LamNI(LamNIAlignmentMixin, LamNIOpticsMixin, LamniGuiTools):
|
||||
print(f"{angle},{voltage1},{voltage2}")
|
||||
time.sleep(.3)
|
||||
|
||||
print("Finished")
|
||||
print("Finished")
|
||||
|
||||
@@ -209,11 +209,29 @@ class LamNIAlignmentMixin:
|
||||
tomo_fit_xray_eye = np.zeros((2, 3))
|
||||
|
||||
params_x = _dev.omny_xray_gui.fit_params_x.get()
|
||||
params_y = _dev.omny_xray_gui.fit_params_y.get()
|
||||
for label, params in (("x", params_x), ("y", params_y)):
|
||||
if (
|
||||
not isinstance(params, dict)
|
||||
or not {"SineModel_0_amplitude", "SineModel_0_shift", "LinearModel_1_intercept"}
|
||||
<= params.keys()
|
||||
):
|
||||
raise RuntimeError(
|
||||
f"No converged DAP sinusoidal fit available for the {label} axis "
|
||||
f"(omny_xray_gui.fit_params_{label} did not return the expected "
|
||||
"named parameters -- got "
|
||||
f"{type(params).__name__ if not isinstance(params, dict) else 'a dict missing keys'}). "
|
||||
"This means the fit hasn't converged / DAP forwarding hasn't produced "
|
||||
"a result yet, not that the correction data is missing. Wait for the "
|
||||
"fit to converge in the XRayEye GUI's Fit tab, or use "
|
||||
"read_xray_eye_correction() to reload from the archived text files "
|
||||
"instead."
|
||||
)
|
||||
|
||||
tomo_fit_xray_eye[0][0] = params_x["SineModel_0_amplitude"]
|
||||
tomo_fit_xray_eye[0][1] = params_x["SineModel_0_shift"]
|
||||
tomo_fit_xray_eye[0][2] = params_x["LinearModel_1_intercept"]
|
||||
|
||||
params_y = _dev.omny_xray_gui.fit_params_y.get()
|
||||
tomo_fit_xray_eye[1][0] = params_y["SineModel_0_amplitude"]
|
||||
tomo_fit_xray_eye[1][1] = params_y["SineModel_0_shift"]
|
||||
tomo_fit_xray_eye[1][2] = params_y["LinearModel_1_intercept"]
|
||||
@@ -348,4 +366,4 @@ class LamNIAlignmentMixin:
|
||||
shift_y = corr_pos_y[-1]
|
||||
|
||||
print(f"Additional correction {label}: x={shift_x}, y={shift_y}")
|
||||
return (shift_x, shift_y)
|
||||
return (shift_x, shift_y)
|
||||
|
||||
@@ -242,7 +242,10 @@ class LamNIOpticsMixin:
|
||||
self._lfzp_in()
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
print("Re-establishing interferometer feedback...")
|
||||
_t0 = time.time()
|
||||
dev.rtx.controller.feedback_enable_with_reset()
|
||||
print(f"Interferometer feedback re-established ({time.time() - _t0:.1f} s).")
|
||||
|
||||
def loptics_in(self):
|
||||
"""Move in the LamNI optics (FZP + OSA)."""
|
||||
@@ -261,7 +264,10 @@ class LamNIOpticsMixin:
|
||||
|
||||
if "rtx" in dev and dev.rtx.enabled:
|
||||
time.sleep(1)
|
||||
print("Re-establishing interferometer feedback...")
|
||||
_t0 = time.time()
|
||||
dev.rtx.controller.feedback_enable_with_reset()
|
||||
print(f"Interferometer feedback re-established ({time.time() - _t0:.1f} s).")
|
||||
|
||||
def lcs_in(self):
|
||||
pass
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
num_elements = 8
|
||||
corr_pos_x[0] = 0.5
|
||||
corr_pos_y[0] = -0.3
|
||||
corr_angle[0] = 0.0
|
||||
corr_pos_x[1] = 0.8
|
||||
corr_pos_y[1] = 0.1
|
||||
corr_angle[1] = 45.0
|
||||
corr_pos_x[2] = 0.6
|
||||
corr_pos_y[2] = 0.9
|
||||
corr_angle[2] = 90.0
|
||||
corr_pos_x[3] = -0.2
|
||||
corr_pos_y[3] = 1.1
|
||||
corr_angle[3] = 135.0
|
||||
corr_pos_x[4] = -0.7
|
||||
corr_pos_y[4] = 0.4
|
||||
corr_angle[4] = 180.0
|
||||
corr_pos_x[5] = -0.9
|
||||
corr_pos_y[5] = -0.5
|
||||
corr_angle[5] = 225.0
|
||||
corr_pos_x[6] = -0.3
|
||||
corr_pos_y[6] = -1.0
|
||||
corr_angle[6] = 270.0
|
||||
corr_pos_x[7] = 0.4
|
||||
corr_pos_y[7] = -0.8
|
||||
corr_angle[7] = 315.0
|
||||
@@ -1,37 +0,0 @@
|
||||
1. Things to test
|
||||
Progress persistence
|
||||
|
||||
Start tomo_scan(), let a few projections run, restart the BEC client mid-scan, confirm client.get_global_var("tomo_progress") still shows heartbeat/tomo_start_time from before the restart
|
||||
After ~10 projections, confirm CLI output shows a real ETA (not "N/A") — the threshold is projection > 9
|
||||
Confirm the GUI progress display (lamnigui_show_progress()) shows a real start time / ETA, not "N/A"
|
||||
Force a gap between projections (pause, or simulate an interlock) and confirm the "Detected a X gap... excluding Y from ETA" message appears and accumulated_idle_time grows
|
||||
Call lamni.tomo_progress_reset(), confirm it clears and prints confirmation
|
||||
|
||||
Correction persistence
|
||||
|
||||
lamni.read_additional_correction(<file>), restart the client, confirm lamni.corr_pos_x/corr_pos_y/corr_angle (+ _2) still hold the loaded values
|
||||
lamni.reset_correction() / reset_correction_2() — confirm they clear to [] and that clears persist across a restart too
|
||||
Confirm lamni.compute_additional_correction(angle) still returns sensible values after a restart
|
||||
|
||||
X-ray eye alignment (full run)
|
||||
|
||||
Run lamni.xrayeye_alignment_start() end-to-end
|
||||
Crosshair appears at the FZP centre after step 0, stays put through all angle steps, disappears at completion
|
||||
Ctrl-C mid-alignment — confirm the crosshair still disappears (abort path)
|
||||
Confirm dev.fsh physically opens/closes correctly (not a leftover omnyfsh reference anywhere)
|
||||
Open the resulting xrayeye_alignmentvalues_<timestamp>.h5: check alignment_values (9,2), alignment_images has real frames, roi_pixel_data has one row per submit, alignment_fit is (3,8)
|
||||
Confirm the old plain-text file is still written unchanged alongside it
|
||||
keep_shutter_open=True path still prompts and closes correctly
|
||||
Watch specifically for whether click positions land where expected (the un-verified /2 pixel-calibration question)
|
||||
|
||||
Burst mode
|
||||
|
||||
Set lamni.frames_per_trigger = N (N>1), run one projection, confirm the detector actually acquires N frames per point
|
||||
Confirm tomo_parameters() shows and lets you edit it
|
||||
Confirm 0, negative, non-int, or bool raise ValueError
|
||||
Confirm leaving it unset still behaves as 1 (no behavior change for existing scans)
|
||||
|
||||
Regression
|
||||
|
||||
extra_tomo.py: exercise MagLamNI.rotate_slowly() — confirm no more AttributeError on self.align
|
||||
One full short tomo_scan() end-to-end, to confirm none of the above broke the main scan path
|
||||
@@ -43,8 +43,13 @@ class XrayEyeAlign:
|
||||
directions.
|
||||
"""
|
||||
|
||||
# Pixel calibration: multiply pixel coordinate by this to get mm
|
||||
PIXEL_CALIBRATION = 0.2 / 218 # mm/pixel (0.2 mm FOV, 218 px)
|
||||
# Pixel calibration in mm/pixel (multiply pixel values to get mm).
|
||||
# The live value is read from the cam_xeye device's ``pixel_calibration``
|
||||
# user parameter (see the ``pixel_calibration`` property, matching the
|
||||
# FlOMNI side); this constant is only the fallback used when that
|
||||
# device/parameter is unavailable.
|
||||
PIXEL_CALIBRATION_DEFAULT = 0.2 / 218 # mm/pixel (0.2 mm FOV, 218 px)
|
||||
PIXEL_CALIBRATION_USER_PARAM = "pixel_calibration"
|
||||
|
||||
def __init__(self, client, lamni: LamNI) -> None:
|
||||
self.client = client
|
||||
@@ -64,10 +69,14 @@ class XrayEyeAlign:
|
||||
# fresh alignment run should do -- see the start of align() below.
|
||||
# alignment_values[k] = [x_mm, y_mm]
|
||||
# k=0 : FZP centre
|
||||
# k=1 : sample at 0° (reference; shift_xy computed from k=0 vs k=1)
|
||||
# k=2 : sample at 45°
|
||||
# k=1 : sample at 0°, pre-shift (used only to compute shift_xy,
|
||||
# the base shift to scan centre -- NOT one of the 8
|
||||
# angle-fit points used in write_output())
|
||||
# k=2 : sample at 0°, post-shift/at scan centre (first real
|
||||
# angle-fit point; reference for all offsets below)
|
||||
# k=3 : sample at 45°
|
||||
# ...
|
||||
# k=8 : sample at 315°
|
||||
# k=9 : sample at 315°
|
||||
self.alignment_values: dict[int, list[float]] = {}
|
||||
self.alignment_images = []
|
||||
|
||||
@@ -80,6 +89,23 @@ class XrayEyeAlign:
|
||||
"""Return the live XRayEye RPC handle stored by LamniGuiTools."""
|
||||
return self.lamni.xeyegui
|
||||
|
||||
@property
|
||||
def pixel_calibration(self) -> float:
|
||||
"""Pixel calibration in mm/pixel.
|
||||
|
||||
Reads the ``pixel_calibration`` user parameter from the cam_xeye
|
||||
device; falls back to ``PIXEL_CALIBRATION_DEFAULT`` if the device or
|
||||
parameter is unavailable. Matches the FlOMNI side, which moved from a
|
||||
hardcoded constant to this camera-sourced value.
|
||||
"""
|
||||
try:
|
||||
mm_per_pixel = dev.cam_xeye.user_parameter.get(self.PIXEL_CALIBRATION_USER_PARAM)
|
||||
except Exception:
|
||||
mm_per_pixel = None
|
||||
if mm_per_pixel is None:
|
||||
return self.PIXEL_CALIBRATION_DEFAULT
|
||||
return float(mm_per_pixel)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ------------------------------------------------------------------
|
||||
@@ -118,7 +144,7 @@ class XrayEyeAlign:
|
||||
dev.cam_xeye.live_mode_enabled.put(True)
|
||||
self.gui.on_live_view_enabled(True)
|
||||
dev.fsh.fshopen()
|
||||
time.sleep(0.5)
|
||||
time.sleep(1)
|
||||
# store the image: the relevant frame for any submit that follows,
|
||||
# since the shutter is closed again (unless keep_shutter_open) by
|
||||
# the time the user actually clicks submit
|
||||
@@ -150,6 +176,37 @@ class XrayEyeAlign:
|
||||
# Main alignment procedure
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _wait_for_dap_fit(self, timeout: float = 30.0, poll_interval: float = 1.0) -> bool:
|
||||
"""Poll ``omny_xray_gui.fit_params_x``/``fit_params_y`` until both report
|
||||
a converged DAP fit, or *timeout* seconds have passed.
|
||||
|
||||
Returns True if a converged fit was seen, False on timeout. Either way,
|
||||
the caller (read_xray_eye_correction_from_gui) does its own check and
|
||||
raises a clear error if the fit still isn't ready -- this just avoids
|
||||
giving up after an arbitrary fixed sleep when the DAP service is simply
|
||||
running a bit slower than usual.
|
||||
"""
|
||||
required_keys = {"SineModel_0_amplitude", "SineModel_0_shift", "LinearModel_1_intercept"}
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
params_x = dev.omny_xray_gui.fit_params_x.get()
|
||||
params_y = dev.omny_xray_gui.fit_params_y.get()
|
||||
if (
|
||||
isinstance(params_x, dict)
|
||||
and required_keys <= params_x.keys()
|
||||
and isinstance(params_y, dict)
|
||||
and required_keys <= params_y.keys()
|
||||
):
|
||||
print(f"DAP fit converged after {time.time() - t0:.1f} s.")
|
||||
return True
|
||||
time.sleep(poll_interval)
|
||||
print(
|
||||
f"DAP fit did not report converged parameters within {timeout:.0f} s -- "
|
||||
"proceeding anyway; the next step will raise a clear error if it's "
|
||||
"still not ready."
|
||||
)
|
||||
return False
|
||||
|
||||
def align(self, keep_shutter_open: bool = False):
|
||||
"""Run the full LamNI X-ray eye alignment.
|
||||
|
||||
@@ -177,6 +234,7 @@ class XrayEyeAlign:
|
||||
then load fit parameters into the global variable store.
|
||||
"""
|
||||
self.lamni.lamnigui_show_xeyealign()
|
||||
self.gui.set_dap_params_forwarding(True)
|
||||
self.send_message("Getting things ready. Please wait...")
|
||||
|
||||
# Actual start of a fresh alignment run -- see the comment in
|
||||
@@ -209,14 +267,8 @@ class XrayEyeAlign:
|
||||
k = 0
|
||||
while True:
|
||||
if dev.omny_xray_gui.submit.get() == 1:
|
||||
val_x = (
|
||||
getattr(dev.omny_xray_gui, f"xval_x_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
val_y = (
|
||||
getattr(dev.omny_xray_gui, f"yval_y_{k}").get()
|
||||
* self.PIXEL_CALIBRATION
|
||||
)
|
||||
val_x = getattr(dev.omny_xray_gui, f"xval_x_{k}").get() * self.pixel_calibration
|
||||
val_y = getattr(dev.omny_xray_gui, f"yval_y_{k}").get() * self.pixel_calibration
|
||||
self.alignment_values[k] = [val_x, val_y]
|
||||
print(
|
||||
f"Clicked position {k}: "
|
||||
@@ -382,8 +434,8 @@ class XrayEyeAlign:
|
||||
# ------------------------------------------------------------------
|
||||
self.write_output()
|
||||
|
||||
fovx = self._xray_fov_xy[0] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.PIXEL_CALIBRATION * 1000 / 2
|
||||
fovx = self._xray_fov_xy[0] * self.pixel_calibration * 1000 / 2
|
||||
fovy = self._xray_fov_xy[1] * self.pixel_calibration * 1000 / 2
|
||||
print(
|
||||
f"Largest FOV from X-ray eye alignment:\n"
|
||||
f" fovx = {fovx:.0f} um, fovy = {fovy:.0f} um"
|
||||
@@ -394,14 +446,20 @@ class XrayEyeAlign:
|
||||
)
|
||||
self.client.set_global_var("tomo_fov_offset", self.shift_xy)
|
||||
|
||||
# Switch GUI to fit tab and wait for DAP to finish fitting
|
||||
# Switch GUI to fit tab and wait for DAP to finish fitting.
|
||||
# A fixed sleep isn't reliable here -- how long the DAP service
|
||||
# takes to fit and forward a result varies -- so poll the actual
|
||||
# signal state instead of guessing a duration.
|
||||
self.lamni.lamnigui_show_xeyealign_fittab()
|
||||
print("Waiting 5 s for DAP sinusoidal fit to converge...")
|
||||
time.sleep(5)
|
||||
print("Waiting for DAP sinusoidal fit to converge...")
|
||||
self._wait_for_dap_fit(timeout=30)
|
||||
|
||||
# Read fit parameters from the GUI into the global variable store
|
||||
print("Loading new alignment parameters from X-ray eye GUI fit.")
|
||||
self.lamni.read_xray_eye_correction_from_gui()
|
||||
try:
|
||||
self.lamni.read_xray_eye_correction_from_gui()
|
||||
finally:
|
||||
self.gui.set_dap_params_forwarding(False)
|
||||
|
||||
if keep_shutter_open:
|
||||
answer = input("Close the shutter now? [Y/n]: ").strip().lower()
|
||||
@@ -446,10 +504,19 @@ class XrayEyeAlign:
|
||||
def write_output(self):
|
||||
"""Compute x/y offsets for each angle and push them to the GUI fit tab.
|
||||
|
||||
Offsets are the displacement of the sample from its position at 0 deg
|
||||
(k=1 is the reference):
|
||||
x_offset[i] = (x_at_0deg - x_at_angle_i) * 1000 [um]
|
||||
y_offset[i] = (y_at_angle_i - y_at_0deg) * 1000 [um]
|
||||
Offsets are the displacement of the sample at each angle from the FZP
|
||||
centre (k=0) -- matching FlOMNI's own reference point
|
||||
(self.alignment_values[0] - self.alignment_values[k]). The FZP centre
|
||||
is the only point in the whole procedure that's independent of
|
||||
rotation angle, so it's the only valid zero-reference: using one of
|
||||
the 8 angle points as the reference instead (as earlier versions of
|
||||
this method did, first via an off-by-one at k=1, then via k=2) forces
|
||||
that one point's own offset to be exactly zero by construction -- not
|
||||
because the sample happens to be centred there, but because
|
||||
subtracting a value from itself is always zero -- which biases the
|
||||
whole sinusoidal fit rather than measuring it.
|
||||
x_offset[i] = (x_at_FZP - x_at_angle_i) * 1000 [um]
|
||||
y_offset[i] = (y_at_angle_i - y_at_FZP) * 1000 [um]
|
||||
|
||||
The array passed to submit_fit_array has shape (3, 8):
|
||||
row 0: angles [0, 45, ..., 315]
|
||||
@@ -467,14 +534,10 @@ class XrayEyeAlign:
|
||||
os.makedirs(os.path.dirname(file), exist_ok=True)
|
||||
with open(file, "w") as f:
|
||||
f.write("angle\thorizontal\tvertical\n")
|
||||
for k in range(1, 9):
|
||||
angle_deg = LAMNI_ALIGNMENT_ANGLES[k - 1]
|
||||
x_off = (
|
||||
self.alignment_values[1][0] - self.alignment_values[k][0]
|
||||
) * 1000
|
||||
y_off = (
|
||||
self.alignment_values[k][1] - self.alignment_values[1][1]
|
||||
) * 1000
|
||||
for k in range(2, 10):
|
||||
angle_deg = LAMNI_ALIGNMENT_ANGLES[k - 2]
|
||||
x_off = (self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000
|
||||
y_off = (self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000
|
||||
f.write(f"{angle_deg}\t{x_off:.4f}\t{y_off:.4f}\n")
|
||||
print(
|
||||
f" Angle {angle_deg:3d} deg: "
|
||||
@@ -484,14 +547,14 @@ class XrayEyeAlign:
|
||||
angles = np.array(LAMNI_ALIGNMENT_ANGLES, dtype=float)
|
||||
x_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[1][0] - self.alignment_values[k][0]) * 1000
|
||||
for k in range(1, 9)
|
||||
(self.alignment_values[0][0] - self.alignment_values[k][0]) * 1000
|
||||
for k in range(2, 10)
|
||||
]
|
||||
)
|
||||
y_offsets = np.array(
|
||||
[
|
||||
(self.alignment_values[k][1] - self.alignment_values[1][1]) * 1000
|
||||
for k in range(1, 9)
|
||||
(self.alignment_values[k][1] - self.alignment_values[0][1]) * 1000
|
||||
for k in range(2, 10)
|
||||
]
|
||||
)
|
||||
data = np.array([angles, x_offsets, y_offsets])
|
||||
|
||||
@@ -3761,7 +3761,12 @@ class Flomni(
|
||||
csaxs_bec_basepath.parent / "bec_ipython_client" / "plugins" / "flomni" / logo_file_rel
|
||||
).resolve()
|
||||
print(logo_file)
|
||||
bec.messaging.scilog.new().add_attachment(logo_file, width=200).add_text(
|
||||
scilog = getattr(bec.messaging, "scilog", None)
|
||||
if scilog is None or not getattr(scilog, "_enabled", False):
|
||||
logger.warning("SciLog is not enabled; skipping PDF report entry.")
|
||||
return
|
||||
|
||||
scilog.new().add_attachment(logo_file, width=200).add_text(
|
||||
content.replace("\n", "<br>")
|
||||
).add_tags("tomoscan").send()
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import termios
|
||||
import threading
|
||||
import time
|
||||
import tty
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import epics
|
||||
@@ -323,11 +324,13 @@ class PtychoReconstructor:
|
||||
with open(json_file, "w") as f:
|
||||
json.dump(json_content, f, indent=4)
|
||||
|
||||
|
||||
class TomoIDManager:
|
||||
"""Registers a tomography measurement in the OMNY sample database
|
||||
and returns its assigned tomo ID.
|
||||
|
||||
Falls back to tomo ID 0 for non-production accounts (e.g. test
|
||||
accounts like "gac-x01dc") which the server rejects.
|
||||
|
||||
Usage:
|
||||
id_manager = TomoIDManager()
|
||||
tomo_id = id_manager.register(
|
||||
@@ -341,11 +344,14 @@ class TomoIDManager:
|
||||
)
|
||||
"""
|
||||
|
||||
# OMNY_URL = "https://omny.web.psi.ch/samples/newmeasurement.php"
|
||||
OMNY_URL = "https://v1p0zyg2w9n2k9c1.myfritz.net/samples/newmeasurement.php"
|
||||
OMNY_USER = ""
|
||||
OMNY_PASSWORD = ""
|
||||
TMP_FILE = "~/currsamplesnr.txt"
|
||||
FALLBACK_TOMO_ID = 0
|
||||
|
||||
@staticmethod
|
||||
def _is_valid_eaccount(eaccount: str) -> bool:
|
||||
"""True for real e-accounts (e.g. "e12345"), False for test accounts."""
|
||||
return bool(re.fullmatch(r"e\d{5}", eaccount.strip()))
|
||||
|
||||
def register(
|
||||
self,
|
||||
@@ -359,18 +365,16 @@ class TomoIDManager:
|
||||
) -> int:
|
||||
"""Register a new measurement and return the assigned tomo ID.
|
||||
|
||||
Args:
|
||||
sample_name (str): Name of the sample.
|
||||
date (str): Date string (e.g. "2024-03-08").
|
||||
eaccount (str): E-account identifier.
|
||||
scan_number (int): First scan number of the measurement.
|
||||
setup (str): Setup name (e.g. "lamni").
|
||||
additional_info (str): Any additional sample information.
|
||||
user (str): User name.
|
||||
|
||||
Returns:
|
||||
int: The tomo ID assigned by the OMNY database.
|
||||
Returns FALLBACK_TOMO_ID (0) if the account is not a real e-account
|
||||
or if the server cannot be reached / returns an unusable response.
|
||||
"""
|
||||
if not self._is_valid_eaccount(eaccount):
|
||||
logger.warning(
|
||||
f"Account '{eaccount}' is not a valid e-account; "
|
||||
f"skipping OMNY registration, using tomo ID {self.FALLBACK_TOMO_ID}."
|
||||
)
|
||||
return self.FALLBACK_TOMO_ID
|
||||
|
||||
url = (
|
||||
f"{self.OMNY_URL}"
|
||||
f"?sample={sample_name}"
|
||||
@@ -381,25 +385,20 @@ class TomoIDManager:
|
||||
f"&additional={additional_info}"
|
||||
f"&user={user}"
|
||||
)
|
||||
# subprocess.run(
|
||||
# f"wget --user={self.OMNY_USER} --password={self.OMNY_PASSWORD}"
|
||||
# f" -q -O {self.TMP_FILE} '{url}'",
|
||||
# shell=True,
|
||||
# )
|
||||
# print(url)
|
||||
|
||||
tmp_file = os.path.expanduser(self.TMP_FILE)
|
||||
result = subprocess.run(f"wget -q -O {tmp_file} '{url}'", shell=True)
|
||||
if result.returncode != 0:
|
||||
raise OMNYToolsError(
|
||||
f"wget failed (exit code {result.returncode}) fetching tomo ID from {self.OMNY_URL}"
|
||||
)
|
||||
try:
|
||||
result = subprocess.run(f"wget -q -O {tmp_file} '{url}'", shell=True, timeout=30)
|
||||
if result.returncode != 0:
|
||||
raise OMNYToolsError(
|
||||
f"wget failed (exit code {result.returncode}) fetching tomo ID from {self.OMNY_URL}"
|
||||
)
|
||||
with open(tmp_file) as f:
|
||||
content = f.read().strip()
|
||||
return int(content)
|
||||
except FileNotFoundError as exc:
|
||||
raise OMNYToolsError(f"wget did not produce output file {tmp_file}") from exc
|
||||
except ValueError as exc:
|
||||
raise OMNYToolsError(
|
||||
f"Unexpected response from tomo ID server, got: {content!r}"
|
||||
) from exc
|
||||
except (subprocess.TimeoutExpired, FileNotFoundError, ValueError, OMNYToolsError) as exc:
|
||||
logger.warning(
|
||||
f"Could not obtain tomo ID from OMNY database ({exc}); "
|
||||
f"falling back to tomo ID {self.FALLBACK_TOMO_ID}."
|
||||
)
|
||||
return self.FALLBACK_TOMO_ID
|
||||
@@ -298,20 +298,10 @@ cam_xeye:
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
userParameter:
|
||||
pixel_calibration: 0.0009174311926605505 # mm/pixel (= 0.2 / 218)
|
||||
|
||||
# ############################################################
|
||||
# ########## OMNY / flOMNI / LamNI fast shutter ##############
|
||||
# ############################################################
|
||||
omnyfsh:
|
||||
description: omnyfsh connects to fast shutter at X12 if device fsh exists
|
||||
deviceClass: csaxs_bec.devices.omny.shutter.OMNYFastShutter
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: baseline
|
||||
|
||||
############################################################
|
||||
############################################################
|
||||
#################### GUI Signals ###########################
|
||||
############################################################
|
||||
omny_xray_gui:
|
||||
@@ -321,13 +311,4 @@ omny_xray_gui:
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
|
||||
# calculated_signal:
|
||||
# description: Calculated signal from alignment for fit
|
||||
# deviceClass: ophyd_devices.ComputedSignal
|
||||
# deviceConfig:
|
||||
# compute_method: "def just_rand():\n return 42"
|
||||
# enabled: true
|
||||
# readOnly: false
|
||||
# readoutPriority: baseline
|
||||
readoutPriority: on_request
|
||||
@@ -16,6 +16,18 @@
|
||||
# omny_panda, omny_xray_gui.
|
||||
############################################################
|
||||
|
||||
############################################################
|
||||
#################### GUI Signals ###########################
|
||||
############################################################
|
||||
omny_xray_gui:
|
||||
description: Gui signals
|
||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayAlignGUI
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
|
||||
############################################################
|
||||
#################### flOMNI Galil motors ###################
|
||||
############################################################
|
||||
|
||||
@@ -136,7 +136,7 @@ lsamrot:
|
||||
sign: 1
|
||||
sim_stppermm: 50154.32099
|
||||
sim_encpermm: 36000
|
||||
sim_velocity: 12
|
||||
sim_velocity: 48 #4x than hw
|
||||
sim_initial_position: 0
|
||||
deviceTags:
|
||||
- lamni
|
||||
@@ -328,7 +328,8 @@ cam_xeye:
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
userParameter:
|
||||
pixel_calibration: 0.0009174311926605505 # mm/pixel (= 0.2 / 218)
|
||||
ddg1:
|
||||
description: Simulated main delay generator for triggering
|
||||
deviceClass: csaxs_bec.devices.sim.simulated_beamline_devices.SimulatedDDG1
|
||||
@@ -347,3 +348,15 @@ fsh:
|
||||
onFailure: raise
|
||||
enabled: true
|
||||
readoutPriority: monitored
|
||||
|
||||
############################################################
|
||||
#################### GUI Signals ###########################
|
||||
############################################################
|
||||
omny_xray_gui:
|
||||
description: Gui signals
|
||||
deviceClass: csaxs_bec.devices.omny.xray_epics_gui.OMNYXRayAlignGUI
|
||||
deviceConfig: {}
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
Reference in New Issue
Block a user