GUI: rotation_data_collection.py and rast_grid added total measurement time and file path protection
This commit is contained in:
@@ -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 GUI’s 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
|
||||
|
||||
Reference in New Issue
Block a user