GUI: rotation_data_collection.py and rast_grid added total measurement time and file path protection

This commit is contained in:
2025-11-25 09:05:20 +01:00
parent 9a733ddfb0
commit 63a0c519e3
3 changed files with 218 additions and 29 deletions
+117 -18
View File
@@ -1,14 +1,16 @@
import copy
import os
from datetime import datetime
from pathlib import Path
from PySide6.QtCore import Signal, Slot, Qt
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit, QSpinBox, QCheckBox
from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QLineEdit, QSpinBox, QCheckBox, QMessageBox
from aaredaqlib.models import SampleShortInfo
from aaredaqlib.models import SampleShortInfo, DAQStatusModel
from aaregui.widgets.title_label import TitleLabel
## Logic for filenames:
## 1. For rasters 'raster/' subfolder is added at the top level of the path (managed by DAQ) - e.g. rasters/20250101/PX-456/01/dataset
## 1. For rasters 'raster/' subfolder is added at the top level of the path (managed by DAQ) - e.g. raster/20250101/PX-456/01/dataset
## 2. For screening 'screening/' subfolder is added at the last level of the path (managed by GUI) - e.g. 20250101/PX-456/01/dataset/screening
## 3. If sample is not registered in the database, it is by default placed in test/<date>
## 4. If sample is registered in the database and has puck information, it is by default placed in <date>/<puck name>/<puck pos>/
@@ -25,8 +27,11 @@ class FilePathPanel(QWidget):
self.__dewar_pos = "None"
self.__puck_name = "Manual"
self.__puck_pos = 0
self.__curr_pgroup = "p11206"
self.__filename = ""
self.__scan_kind = "raster" # default: "rotation" | "screening" | "raster"
self.__formatted_date = datetime.now().strftime('%Y%m%d')
grid_layout.addWidget(TitleLabel("Dataset path", self), 0, 0, 1, 2)
@@ -73,6 +78,35 @@ class FilePathPanel(QWidget):
self.run_number_edit.valueChanged.connect(self.set_run_number)
self.update_filename()
def _expand_macros(self, base: str, rn: int) -> str:
name = f"{base}_{rn:03d}"
name = name.replace('{date}', self.__formatted_date)
name = name.replace('{sample}', self.__sample_name)
name = name.replace('{puck}', self.__puck_name)
name = name.replace('{pos}', f"{self.__puck_pos:02d}")
name = name.replace('{sample_id}', f"{self.__sample_id}")
return name
def _effective_dataset_base(self, base_no_run: str) -> str:
root = Path("/sls/mx/data") / self.__curr_pgroup / "raw"
p = Path(base_no_run)
match self.__scan_kind:
case "rotation":
p = Path("data") / p
case "screening":
p = Path("screening") / p
case "raster":
p = Path("raster") / p
case _:
pass
return str(root / p)
def _exists_for_run(self, expanded_base_with_run: str) -> bool:
# expanded_base_with_run is the base without scan-kind transforms yet
effective = self._effective_dataset_base(expanded_base_with_run)
# Consider master file and directory as taken
return os.path.exists(f"{effective}_master.h5") or os.path.exists(effective)
def update_filename(self):
dir_name = self.directory_edit.text()
file_prefix = self.file_prefix_edit.text()
@@ -81,26 +115,37 @@ class FilePathPanel(QWidget):
dir_name = dir_name.replace('{prefix}', file_prefix)
if dir_name == "":
self.__filename = ""
base = ""
elif dir_name[-1] == "/":
self.__filename = dir_name
base = dir_name
else:
self.__filename = dir_name + "/"
base = dir_name + "/"
if file_prefix == "":
self.__filename += "run"
else:
self.__filename += file_prefix
base += "run" if file_prefix == "" else file_prefix
self.__filename += f"_{run_number:03d}"
self.__filename = self.__filename.replace('{date}', self.__formatted_date)
self.__filename = self.__filename.replace('{sample}', self.__sample_name)
self.__filename = self.__filename.replace('{puck}', self.__puck_name)
self.__filename = self.__filename.replace('{pos}', f"{self.__puck_pos:02d}")
self.__filename = self.__filename.replace('{sample_id}', f"{self.__sample_id}")
# Find next free run number using effective dataset path
rn = run_number
while True:
candidate_base = self._expand_macros(base, rn)
if not self._exists_for_run(candidate_base):
break
rn += 1
if rn > self.run_number_edit.maximum():
break
self.file_name_label.setText(self.__filename + "_master.h5")
self.file_name_label.setStyleSheet("color: rgb(0, 0, 0);")
if rn != run_number:
self.run_number_edit.blockSignals(True)
self.run_number_edit.setValue(rn)
self.run_number_edit.blockSignals(False)
# Store the GUIs base (without applying scan-kind transforms) for wiring into requests later
self.__filename = self._expand_macros(base, rn)
# Preview label shows the effective path (what will be written)
effective = self._effective_dataset_base(self.__filename)
exists = os.path.exists(f"{effective}_master.h5") or os.path.exists(effective)
self.file_name_label.setText(effective + "_master.h5")
self.file_name_label.setStyleSheet("color: rgb(200, 0, 0);" if exists else "color: rgb(0, 0, 0);")
self.path_updated.emit(self.__filename)
@Slot()
@@ -151,3 +196,57 @@ class FilePathPanel(QWidget):
else:
self.directory_edit.setText(f"{self.__formatted_date}/manual/{self.__sample_name}")
self.update_filename()
@Slot(DAQStatusModel)
def update_daq_status(self, status: DAQStatusModel):
if status.session.current_pgroup:
self.__curr_pgroup = status.session.current_pgroup
@Slot(str)
def set_scan_kind(self, kind: str):
# kind in {"rotation","screening","raster"}
self.__scan_kind = kind
self.update_filename()
def effective_path_for_base(self, base_with_run: str) -> str:
return self._effective_dataset_base(base_with_run)
def next_free_run_from(self, start_rn: int) -> tuple[int, str]:
# Compute next free run number and updated base
dir_name = self.directory_edit.text().replace('{prefix}', self.file_prefix_edit.text())
base = (dir_name if dir_name.endswith("/") or dir_name == "" else dir_name + "/")
base += ("run" if self.file_prefix_edit.text() == "" else self.file_prefix_edit.text())
rn = start_rn
while rn <= self.run_number_edit.maximum():
candidate_base = self._expand_macros(base, rn)
if not self._exists_for_run(candidate_base):
return rn, candidate_base
rn += 1
return start_rn, self._expand_macros(base, start_rn)
def file_path_error_box(self, scan_kind: str):
self.set_scan_kind(scan_kind)
base = self.filename # same base we emit
effective = self.effective_path_for_base(base)
if os.path.exists(f"{effective}_master.h5") or os.path.exists(effective):
reply = QMessageBox.question(
self,
"File exists",
#f"This file already exists:\n{effective}_master.h5\nDo you wish to overwrite?",
f"This file already exists:\n{effective}_master.h5\n Updating run number",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.No:
# bump run and update
curr = self.run_number_edit.value()
new_rn, _ = self.next_free_run_from(curr + 1)
self.run_number_edit.setValue(new_rn)
self.update_filename()
return False
else:
return False
return False
return True
@@ -8,7 +8,9 @@ from aaregui.panels.scan_settings_panel import ScanSettingsPanel
from aaregui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric
from aaregui.widgets.number_line_edit import NumberLineEdit
from aaregui.widgets.raster_grid_table import RasterGridTable
from aaredaqlib.logger_config import setup_logger
logger = setup_logger("aareGUI")
class RasterDataCollectionPanel(ScanSettingsPanel):
grid_size_updated = Signal(float, float)
@@ -30,6 +32,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self.__n_y = raster_mgr.active_grid.n_y
self.__size_x = raster_mgr.active_grid.grid_size_mm.x * 1000.0
self.__size_y = raster_mgr.active_grid.grid_size_mm.y * 1000.0
self.__total_time = raster_mgr.active_grid.exp_time_s*self.__n_x*self.__n_y
self._layout.addWidget(QLabel("Grid element size", parent=self), 3, 0)
@@ -112,15 +115,24 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
)
self._layout.addItem(horizontal_spacer, 10, 0, 1, 5)
self._layout.addWidget(QLabel("Measurement time", parent=self), 11, 0)
self.total_time = QLabel(f"{self.__total_time}")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.total_time, 11, 1, 1, 3)
self._layout.addWidget(QLabel("min", parent=self), 11, 4)
self.image_time_enter.newValue.connect(self.exp_time_s)
self.calculate_total_time()
self.start_button = QPushButton("Evaluate grid")
self.start_button.setStyleSheet("color: rgb(78, 154, 6);")
self.start_button.clicked.connect(self.evaluate_grid)
self._layout.addWidget(self.start_button, 11, 0, 1, 5)
self.start_button.clicked.connect(self._on_evaluate_clicked)
self._layout.addWidget(self.start_button, 12, 0, 1, 5)
self.auto_button = QPushButton("Feeling lucky")
self.auto_button.setStyleSheet("color: rgb(78, 154, 6);")
self.auto_button.clicked.connect(self.evaluate_grid_auto)
self._layout.addWidget(self.auto_button, 12, 0, 1, 5)
self.auto_button.clicked.connect(self._on_evaluate_auto_clicked)
self._layout.addWidget(self.auto_button, 13, 0, 1, 5)
self.update_grid_scan_size()
@@ -135,6 +147,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
@Slot(float)
def exp_time_s(self, inp: float):
self.exp_time_updated.emit(inp)
self.calculate_total_time()
@Slot(int, int, float, float)
def grid_scan_size_change(self, n_x: int, n_y: int, size_x_mm: float, size_y_mm: float):
@@ -142,6 +155,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self.__size_y = size_y_mm * 1000.0
self.__n_x = n_x
self.__n_y = n_y
self.calculate_total_time()
self.update_grid_scan_size()
@Slot(DAQStatusModel)
@@ -174,6 +188,13 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
self.size_x_label.setText(f"{self.__size_x * self.__n_x:.1f}")
self.n_y_label.setText(str(self.__n_y))
self.size_y_label.setText(f"{self.__size_y * self.__n_y:.1f}")
self.total_time.setText(f"{(self.__total_time / 60.0):.2f}")
try:
self.exp_time_s(self.image_time_enter.value)
except ValueError as e:
logger.warning(f"Invalid exposure time: {e} reseting to default")
self.exp_time_s(0.02)
def metric_changed(self, _: int):
self.grid_metric_updated.emit(self.metric_combo.currentData())
@@ -184,3 +205,32 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
('exposure', self.image_time_enter, None),
# Add other raster-specific parameters here as needed
]
def calculate_total_time(self):
if self.__n_x <= 0 or self.__n_y <= 0 or self.image_time_enter.value < 0:
self.__total_time = 0.0
else:
self.__total_time = self.__n_x * self.__n_y * self.image_time_enter.value
# Show minutes
self.total_time.setText(f"{(self.__total_time/60.0):.2f}")
@Slot()
def _on_evaluate_clicked(self):
p = self.parent()
if hasattr(p, "file_path_panel"):
reply = p.file_path_panel.file_path_error_box("raster")
logger.debug(f"reply from file path panel: {reply}")
if not reply:
return
self.evaluate_grid.emit()
@Slot()
def _on_evaluate_auto_clicked(self):
p = self.parent()
if hasattr(p, "file_path_panel"):
reply = p.file_path_panel.file_path_error_box("raster")
logger.debug(f"reply from file path panel: {reply}")
if not reply:
return
self.evaluate_grid_auto.emit()
@@ -1,7 +1,7 @@
from pathlib import Path
from PySide6.QtCore import Slot, Signal, Qt
from PySide6.QtWidgets import QLabel, QComboBox, QPushButton
from PySide6.QtWidgets import QLabel, QComboBox, QPushButton, QMessageBox
from aaredaqlib.diffraction_geometry import DiffractionGeometry
from aaredaqlib.logger_config import setup_logger
@@ -14,7 +14,7 @@ logger = setup_logger("aareGUI")
def add_screening_to_path(path):
p = Path(path)
return p.parent / "screening" / p.name
return "screening" / p
def add_data_to_path(path):
p = Path(path)
@@ -33,11 +33,13 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
default_dtz=default_dtz,
default_transmission=default_transmission)
self.__curr_pgroup = "p11206"
self._filename = ""
self._previous_sample_was_none_rotation = True
self.__omega = 0
self.__dose_mgy = 0
self.__total_time = 0.0
self._layout.addWidget(QLabel("Start angle", parent=self), 3, 0)
self.start_angle = NumberLineEdit(-720, 720.0, 0.0, decimals=3, parent=self)
@@ -97,23 +99,44 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
self._layout.addWidget(self.image_time_enter, 12, 1, 1, 3)
self._layout.addWidget(QLabel("s", parent=self), 12, 4)
self._layout.addWidget(QLabel("Dose", parent=self), 13, 0)
self._layout.addWidget(QLabel("Total measurement time", parent=self), 13, 0)
self.total_time = QLabel(f"{self.__total_time}")
self.total_time.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.total_time, 13, 1, 1, 3)
self._layout.addWidget(QLabel("min", parent=self), 13, 4)
self.total_angle.newValue.connect(self.calculate_measurement_time)
self.image_angle.newValue.connect(self.calculate_measurement_time)
self.image_time_enter.newValue.connect(self.calculate_measurement_time)
# Initial compute
self.calculate_measurement_time()
self._layout.addWidget(QLabel("Dose", parent=self), 14, 0)
self.dose = QLabel(f"{self.__dose_mgy}")
self.dose.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
self._layout.addWidget(self.dose, 13, 1, 1, 3)
self._layout.addWidget(QLabel("MGy", parent=self), 13, 4)
self._layout.addWidget(self.dose, 14, 1, 1, 3)
self._layout.addWidget(QLabel("MGy", parent=self), 14, 4)
self._layout.removeWidget(self.reload_params_button)
self._layout.addWidget(self.reload_params_button, 14, 0, 1, 6)
self._layout.addWidget(self.reload_params_button, 15, 0, 1, 6)
self.reload_params_button.setVisible(True)
self.measurement_button = QPushButton("Run rotation")
self.measurement_button.setStyleSheet("color: rgb(78, 154, 6);")
self.measurement_button.clicked.connect(self.run_measurement)
self._layout.addWidget(self.measurement_button, 15, 0, 1, 6)
self._layout.addWidget(self.measurement_button, 16, 0, 1, 6)
@Slot()
def run_screening(self):
p = self.parent()
logger.debug("make box")
if hasattr(p, "file_path_panel"):
logger.debug("should make box")
reply = p.file_path_panel.file_path_error_box("screening")
logger.debug(f"reply from file path panel: {reply}")
if not reply:
return
logger.debug("no box")
screening_settings = self.screening_type.currentData()
r = RotationScanRequest(
file_prefix=str(add_screening_to_path(self._filename)),
@@ -130,6 +153,12 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
@Slot()
def run_measurement(self):
p = self.parent()
if hasattr(p, "file_path_panel"):
reply = p.file_path_panel.file_path_error_box("rotation")
logger.debug(f"reply from file path panel: {reply}")
if not reply:
return
r = RotationScanRequest(
file_prefix=str(add_data_to_path(self._filename)),
start_omega_deg=self.start_angle.value,
@@ -154,6 +183,14 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
return round(total_angle / image_angle)
def calculate_measurement_time(self):
if self.image_angle.value <= 0 or self.total_angle.value <= 0 or self.image_time_enter.value < 0:
self.__total_time = 0.0
self.total_time.setText("0.0")
return
self.__total_time = (self.total_angle.value / self.image_angle.value) * self.image_time_enter.value
self.total_time.setText(f"{self.__total_time / 60:.2f}")
@Slot(str)
def update_filename(self, filename: str):
self._filename = filename
@@ -186,6 +223,9 @@ class RotationDataCollectionPanel(ScanSettingsPanel):
elif s.sample is not None:
self._previous_sample_was_none_rotation = False
self.calculate_measurement_time()
if s.session.current_pgroup:
self.__curr_pgroup = s.session.current_pgroup
lambda_a = s.diffraction.wavelength_angstrom
kdose = 2000 / (lambda_a * lambda_a)
beam_area = s.geom.beam_size_mm.x * s.geom.beam_size_mm.y * 1e6