Live changes from early July, PX-III #111

Merged
perl_d merged 1 commits from pxiii-live-changes into main 2026-07-27 10:33:43 +02:00
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
Outdated
Review

this needs fixing per beamline

this needs fixing per beamline
Outdated
Review

leaving for feature with beamline specific dispatch

leaving for feature with beamline specific dispatch
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}"
Outdated
Review

why is this different from the one in daq

why is this different from the one in daq
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):