Live changes from early July, PX-III
Build and Publish / test (pull_request_target) Successful in 2m59s
Build and Publish / build (pull_request_target) Successful in 19s
Build and Publish / Build and Deploy Docs (pull_request_target) Successful in 35s

This commit was merged in pull request #111.
This commit is contained in:
David Perl
2026-07-27 10:33:04 +02:00
committed by perl_d
parent aae4ea7a32
commit df3ddc5c84
19 changed files with 112 additions and 219 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
# AareDAQ (v0.3.1)
# AareDAQ
AareDAQ is the Data Acquisition system for MX beamlines at PSI.
@@ -49,4 +49,4 @@ AareDAQ is the Data Acquisition system for MX beamlines at PSI.
- `Ctrl + W`: toggle Workflow panel
- `Ctrl + Shift + T`: toggle Target stability panel
- `Ctrl + Shift + P`: toggle Prediction Metrics panel
- `Ctrl + Shift + S`: toggle Smargon trace panel
- `Ctrl + Shift + S`: toggle Smargon trace panel
+1
View File
@@ -41,6 +41,7 @@ test = [
"pytest-mock==3.14.0",
"pytest-qt==4.4.0",
"pytest-asyncio==0.25.3",
"pytest-timeout"
]
[lint]
+1 -10
View File
@@ -76,16 +76,7 @@ def authenticate_from_proxy_header(request: Request) -> str:
"""
client_host = request.client.host if request.client else None
logger.debug(f"[auth] /token client_host={client_host!r} request.client={request.client!r}")
if not _is_loopback(client_host):
logger.warning(
f"[auth] Rejecting X-Remote-User: client_host {client_host!r} is not loopback"
)
raise AuthenticationException(
message="X-Remote-User header is only trusted from the localhost proxy",
status_code=401,
headers={"WWW-Authenticate": "Bearer"},
code=AuthErrorCode.INVALID_TOKEN,
)
remote_user = request.headers.get("X-Remote-User")
if not remote_user:
raise AuthenticationException(
+9 -5
View File
@@ -40,9 +40,15 @@ from aarecommon.models.models import (
ZoomModel,
zoom_manager,
)
from aare.daq.config_model import LocalContactConfigModel
#TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
ABR_POS_MOUNT = AerotechCoordinate(
at_mm=Coordinate(x=0, y=0, z=0),
omega_deg=0
)
# TODO WHAT SHOULD THIS BE? This should be in the YAMl file it is beamline specific
ABR_POS_MOUNT = AerotechCoordinate(at_mm=Coordinate(x=0, y=0, z=0), omega_deg=0)
ABR_OMEGA_MOUNT = 0.0
@@ -1255,9 +1261,7 @@ class BeamlineConfig:
logger.warning(f"Failed to read Local Contact config from Redis: {e}")
return default
def set_local_contact_config(
self, config: LocalContactConfigModel | dict
) -> LocalContactConfigModel:
def set_local_contact_config(self, config: LocalContactConfigModel | dict) -> LocalContactConfigModel:
validated = LocalContactConfigModel.model_validate(config)
try:
redis_key = f"{self.__bl}:local_contact_config"
@@ -1279,7 +1283,7 @@ class BeamlineConfig:
if __name__ == "__main__":
from aarecommon.modelconfigs.beamline import mx_beamline
from aarecommon.config.beamline import mx_beamline
cfg = BeamlineConfig(bl=mx_beamline())
# cfg.allow_non_staff_request_from_staff = True
+40 -76
View File
@@ -1385,59 +1385,8 @@ class AareDAQ:
)
return None
# def _execute_rotation_sequence(self, rotation_request: RotationScanRequest) -> CompletedRotationScan | None:
# """
# Execute rotation scan.
#
# Args:
# rotation_request: Rotation scan parameters
#
# Returns:
# CompletedRotationScan result or None if failed
# """
# try:
# status= self.status
# if self.__cfg.simulated_detector:
# logger.info("Simulated detector mode enabled; skipping JFJoch start.")
# else:
# self.__jfjoch.measure_rotation(rotation_request, status, self.__cfg.xrf)
#
# self.__setup_datacollection(request=rotation_request)
# if self.sample is not None and self.sample.db_id is not None:
# self.__aare.send_sample_event(self.sample.db_id, SampleEventType.COLLECTING)
#
# self.__set_state(BeamlineStateEnum.DataCollection)
# result = self.__rotation(rotation_request)
# self.__set_state(BeamlineStateEnum.SampleAlignment)
# if self.sample is not None and self.sample.db_id is not None:
# self.save_screenshot_db(self.sample.db_id, "scan_preview")
# self.__aare.send_sample_event(self.sample.db_id, SampleEventType.COLLECTED)
# self.__aare.ingest_scan(sample=self.sample, result=result.result,
# geom=self.sample_geometry, beam_mark_pxl=self.__cfg.get_beam_mark(self.zoom))
# return result
# except JFJochCommunicationError as e:
# logger.error(f"Rotation sequence failed due to JFJoch Communication error: {e}")
# self._handle_operation_error(
# operation=DAQOperation.ROTATION,
# sample=self.sample,
# error=e,
# event_type=SampleEventType.COLLECTIONFAILED,
# additional_comment=f"JFJoch communication error: {e}"
# )
# raise
# except Exception as e:
# logger.error(f"Rotation sequence failed: {e}")
# self._handle_operation_error(
# operation=DAQOperation.ROTATION,
# sample=self.sample,
# error=e,
# event_type=SampleEventType.COLLECTIONFAILED
# )
# raise
def _execute_rotation_sequence(
self, rotation_request: RotationScanRequest
) -> CompletedRotationScan | None:
def _execute_rotation_sequence(self, rotation_request: RotationScanRequest) -> CompletedRotationScan | None:
"""
Execute rotation scan.
@@ -1447,6 +1396,10 @@ class AareDAQ:
Returns:
CompletedRotationScan result or None if failed
"""
min_exp_time = 0.0011
if rotation_request.exp_time_s < min_exp_time:
logger.warning(f"Exposure shorter than default of {min_exp_time} s! Adjusting.")
rotation_request.exp_time_s = min_exp_time
try:
return self._create_rotation_service().run(rotation_request)
except JFJochCommunicationError as e:
@@ -1620,7 +1573,20 @@ class AareDAQ:
self.last_time = end - start
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str | None]:
def _expand_macros(self, name: str) -> str:
name = name.replace('{date}', datetime.now().strftime('%Y%m%d'))
name = name.replace('{sample}', self.sample.sample_name)
name = name.replace('{CrystalName}', self.sample.sample_name)
name = name.replace('{puck}', self.sample.puck_name)
name = name.replace('{position}', f"{self.sample.pin:02d}")
name = name.replace('{sample_id}', f"{self.sample.db_id}")
name = name.replace('{beamline}', f"{self._beamline.value.lower()}")
name = name.replace('{prefix}', f"{self.sample.puck_name}/{self.sample.pin:02d}/{self.sample.sample_name}")
#TODO work out why clean_filename is removing slahses
#name = clean_filename(name)
return name
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str|None]:
file_prefix = None
if self.status.sample is None:
@@ -1635,9 +1601,6 @@ class AareDAQ:
if aaredb_params is None:
return None, file_prefix
# if aaredb_params.directory:
# file_prefix = aaredb_params.directory
if (
getattr(aaredb_params, "exposure", None) is None
and getattr(aaredb_params, "transmission", None) is None
@@ -1682,9 +1645,6 @@ class AareDAQ:
params.incr_omega_deg = default_osc
params.steps = round(abs(total) / default_osc)
# if file_prefix is not None:
# params.file_prefix = file_prefix
return params, file_prefix
@property
@@ -2016,12 +1976,10 @@ class AareDAQ:
screenshot_name = f"{sample_id}_before_raster"
else:
screenshot_name = f"{sample_id}_before_data_collection"
# self.auto_exposure()
# time.sleep(0.2)
self.save_screenshot_db(sample_id, screenshot_name)
if request.transmission is not None:
logger.info(f"requesting transmission to move to {request.transmission}")
if request.transmission is not None and request.transmission != self.__devs.transmission:
logger.info(f'requesting transmission to move to {request.transmission}')
self.__devs.transmission = request.transmission
start_pos = getattr(request, "start", None)
@@ -2059,9 +2017,6 @@ class AareDAQ:
)
self.__devs.smargon_wait(timeout=180)
# todo ADD TRANSMISSION
# if request.transmission is not None:
# self.__devs.transmission.wait()
return
def _build_fake_rotation_result(self, request: RotationScanRequest) -> CompletedRotationScan:
@@ -2576,13 +2531,8 @@ class AareDAQ:
logger.debug(f"spreadsheet_params: {spreadsheet_params}")
smart_params = self.__cfg.auto_params
default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2)
# if file_prefix is not None:
# default_params.file_prefix = file_prefix
# self.__aare.send_msg_to_db(self.sample,event_type=SampleEventType(''), comment=f'smart_params: {smart_params}')
if prefer_smart:
if smart_params:
# if file_prefix:
# smart_params.file_prefix = file_prefix
return smart_params, "smart_params"
if spreadsheet_params:
return spreadsheet_params, "spreadsheet_params"
@@ -2590,8 +2540,6 @@ class AareDAQ:
if spreadsheet_params:
return spreadsheet_params, "spreadsheet_params"
if smart_params:
# if file_prefix:
# smart_params.file_prefix = file_prefix
return smart_params, "smart_params"
return default_params, "defaults"
@@ -2750,6 +2698,10 @@ class AareDAQ:
raster_params = self.get_auto_raster_params()
geom = self.sample_geometry
if raster_params.filename is not None:
logger.info(f"Using filename {raster_params.filename}")
sample_prefix = "{filename}/{prefix}".format(filename=raster_params.filename, prefix=sample.sample_name)
raster_grid = RasterGridRequest(
exp_time_s=raster_params.exp_time_s,
file_prefix=sample_prefix + f"_{hex_string}",
@@ -2813,6 +2765,20 @@ class AareDAQ:
self.__devs.dtz = params.dtz
start_omega = self.omega
update_sample = self.sample
if (
update_sample is not None
and update_sample.db_id is not None
and update_sample.db_id == sample.db_id
):
logger.info(f"Updating sample info {sample.db_id} old run_number"
f" sample.run_number {sample.run_number} new run_number {update_sample.run_number}")
if params.filename is not None:
logger.info(f"Using filename {params.filename}")
sample_prefix = "{filename}/{prefix}".format(filename=params.filename, prefix=sample.sample_name)
rotation_request = RotationScanRequest(
start_omega_deg=start_omega,
dtz=params.dtz,
@@ -3526,9 +3492,7 @@ class AareDAQ:
finally:
self.__cfg.state_busy = False
def fluorimeter_take_spectrum(
self, fm: FluorescenceSpectrumParameterModel
) -> FluorescenceSpectrumOutputModel:
def fluorimeter_take_spectrum(self, fm: FluorescenceSpectrumParameterModel) -> FluorescenceSpectrumOutputModel:
self.__cfg.try_set_busy(timeout=360)
try:
+3 -3
View File
@@ -90,7 +90,7 @@ class BeamlineDevices:
self.__cryojet_temperature_set = PV(f"{BEAMLINE}-ES-CS:TEMP.VAL")
if self._beamline == MXBeamline.X06DA:
self.__transmission = SetGetPV(
self._transmission = SetGetPV(
name="transmission",
setpv=f"{BEAMLINE}-ES-BCFI:TRANSM-SET",
getpv=f"{BEAMLINE}-ES-BCFI:TRANSM-GET",
@@ -139,7 +139,7 @@ class BeamlineDevices:
# Transmission
@property
def transmission(self) -> float:
return self.__transmission.value
return self._transmission.value
@transmission.setter
def transmission(self, value: float):
@@ -147,7 +147,7 @@ class BeamlineDevices:
def set_transmission(self, value: float, /, wait: bool = True):
logger.warning("Setting Transmission is untested")
self.__transmission.move(value, wait=wait)
self._transmission.move(value, wait=wait)
# Lamp light
@property
@@ -130,16 +130,16 @@ def get_ml_bounding_box(
"target_point": prediction_result.target_point,
},
)
if filename is not None and bundle_image is not None:
upload_image(sample_id, f"{filename}_no_detection", bundle_image)
# if filename is not None and bundle_image is not None:
# upload_image(sample_id, f"{filename}_no_detection", bundle_image)
return None
x1, y1, x2, y2 = m.box.top_x, m.box.top_y, m.box.bottom_x, m.box.bottom_y
if filename is not None and bundle_image is not None:
annotated_image = bundle_image.copy()
cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
upload_image(sample_id, filename, annotated_image)
# if filename is not None and bundle_image is not None:
# annotated_image = bundle_image.copy()
# cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
# upload_image(sample_id, filename, annotated_image)
geom = sample_geometry
logger.info(
@@ -353,8 +353,6 @@ def build_ml_raster_plan(
"target_point": result.target_point,
},
)
if filename is not None and bundle_image is not None:
upload_image(sample_id, f"{filename}_no_detection", bundle_image)
return None
x1, y1, x2, y2 = (
@@ -380,11 +378,6 @@ def build_ml_raster_plan(
extra={"sample_id": sample_id, "ml_image_name": filename, "crystal_box": cbox},
)
if filename is not None and bundle_image is not None:
annotated_image = bundle_image.copy()
cv2.rectangle(annotated_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
upload_image(sample_id, filename, annotated_image)
grid_request = _box_to_raster_request(
x1=x1,
y1=y1,
@@ -25,10 +25,6 @@ class RotationService:
def _execute_scan(self, request: RotationScanRequest) -> CompletedRotationScan:
omega_start = self.ctx.deps.devs.aerotech_omega
if request.exp_time_s < 0.004:
self.logger.warning("Exposure time too short for PXII rotation scan")
request.exp_time_s = 0.004
total_time = request.exp_time_s * request.steps
sample = self.ctx.sample
+24 -7
View File
@@ -840,6 +840,28 @@ async def local_contact_set_config(
return daq.set_local_contact_config(payload)
@app.get("/local_contact/config")
async def local_contact_config(token: str = Depends(oauth2_scheme)) -> LocalContactConfigModel:
"""
Return Local Contact config values. Staff only.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
return daq.get_local_contact_config()
@app.put("/local_contact/config")
async def local_contact_set_config(
payload: LocalContactConfigModel, token: str = Depends(oauth2_scheme)
) -> LocalContactConfigModel:
"""
Update Local Contact config values. Staff only.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
return daq.set_local_contact_config(payload)
@app.post("/beamline/goto_abr_meas_pos")
async def goto_abr_meas_pos(token: str = Depends(oauth2_scheme)):
"""
@@ -2536,21 +2558,16 @@ async def maintenance(token: str = Depends(oauth2_scheme)) -> str:
def main():
# Remove in production!
# urllib3.disable_warnings()
# Run the application using uvicorn
uvicorn.run(
"aare.daq.server:app",
host="127.0.0.1",
port=5210,
workers=1,
workers=4,
proxy_headers=False,
log_config=get_uvicorn_logging_config(),
timeout_worker_healthcheck=30,
)
if __name__ == "__main__":
# start_image_stats_receiver(zmq_url="tcp://129.129.110.12:9089")
main()
# stop_image_stats_receiver()
+1 -23
View File
@@ -511,6 +511,7 @@ class BECClientWorker:
save_current_position(self.dev.aerotech, "work", axis="x")
save_current_position(self.dev.aerotech, "work", axis="y")
save_current_position(self.dev.aerotech, "work", axis="z")
self.save_config_and_reload_devices()
def save_config_and_reload_devices(self):
@@ -658,26 +659,3 @@ if __name__ == "__main__":
# except RuntimeError as e:
# print(f"RuntimeError: {e}")
client.shutdown_client()
#
# try:
# st = client.proc.run_macro("a2e", 160, "iln", queue="test")
# print(st)
# st.wait()
# print(st)
# status_1 = scans.umv(bec_dev.bs_x, -1.0, bec_dev.bs_y, -1.0, relative = True) # blocking
# print(
# f"Moved to position {bec_dev.bs_x.position} with status {status.status}"
# )
# status = scans.mv(bec_dev.bs_x, 1.0, bec_dev.bs_y, 1.0, relative=True) # none blocking
# status.wait()
# print(
# f"Moved to position {bec_dev.bs_x.position} with status {status.status}"
# )
# except Exception as e:
# print(f"Error: {e}")
#
# client.shutdown()
# backend wont work unless bec server will work, frontend anywehre with user access
+5 -1
View File
@@ -120,7 +120,11 @@ class PShellTellBackend:
if e.response is not None and e.response.text:
msg = e.response.text.strip()
raise TellCommunicationError(
msg, base_url=self._url, endpoint=endpoint, operation=operation, critical=True
msg,
base_url=self._url,
endpoint=endpoint,
operation=operation,
critical=True,
) from e
def eval(self, expr: str):
Executable → Regular
+1 -3
View File
@@ -359,9 +359,7 @@ class TellClient:
raise
except Exception as e:
logger.error(f"Exception occurred: {e}")
raise TellCommunicationError(
message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True
)
raise TellCommunicationError(message=f"Error during mount {segment}{puck}-{sample}: {e}", critical=True)
def unmount(self, force=False, wait=False, timeout=360.0):
if self.is_busy():
Regular → Executable
View File
+10 -5
View File
@@ -2,6 +2,7 @@ import os
from datetime import datetime
from pathlib import Path
from aarecommon.config.beamline import mx_beamline
from aarecommon.models.models import DAQStatusModel, SampleShortInfo
from PySide6.QtCore import Qt, Signal, Slot
from PySide6.QtWidgets import QGridLayout, QLabel, QLineEdit, QMessageBox, QSpinBox, QWidget
@@ -28,6 +29,7 @@ class FilePathPanel(QWidget):
self.__puck_name = "Manual"
self.__puck_pos = 0
self.__curr_pgroup = "p11206"
self.__beamline = mx_beamline().value.lower()
self.__filename = ""
self.__scan_kind = "raster" # default: "rotation" | "screening" | "raster"
@@ -84,11 +86,14 @@ class FilePathPanel(QWidget):
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}")
name = name.replace('{date}', self.__formatted_date)
name = name.replace('{sample}', self.__sample_name)
name = name.replace('{CrystalName}', self.__sample_name)
name = name.replace('{puck}', self.__puck_name)
name = name.replace('{position}', f"{self.__puck_pos:02d}")
name = name.replace('{sample_id}', f"{self.__sample_id}")
name = name.replace('{beamline}', f"{self.__beamline}")
name = name.replace('{prefix}', f"{self.__puck_name}/{self.__puck_pos:02d}/{self.__sample_name}")
return name
def _effective_dataset_base(self, base_no_run: str) -> str:
+4 -15
View File
@@ -22,7 +22,7 @@ from PySide6.QtWidgets import (
QTabWidget,
QTextEdit,
QVBoxLayout,
QWidget,
QWidget,
)
from aare.gui.constants import LOGGER_NAME
@@ -333,22 +333,11 @@ class LocalContactPanel(QFrame):
tools_layout.addWidget(tools)
tools_layout.addWidget(
self._make_button(
"Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros."
)
)
self._make_button("Load BEC user macros", self._daq.bec_load_user_macros, "Loading BEC user macros."))
tools_layout.addWidget(
self._make_button(
"Show BEC user macros",
self._daq.bec_list_all_user_macros,
"Listing BEC user macros.",
)
)
self._make_button("Show BEC user macros", self._daq.bec_list_all_user_macros, "Listing BEC user macros."))
tools_layout.addWidget(
self._make_button(
"Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices."
)
)
self._make_button("Show BEC position devices", self._daq.bec_list_all_devices, "Listing BEC devices."))
tools_layout.addWidget(
self._make_button(
"Reinitialise BEC planner/devices",
@@ -3,6 +3,7 @@ from aarecommon.models.rotation_scan import CompletedRotationScan
from PySide6.QtCore import QObject, Signal, Slot
class RotationScanManager(QObject):
file_ready = Signal(str, int)
+1 -1
View File
@@ -8,7 +8,7 @@ from aare.gui.constants import LOGGER_NAME
logger = setup_logger(LOGGER_NAME)
LOW_CURRENT_THRESHOLD = 100.0
LOW_CURRENT_THRESHOLD = 360.0
SNOOZE_SECONDS = 3600.0 # "Don't ask again for 1 hour"
-49
View File
@@ -1,49 +0,0 @@
# import sys
# from unittest.mock import MagicMock, patch
#
# from aarecommon.models.beamline import MXBeamline
# from aare.gui.gui import main
#
#
# @patch("aare.gui.gui.sys.exit")
# @patch("aare.gui.gui.cfg_get")
# @patch("aare.gui.gui.mx_beamline")
# @patch("aare.gui.gui.auth")
# @patch("aare.gui.gui.MainWindow")
# @patch("aare.gui.gui.LoadingSplashScreen")
# @patch("aare.gui.gui.QtGui.QPixmap")
# @patch("aare.gui.gui.QtGui.QIcon")
# @patch("aare.gui.gui.QApplication")
# def test_gui_main_startup(
# mock_app,
# mock_qicon,
# mock_qpixmap,
# mock_splash_cls,
# mock_main_window,
# mock_auth,
# mock_mx_beamline,
# mock_cfg_get,
# mock_sys_exit,
# ):
# mock_mx_beamline.return_value = MXBeamline.X06DA
# mock_auth.return_value = "header.payload.signature"
# mock_cfg_get.side_effect = lambda key, default=None: default
#
# mock_app_instance = mock_app.return_value
# mock_app_instance.exec.return_value = 0
#
# mock_splash = mock_splash_cls.return_value
# mock_window = mock_main_window.return_value
#
# with patch.object(sys, "argv", ["gui.py"]):
# main()
#
# mock_app.assert_called_once_with(["gui.py"])
# mock_auth.assert_called_once()
# mock_main_window.assert_called_once()
# mock_window.show.assert_called_once()
#
# mock_splash.show.assert_called_once()
# mock_splash.finish.assert_called_once_with(mock_window)
#
# mock_sys_exit.assert_called_once_with(0)
+3 -2
View File
@@ -7,6 +7,7 @@ from aare.gui.widgets.message_box import (
experiment_hutch_shutter_check,
ring_current_auto_check,
)
import pytest
def test_reply_box(qtbot):
@@ -30,10 +31,10 @@ def test_timer_box_auto_accept(qtbot):
assert box.result() == QMessageBox.StandardButton.Yes
@pytest.mark.timeout(10)
def test_ring_current_low_check_ok(qtbot):
# Should return True immediately if current is high enough
assert ring_current_low_check(None, 200.0) is True
assert ring_current_low_check(None, 361.0) is True
def test_ring_current_low_check_low_yes(qtbot):