From 7be5ccefd09d10cdfc9e8ce4e4e1a583c843d465 Mon Sep 17 00:00:00 2001 From: x12sa Date: Fri, 3 Jul 2026 13:28:34 +0200 Subject: [PATCH] random offset control added to gui, and json file writing --- .../plugins/omny/omny_general_tools.py | 46 ++++++++++++++----- .../widgets/tomo_params/tomo_params.py | 33 ++++++++----- 2 files changed, 55 insertions(+), 24 deletions(-) diff --git a/csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py b/csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py index 1f9d276..ca05a82 100644 --- a/csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py +++ b/csaxs_bec/bec_ipython_client/plugins/omny/omny_general_tools.py @@ -1,6 +1,7 @@ import builtins import datetime import fcntl +import json import os import socket import subprocess @@ -28,9 +29,12 @@ if builtins.__dict__.get("bec") is not None: def umv(*args): return scans.umv(*args, relative=False) + + def umvr(*args): return scans.umv(*args, relative=True) + class OMNYToolsError(Exception): pass @@ -157,8 +161,10 @@ class OMNYTools: termios.tcsetattr(fd, termios.TCSADRAIN, old_term) fcntl.fcntl(fd, fcntl.F_SETFL, old_flags) + import socket + class PtychoReconstructor: """Writes ptychography reconstruction queue files after each scan projection. @@ -195,6 +201,8 @@ class PtychoReconstructor: next_scan_number: int, base_path: str = "~/data/raw/analysis/", probe_file_propagation: float | None = None, + random_offset_x: float | None = None, + random_offset_y: float | None = None, ): """Write a reconstruction queue file for the given scan list. @@ -211,19 +219,25 @@ class PtychoReconstructor: omitted entirely otherwise, matching the old reconstruction.mac behavior of only writing this parameter when progagateprobeinsteadofsample==1. + random_offset_x (float, optional): Random x shift [um] applied for + this single-point acquisition. Written to the JSON sidecar as + ``random_offset_x`` when given; omitted otherwise. + random_offset_y (float, optional): Random y shift [um] applied for + this single-point acquisition. Written to the JSON sidecar as + ``random_offset_y`` when given; omitted otherwise. """ if not self._accounts_match(): - logger.warning("Active BEC account does not match system user — skipping queue file write.") + logger.warning( + "Active BEC account does not match system user — skipping queue file write." + ) return - + base_path = os.path.expanduser(base_path) queue_path = Path(os.path.join(base_path, self.folder_name)) queue_path.mkdir(parents=True, exist_ok=True) last_scan_number = next_scan_number - 1 - queue_file = os.path.abspath( - os.path.join(queue_path, f"scan_{last_scan_number:05d}.dat") - ) + queue_file = os.path.abspath(os.path.join(queue_path, f"scan_{last_scan_number:05d}.dat")) with open(queue_file, "w") as f: scans = " ".join(str(s) for s in scan_list) f.write(f"p.scan_number {scans}\n") @@ -231,6 +245,17 @@ class PtychoReconstructor: f.write(f"p.probe_file_propagation {probe_file_propagation:.6f}\n") f.write("p.check_nextscan_started 1\n") + json_file = os.path.abspath(os.path.join(queue_path, f"{last_scan_number:06d}.json")) + json_content = {"scan_id": last_scan_number} + if probe_file_propagation is not None: + json_content["probe_file_propagation"] = round(probe_file_propagation, 6) + if random_offset_x is not None: + json_content["random_offset_x"] = round(random_offset_x, 6) + if random_offset_y is not None: + json_content["random_offset_y"] = round(random_offset_y, 6) + 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 @@ -249,7 +274,7 @@ class TomoIDManager: ) """ - #OMNY_URL = "https://omny.web.psi.ch/samples/newmeasurement.php" + # OMNY_URL = "https://omny.web.psi.ch/samples/newmeasurement.php" OMNY_URL = "https://v1p0zyg2w9n2k9c1.myfritz.net/samples/newmeasurement.php" OMNY_USER = "" OMNY_PASSWORD = "" @@ -294,12 +319,9 @@ class TomoIDManager: # f" -q -O {self.TMP_FILE} '{url}'", # shell=True, # ) - #print(url) + # print(url) tmp_file = os.path.expanduser(self.TMP_FILE) - result = subprocess.run( - f"wget -q -O {tmp_file} '{url}'", - shell=True, - ) + 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}" @@ -313,4 +335,4 @@ class TomoIDManager: except ValueError as exc: raise OMNYToolsError( f"Unexpected response from tomo ID server, got: {content!r}" - ) from exc + ) from exc \ No newline at end of file diff --git a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py index 9e1b9a8..201050a 100644 --- a/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py +++ b/csaxs_bec/bec_widgets/widgets/tomo_params/tomo_params.py @@ -104,6 +104,7 @@ QUEUE_PARAM_NAMES = ( "manual_shift_y", "frames_per_trigger", "single_point_instead_of_fermat_scan", + "single_point_random_shift_max", "tomo_type", "tomo_angle_range", "tomo_angle_stepsize", @@ -126,6 +127,7 @@ DEFAULTS: dict[str, Any] = { "manual_shift_y": 0.0, "frames_per_trigger": 1, "single_point_instead_of_fermat_scan": False, + "single_point_random_shift_max": 0.0, "tomo_type": 1, "corridor_size": -1.0, "tomo_angle_range": 180, @@ -364,6 +366,14 @@ class TomoParamsWidget(BECWidget, QWidget): self._add_int(common_form, "frames_per_trigger", "Frames / trigger", min_=1, max_=100) sp = self._add_bool(common_form, "single_point_instead_of_fermat_scan", "Single-point scan") sp.stateChanged.connect(self._on_single_point_changed) + self._add_double( + common_form, + "single_point_random_shift_max", + "Random shift max (µm)", + min_=0.0, + max_=10.0, + decimals=3, + ) vbox.addLayout(common_form) # type-1 section @@ -590,6 +600,9 @@ class TomoParamsWidget(BECWidget, QWidget): return "tomo_type must be 1, 2, or 3" if params.get("tomo_angle_range") not in (180, 360): return "tomo_angle_range must be 180 or 360" + rshift = params.get("single_point_random_shift_max", 0.0) + if not 0 <= rshift <= 10: + return "single_point_random_shift_max must be between 0 and 10 µm" return None # ── type visibility ─────────────────────────────────────────────────────── @@ -659,6 +672,8 @@ class TomoParamsWidget(BECWidget, QWidget): self._pw["stitch_y"].setValue(0) self._pw["stitch_x"].setEnabled(self._edit_mode and not checked) self._pw["stitch_y"].setEnabled(self._edit_mode and not checked) + # random shift only applies to single-point acquisitions + self._pw["single_point_random_shift_max"].setEnabled(self._edit_mode and checked) def _on_poll(self) -> None: """Periodic refresh: params (guarded against edit mode) + sample name.""" @@ -882,10 +897,13 @@ class TomoParamsWidget(BECWidget, QWidget): if key in skip: continue widget.setEnabled(enabled) - # stitch locked when single_point is active - if enabled and self._pw["single_point_instead_of_fermat_scan"].isChecked(): + # stitch locked when single_point is active; random shift is the + # inverse -- only editable while single_point is active + single_point = self._pw["single_point_instead_of_fermat_scan"].isChecked() + if enabled and single_point: self._pw["stitch_x"].setEnabled(False) self._pw["stitch_y"].setEnabled(False) + self._pw["single_point_random_shift_max"].setEnabled(enabled and single_point) # projection requested total only meaningful in edit mode self._pw["_requested_total"].setEnabled(enabled) @@ -981,16 +999,7 @@ class TomoQueueDialog(QDialog): self._table = QTableWidget(0, 8) self._table.setHorizontalHeaderLabels( - [ - "#", - "Label", - "Status", - "Type", - "Projections", - "Exp (s)", - "Step (µm)", - "Added at", - ] + ["#", "Label", "Status", "Type", "Projections", "Exp (s)", "Step (µm)", "Added at"] ) self._table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) self._table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)