random offset control added to gui, and json file writing

This commit is contained in:
x12sa
2026-07-07 10:41:40 +02:00
committed by holler
co-authored by holler
parent cc7cfeef3e
commit 7be5ccefd0
2 changed files with 55 additions and 24 deletions
@@ -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
@@ -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)