From 7676faf3ffe0e1a8ba80a6295585262ea4e390b4 Mon Sep 17 00:00:00 2001 From: x12sa Date: Wed, 1 Jul 2026 11:56:30 +0200 Subject: [PATCH 1/7] added predict gap to csaxs --- .../bec_ipython_client/plugins/cSAXS/cSAXS.py | 102 +++++++++++++++--- .../cSAXS/intensity_map_predict_gap.py | 26 +++++ 2 files changed, 114 insertions(+), 14 deletions(-) create mode 100755 csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py index f24d80d..42325de 100644 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/cSAXS.py @@ -1,23 +1,21 @@ -# import builtins -# import datetime -# import os -# import subprocess -# import time -# from pathlib import Path +import inspect -# import numpy as np from bec_lib import bec_logger -# from bec_lib.alarm_handler import AlarmBase -# from bec_lib.pdf_writer import PDFWriter from typeguard import typechecked - +from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics +from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission +from csaxs_bec.bec_ipython_client.plugins.cSAXS.intensity_map_predict_gap import ( + predict_gap as _predict_gap, +) +from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSInitSmaractStages from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSSmaract from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools -from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission -from csaxs_bec.bec_ipython_client.plugins.cSAXS.diagnostics import cSAXSDiagnostics -from csaxs_bec.bec_ipython_client.plugins.cSAXS.slits import cSAXSSlits + +logger = bec_logger.logger + + class cSAXSError(Exception): pass @@ -36,6 +34,80 @@ class cSAXS( self.diagnostics = cSAXSDiagnostics() super().__init__(client=client) + # ------------------------------------------------------------------ + # Undulator + # ------------------------------------------------------------------ + + def predict_gap(self, energy: float, n: int = 3) -> None: + """Print the predicted undulator gap for *energy* [keV] on harmonic *n*. + + Examples + -------- + csaxs.predict_gap(6.2) # h=3 (default) + csaxs.predict_gap(10.0, n=5) # explicit harmonic + """ + import math + + gap = float(_predict_gap(energy, n=n)) + if math.isnan(gap): + print(f"Energy {energy:.3f} keV is unreachable on harmonic {n}.") + else: + print(f"Predicted gap for {energy:.3f} keV (h={n}): {gap:.4f} mm") + + # ------------------------------------------------------------------ + # Help / discovery + # ------------------------------------------------------------------ + + def commands(self) -> None: + """Print a table of all available cSAXS commands and sub-namespaces.""" + from rich import box + from rich.console import Console + from rich.table import Table + + console = Console() + + entries: list[tuple[str, str]] = [] + seen: set[str] = set() + + for cls in type(self).__mro__: + if cls is object: + continue + module = getattr(cls, "__module__", "") or "" + if "csaxs_bec" not in module: + continue + for name, func in inspect.getmembers(cls, predicate=inspect.isfunction): + if name.startswith("_") or name in seen: + continue + seen.add(name) + doc = (inspect.getdoc(func) or "").split("\n")[0].strip() + entries.append((name, doc)) + + entries.sort(key=lambda x: x[0]) + + tbl = Table(title="cSAXS Commands", box=box.SQUARE, show_lines=False) + tbl.add_column("Command", style="cyan bold", no_wrap=True, min_width=46) + tbl.add_column("Description") + for name, doc in entries: + tbl.add_row(f"csaxs.{name}()", doc) + console.print(tbl) + console.print("") + + ns = Table(title="Sub-namespaces", box=box.SQUARE, show_lines=False) + ns.add_column("Access", style="cyan bold", no_wrap=True, min_width=46) + ns.add_column("Description") + for access, desc in [ + ("csaxs.diagnostics.show_all()", "All diagnostic device readbacks"), + ( + "csaxs.diagnostics.bpm_xbox1 / .bpm_xbox2", + "BPM diagnostics — .show_all(), .gain(val)", + ), + ("csaxs.diagnostics.bim", "BIM diagnostics — .show_all(), .gain(val)"), + ("csaxs.diagnostics.beamstop", "Beamstop diode — .show_all(), .gain(val)"), + ("csaxs.diagnostics.polarization", "Polarization diodes — .show_all(), .gain(val)"), + ("csaxs.OMNYTools.*", "OMNY instrument tools"), + ]: + ns.add_row(access, desc) + console.print(ns) # this is the csaxs master file that imports all routines from csaxs @@ -45,4 +117,6 @@ class cSAXS( # csaxs = cSAXS(bec) # # then all commands can be accessed by for example -# csaxs._cSAXS_smaract_stages_..... \ No newline at end of file +# csaxs.commands() +# csaxs.predict_gap(6.2) +# csaxs._cSAXS_smaract_stages_... \ No newline at end of file diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py new file mode 100755 index 0000000..e991ba7 --- /dev/null +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py @@ -0,0 +1,26 @@ +"""Undulator gap predictor emitted by plot_intensity_map.py. +Edit the fitted constants in the signature to retune.""" + +import numpy as np + + +def predict_gap(energy, n=3, gap_min=5.0, + E_inf=3.83167, c0=3.1133, c1=-0.644678, c2=0.0210398): + """Undulator gap [mm] to place `energy` [keV] on harmonic `n`. + Fitted constants are the defaults below; edit them to retune. + Returns NaN where the energy is unreachable on that harmonic.""" + energy = np.asarray(energy, float) + arg = E_inf * n / energy - 1.0 # required K^2/2; must be > 0 + with np.errstate(invalid="ignore", divide="ignore"): + y = np.log(arg) + if abs(c2) < 1e-12: + g = (y - c0) / c1 + else: + disc = c1 * c1 - 4.0 * c2 * (c0 - y) + sq = np.sqrt(np.where(disc >= 0, disc, np.nan)) + r1 = (-c1 + sq) / (2.0 * c2) + r2 = (-c1 - sq) / (2.0 * c2) + g = np.where(c1 + 2.0 * c2 * r1 < 0, r1, r2) + g = np.where(arg > 0, g, np.nan) # above harmonic cutoff + g = np.where(g >= gap_min, g, np.nan) # below mechanical minimum + return g -- 2.54.0 From 10b48d009bd2bff4cd884d993a9e8045b968fba2 Mon Sep 17 00:00:00 2001 From: menzel Date: Wed, 1 Jul 2026 17:57:37 +0200 Subject: [PATCH 2/7] Update predict_gap constants to corrected 3-parameter fit Replace the 4-parameter (quadratic) constants with the 3-parameter pure-exponential fit from plot_intensity_map.py (operating-locus calibration, gap-residual RMS ~14 um). c2 was insignificant (0.2 sigma) and left E_inf degenerate (+/-11 keV). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../plugins/cSAXS/intensity_map_predict_gap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py index e991ba7..dbdc992 100755 --- a/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py +++ b/csaxs_bec/bec_ipython_client/plugins/cSAXS/intensity_map_predict_gap.py @@ -5,7 +5,7 @@ import numpy as np def predict_gap(energy, n=3, gap_min=5.0, - E_inf=3.83167, c0=3.1133, c1=-0.644678, c2=0.0210398): + E_inf=3.2878, c0=2.46086, c1=-0.468091, c2=0.0): """Undulator gap [mm] to place `energy` [keV] on harmonic `n`. Fitted constants are the defaults below; edit them to retune. Returns NaN where the energy is unreachable on that harmonic.""" -- 2.54.0 From e609ac1ccb85713f726024386d3b68f6d480231d Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 2 Jul 2026 07:34:55 +0200 Subject: [PATCH 3/7] optics out also in x --- .../bec_ipython_client/plugins/flomni/flomni_optics_mixin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py index 0ac68d8..1b825e7 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni_optics_mixin.py @@ -98,8 +98,9 @@ class FlomniOpticsMixin: dev.rtx.controller.feedback_disable() self.fosa_out() + foptx_out = self._get_user_param_safe("foptx", "out") fopty_out = self._get_user_param_safe("fopty", "out") - umv(dev.fopty, fopty_out) + umv(dev.foptx, foptx_out, dev.fopty, fopty_out) if "rtx" in dev and dev.rtx.enabled: time.sleep(1) @@ -237,7 +238,7 @@ class FlomniOpticsMixin: console.print(table) - diameters = [150e-6, 250e-6] + diameters = [140e-6, 170e-6, 200e-6, 250e-6] console = Console() table = Table(title="Outermost zone width \033[1m30 nm\033[0m", box=box.SQUARE) -- 2.54.0 From b5a8c8c8c6c9d9ca407783f18de83b478d48a75f Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 2 Jul 2026 07:35:16 +0200 Subject: [PATCH 4/7] single point acquisition zero z damaged hardware --- .../bec_ipython_client/plugins/flomni/flomni.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py index d2c6fff..2dbca15 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/flomni.py @@ -2560,6 +2560,10 @@ class Flomni( + self.manual_shift_y ) sum_offset_z = offsets[2] + + # TODO this fix is while the tracker z is broken + probe_propagation = -sum_offset_z * 1e-6 + sum_offset_z = 0 # --- positioning + laser tracker, mirroring # flomni_fermat_scan._prepare_setup_part2 --- @@ -2580,6 +2584,7 @@ class Flomni( # --- acquire --- n_frames = frames_per_trigger if frames_per_trigger is not None else self.frames_per_trigger scans.acquire(exp_time=self.tomo_countingtime, frames_per_trigger=n_frames) + self.tomo_reconstruct(probe_propagation=probe_propagation) def _tomo_type1_actual_grid(self) -> tuple[int, float, int]: """Compute the actual (achievable) tomo_type==1 grid from the @@ -2618,13 +2623,17 @@ class Flomni( print(f"Frames per trigger (burst) = {self.frames_per_trigger}") print(f"Single point instead of fermat = {self.single_point_instead_of_fermat_scan}") print("") + if self.tomo_type == 1: print("\x1b[1mTomo type 1:\x1b[0m 8 equally spaced sub-tomograms") print(f"Angular range = {self.tomo_angle_range} degrees") - print( - f"Total number of projections: {(self.tomo_angle_range/self.tomo_angle_stepsize)*8}" - ) - print(f"Angular step within sub-tomogram: {self.tomo_angle_stepsize} degrees") + # N, step, total_projections all come from the same helper + # sub_tomo_scan() effectively uses internally - see + # _tomo_type1_actual_grid() for why this can't just read + # self.tomo_angle_stepsize directly. + _, achievable_step, total_projections = self._tomo_type1_actual_grid() + print(f"Total number of projections: {total_projections}") + print(f"Angular step within sub-tomogram: {achievable_step:.3f} degrees") print( "Angular step of the final (combined) tomogram:" f" {self.tomo_angle_range / total_projections:.3f} degrees" -- 2.54.0 From 25f38b420b5f63b845a999b24cdb318523e57748 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 2 Jul 2026 07:35:35 +0200 Subject: [PATCH 5/7] parameter adjustments --- csaxs_bec/device_configs/ptycho_flomni.yaml | 31 +++++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/csaxs_bec/device_configs/ptycho_flomni.yaml b/csaxs_bec/device_configs/ptycho_flomni.yaml index 0b29b8c..c7d8d7b 100644 --- a/csaxs_bec/device_configs/ptycho_flomni.yaml +++ b/csaxs_bec/device_configs/ptycho_flomni.yaml @@ -76,7 +76,10 @@ foptx: connectionTimeout: 20 userParameter: #170 micron, 60 nm - in: -13.831 + #in: -13.831 + #250 micron, 30 nm, Abe structures + in: -13.8809375 + out: -14.1809 fopty: description: Optics Y deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -95,8 +98,11 @@ fopty: connectionTimeout: 20 userParameter: #170 micron, 60 nm - in: 0.42 - out: 0.57 + #in: 0.42 + #out: 0.57 + #250 micron, 30 nm, Abe structures + in: 2.8299 + out: 2.8299 foptz: description: Optics Z deviceClass: csaxs_bec.devices.omny.galil.fgalil_ophyd.FlomniGalilMotor @@ -157,7 +163,7 @@ fsamy: host: mpc2844.psi.ch limits: - 2 - - 3.3 + - 3.8 port: 8081 sign: 1 enabled: true @@ -305,7 +311,10 @@ fosax: #in: 8.7568 #out: 5.1 #170 micron, 60 nm, 7.9 kev - in: 8.731922 + # in: 8.731922 + # out: 5.1 + #250 micron, 30 nm, Abe structures + in: 8.755141 out: 5.1 fosay: description: OSA Y @@ -327,8 +336,9 @@ fosay: #170 micron, 60 nm, 7.6 kev #in: -0.0235 #170 micron, 60 nm, 7.6 kev - in: -0.0422 - + #in: -0.0422 + #250 micron, 30 nm, Abe structures + in: -2.357436 fosaz: description: OSA Z deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor @@ -350,8 +360,11 @@ fosaz: #in: 8.5 #out: 6 #170 micron, 60 nm, 7.9 kev, foptz 15.9 - in: 11.9 - out: 6 + # in: 11.9 + # out: 6 + # micron, 30 nm, 7.9 kev, foptz 32 //abe's fzp's + in: -2 + out: -5 ############################################################ #################### flOMNI RT motors ###################### -- 2.54.0 From 500c1e6c534015b8c4fc361e030985f286ce1f70 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 2 Jul 2026 07:35:51 +0200 Subject: [PATCH 6/7] save roi in pixels with images --- .../plugins/flomni/x_ray_eye_align.py | 82 +++++++++++-------- 1 file changed, 46 insertions(+), 36 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py index 732dbd9..a1e2717 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py @@ -70,14 +70,26 @@ class XrayEyeAlign: # so a second submission at step==1 is treated as the real angle-0 # fit point instead of triggering another height correction. self._height_centered = False + # Raw pixel coords + ROI size collected at each submit: + # [[step_k, x_px, y_px, w_px, h_px, image_idx], ...] + # image_idx refers to alignment_images[image_idx], i.e. the last + # frame captured before that submit (shutter is closed at submit time). + self.roi_pixel_data = [] - def _save_alignment_data(self, file_path: str): - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with h5py.File(os.path.expanduser(file_path), "w") as f: + def _save_alignment_data(self, file_path: str, fit_data: np.ndarray | None = None): + expanded = os.path.expanduser(file_path) + os.makedirs(os.path.dirname(expanded), exist_ok=True) + with h5py.File(expanded, "w") as f: f.create_dataset( "alignment_values", data=np.array(list(self.alignment_values.values())) ) f.create_dataset("alignment_images", data=np.array(self.alignment_images)) + if self.roi_pixel_data: + ds = f.create_dataset("roi_pixel_data", data=np.array(self.roi_pixel_data)) + ds.attrs["columns"] = ["step_k", "x_px", "y_px", "w_px", "h_px", "image_idx"] + if fit_data is not None: + ds = f.create_dataset("alignment_fit", data=fit_data) + ds.attrs["rows"] = ["angles_deg", "offsets_um", "zeros"] def update_frame(self, keep_shutter_open=False): if self.flomni._flomnigui_check_attribute_not_exists("xeyegui"): @@ -220,6 +232,20 @@ class XrayEyeAlign: # reset submit channel dev.omny_xray_gui.submit.set(0) + # Raw pixel position and ROI size at submit time. + # The relevant image is the last captured frame (shutter is + # closed by the time the user clicks submit). + _raw_x = getattr(dev.omny_xray_gui, f"xval_x_{k}").get() + _raw_y = getattr(dev.omny_xray_gui, f"yval_y_{k}").get() + _raw_w = getattr(dev.omny_xray_gui, f"width_x_{k}").get() + _raw_h = getattr(dev.omny_xray_gui, f"width_y_{k}").get() + _img_idx = len(self.alignment_images) - 1 + print( + f" Submit k={k}: px x={_raw_x:.1f} y={_raw_y:.1f} " + f"w={_raw_w:.1f} h={_raw_h:.1f} img={_img_idx}" + ) + self.roi_pixel_data.append([k, _raw_x, _raw_y, _raw_w, _raw_h, _img_idx]) + # Controls whether `k` advances to the next step below. Left # True except for the height-centering submission, which # reuses k==1 for a second, real submission afterwards. @@ -252,9 +278,8 @@ class XrayEyeAlign: self.gui.show_crosshair() self.send_message( - "Adjust sample height with the arrows if needed, then mark " - "the sample and submit - height will be centered automatically" - ) + "Submit height. Use arrows if far off." + ) self.gui.enable_submit_button(True) self.movement_buttons_enabled(True, True) @@ -373,37 +398,22 @@ class XrayEyeAlign: ) def write_output(self): - file = os.path.expanduser("~/data/raw/logs/xrayeye_alignmentvalues/xrayeye_alignmentvalues") timestamp = time.strftime("%Y%m%d_%H%M%S") - self._save_alignment_data(file + f"_image_data_{timestamp}.h5") - if not os.path.exists(file): - os.makedirs(os.path.dirname(file), exist_ok=True) + file_h5 = f"~/data/raw/logs/xrayeye_alignmentvalues/xrayeye_alignmentvalues_{timestamp}.h5" - with open(file, "w") as alignment_values_file: - alignment_values_file.write("angle\thorizontal\n") + fovx_offsets = np.zeros(5) + for k in range(1, 6): + fovx_offset = self.alignment_values[0] - self.alignment_values[k] + fovx_offsets[k - 1] = fovx_offset + print(f"Alignment number {k}, value x {fovx_offset}") - # Initialize an empty list to store fovx values - fovx_list = [] - fovx_offsets = np.zeros(5) # holds offsets for k = 1..5 - - for k in range(1, 6): - fovx_offset = self.alignment_values[0] - self.alignment_values[k] - fovx_offsets[k - 1] = fovx_offset # store in array - - fovx_x = (k - 1) * 45 - fovx_list.append([fovx_x, fovx_offset * 1000]) # Append the data to the list - - print(f"Alignment number {k}, value x {fovx_offset}") - alignment_values_file.write(f"{fovx_x}\t{fovx_offset * 1000}\n") - - # Now build final numpy array: - data = np.array( - [ - [0, 45, 90, 135, 180], # angles - fovx_offsets * 1000, # fovx_offset values - [0, 0, 0, 0, 0], - ] - ) + data = np.array( + [ + [0, 45, 90, 135, 180], # angles + fovx_offsets * 1000, # fovx_offset values + [0, 0, 0, 0, 0], + ] + ) + self._save_alignment_data(file_h5, fit_data=data) self.gui.submit_fit_array(data) - print(f"fit submited with {data}") - # self.flomni.flomnigui_show_xeyealign_fittab() \ No newline at end of file + print(f"fit submited with {data}") \ No newline at end of file -- 2.54.0 From c109687cf3bc5de653283d9198b507b91c045ea4 Mon Sep 17 00:00:00 2001 From: x12sa Date: Thu, 2 Jul 2026 08:44:10 +0200 Subject: [PATCH 7/7] added ROI selector to saved datafile --- .../bec_ipython_client/plugins/flomni/x_ray_eye_align.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py index a1e2717..422046f 100644 --- a/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py +++ b/csaxs_bec/bec_ipython_client/plugins/flomni/x_ray_eye_align.py @@ -278,7 +278,12 @@ class XrayEyeAlign: self.gui.show_crosshair() self.send_message( +<<<<<<< Updated upstream "Submit height. Use arrows if far off." +======= + "Adjust sample height with the arrows if needed, then mark " + "the sample and submit - height will be centered automatically" +>>>>>>> Stashed changes ) self.gui.enable_submit_button(True) self.movement_buttons_enabled(True, True) -- 2.54.0