Feat/select meitians gridscan analysis #198

Open
perl_d wants to merge 3 commits from feat/select_meitians_gridscan_analysis into main
9 changed files with 373 additions and 304 deletions
+1 -3
View File
@@ -7,12 +7,10 @@ requires-python = ">=3.11"
dependencies = [
"uv",
"gunicorn",
# >=0.7: DataCollectionParameters.transmission is a 0-1 fraction, which
# the scan panels rely on (older releases held an int percentage).
"aarecommon>=0.7.3",
"pydantic>=2.11",
"numpy",
"jfjoch_client>=1.0.0rc165",
"jfjoch_client>=1.0.0rc166",
"pyJWT",
"pyzmq",
"opencv-python-headless",
+47 -63
View File
@@ -13,6 +13,7 @@ from aarecommon.math.coordinate import Coordinate
from aarecommon.math.find_xtal import compute_crystal_score_array
from aarecommon.math.sample_geometry import SampleGeometryModel
from aarecommon.models.beamline import MXBeamline
from aarecommon.models.gridscan_decision import GridScanDecision
from aarecommon.models.models import (
DAQStatusModel,
PuckLoadedInfo,
@@ -35,6 +36,7 @@ from aareDB import (
)
from aareDB import Detector as DetectorParameters
from jfjoch_client.models import ScanResult
from numpy.typing import NDArray
from pydantic import StrictInt
logger = setup_logger("aareDAQ")
@@ -303,20 +305,27 @@ class AareWrapper:
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
analysis_image_scores: list[float | None] | None,
geom: SampleGeometryModel,
com: CenterOfMassModel | None,
com: CenterOfMassModel,
beam_mark_pxl: tuple[float, float],
decision: GridScanDecision | None,
):
if sample is None:
logger.error("No sample for DB gridscan ingestion request!")
return
payload_model = self.format_gridscan_payload(
sample, raster_result, raster_request, geom, com, beam_mark_pxl
)
if payload_model is None:
return
payload = payload_model.model_dump()
payload = self.format_gridscan_payload(
sample,
raster_result,
raster_request,
geom,
com,
analysis_image_scores,
beam_mark_pxl,
decision,
).model_dump_json()
url = f"{self._host}/protected_router/gridscan_runner/ingest"
headers = {
@@ -327,14 +336,13 @@ class AareWrapper:
url,
auth=(os.getenv("AAREDB_USERNAME"), os.getenv("AAREDB_PASSWORD")),
headers=headers,
data=json.dumps(payload),
data=payload,
timeout=30,
verify=self._ssl_ca_cert,
cert=(self._cert_file, self._key_file),
)
response.raise_for_status()
logger.info(f"Response status code: {response.status_code}")
logger.info(f"AareDB gridscan ingestion response status code: {response.status_code}")
def format_gridscan_payload(
self,
@@ -342,64 +350,40 @@ class AareWrapper:
raster_result: ScanResult,
raster_request: RasterGridRequest,
geom: SampleGeometryModel,
com: CenterOfMassModel | None,
com: CenterOfMassModel,
analysis_image_scores: list[float | None] | None,
beam_mark_pxl: tuple[float, float],
) -> RasterPayloadModel | None:
decision: GridScanDecision | None,
) -> RasterPayloadModel:
cell_size_pxl = Coordinate(
x=raster_request.grid_size_mm.x / geom.pixel_in_mm,
y=raster_request.grid_size_mm.y / geom.pixel_in_mm,
)
try:
cell_size_pxl = Coordinate(
x=raster_request.grid_size_mm.x / geom.pixel_in_mm,
y=raster_request.grid_size_mm.y / geom.pixel_in_mm,
)
if raster_request.smargon_top_left is None:
beam_loc = geom.smargon.sh_mm
else:
beam_loc = raster_request.smargon_top_left.sh_mm
if raster_request.smargon_top_left is None:
beam_loc = geom.smargon.sh_mm
else:
beam_loc = raster_request.smargon_top_left.sh_mm
start_pxl = geom.smargon_to_picture(beam_loc)
start_pxl = geom.smargon_to_picture(beam_loc)
com_grid_pxl = com.get_com_pxl(raster_request, geom)
center_pxl = Coordinate(x=start_pxl.x + com_grid_pxl.x, y=start_pxl.y + com_grid_pxl.y)
if com:
com_grid_pxl = com.get_com_pxl(raster_request, geom)
x = start_pxl.x + com_grid_pxl.x
y = start_pxl.y + com_grid_pxl.y
center_pxl = Coordinate(x=x, y=y)
else:
center_pxl = None
try:
score_arr = compute_crystal_score_array(raster_result.images)
score = [
float(score_arr[img.nx, img.ny])
if img.nx is not None and img.ny is not None
else None
for img in raster_result.images
]
except Exception as e:
logger.warning(
f"raster score computation failed, sending null score: {e}", exc_info=True
)
score = None
payload = RasterPayloadModel(
request=raster_request,
result=raster_result,
sample_id=sample.db_id,
attach_image=True,
centre_of_mass=com,
raster_score=score, # TODO: Check whether AareDB needs to change for taking this input
start_pxl=start_pxl,
center_pxl=center_pxl,
cell_size_pxl=cell_size_pxl,
beam_mark_pxl=beam_mark_pxl,
beam_size_mm=geom.beam_size_mm,
)
return payload
except Exception as e:
logger.error(e)
raise
return RasterPayloadModel(
request=raster_request,
result=raster_result,
sample_id=sample.db_id,
attach_image=True,
centre_of_mass=com,
raster_score=analysis_image_scores,
start_pxl=start_pxl,
center_pxl=center_pxl,
cell_size_pxl=cell_size_pxl,
beam_mark_pxl=beam_mark_pxl,
beam_size_mm=geom.beam_size_mm,
decision=decision,
)
@log_timing(logger, "AareDB call")
def ingest_scan(
+10
View File
@@ -10,6 +10,7 @@ import numpy as np
import redis
import redis_lock
from aarecommon.config.beamline import cfg_get
from aarecommon.config.config_models import GridscanAnalysisMode
from aarecommon.config.logger import setup_logger
from aarecommon.errors.exception_handler import BeamlineBusyException
from aarecommon.math.coordinate import AerotechCoordinate, Coordinate
@@ -656,6 +657,15 @@ class BeamlineConfig:
self._client.set(f"{self._bl}:beam_size_x", data.x)
self._client.set(f"{self._bl}:beam_size_y", data.y)
@property
def gridscan_analysis_mode(self) -> GridscanAnalysisMode:
data = self._client.get(f"{self._bl}:gridscan_analysis_mode")
return GridscanAnalysisMode(data) if data is not None else GridscanAnalysisMode.FindXtal
@gridscan_analysis_mode.setter
def gridscan_analysis_mode(self, val: GridscanAnalysisMode) -> GridscanAnalysisMode:
return self._client.set(f"{self._bl}:gridscan_analysis_mode", str(val))
def _get_settings(self) -> BeamlineSettingsModel:
tmp = self._client.get(f"{self._bl}:settings")
if tmp is None:
+15 -2
View File
@@ -41,6 +41,7 @@ from aarecommon.errors.exception_handler import (
)
from aarecommon.math.coordinate import AerotechCoordinate, Coordinate, SmargonCoordinate
from aarecommon.math.diffraction_geometry import DiffractionGeometry
from aarecommon.math.find_xtal import CenterOfMassModel
from aarecommon.math.sample_geometry import SampleGeometryModel
from aarecommon.models.automation import (
AutomationProgress,
@@ -64,10 +65,11 @@ from aarecommon.models.models import (
SimpleScanParameters,
ZoomModeEnum,
)
from aarecommon.models.raster_grid import CompletedRasterGrid, RasterGridRequest
from aarecommon.models.raster_grid import CompletedRasterGrid, GridScanDecision, RasterGridRequest
from aarecommon.models.rotation_scan import CompletedRotationScan, RotationScanRequest
from aarecommon.models.tell import TellPhaseEnum, TellStateModel
from aareDB import SampleEventType
from jfjoch_client import ScanResult
from aare.beamline_dispatch.protocols import BeamlineDispatch
from aare.daq import workflows
@@ -214,15 +216,26 @@ class _DAQScanIngestor:
)
def ingest_gridscan(
self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl
self,
*,
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
analysis_image_scores: list[float | None] | None,
geom: SampleGeometryModel,
com: CenterOfMassModel,
beam_mark_pxl: tuple[float, float],
decision: GridScanDecision | None,
) -> None:
self._daq._aare.ingest_gridscan(
sample=sample,
raster_result=raster_result,
raster_request=raster_request,
analysis_image_scores=analysis_image_scores,
geom=geom,
com=com,
beam_mark_pxl=beam_mark_pxl,
decision=decision,
)
+27 -2
View File
@@ -1,6 +1,11 @@
from dataclasses import dataclass
from typing import Protocol
from aarecommon.math.find_xtal import CenterOfMassModel
from aarecommon.models.models import SampleGeometryModel, SampleShortInfo
from aarecommon.models.raster_grid import GridScanDecision, RasterGridRequest
from jfjoch_client import ScanResult
from aare.daq.config import BeamlineStateEnum
from aare.daq.operations.screenshot.service import ScreenshotService
@@ -29,7 +34,16 @@ class ScanIngestor(Protocol):
def ingest_scan(self, *, sample, result, geom, beam_mark_pxl) -> None: ...
def ingest_gridscan(
self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl
self,
*,
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
analysis_image_scores: list[float | None] | None,
geom: SampleGeometryModel,
com: CenterOfMassModel,
beam_mark_pxl: tuple[float, float],
decision: GridScanDecision | None,
) -> None: ...
@@ -87,15 +101,26 @@ class ScanIngestionService:
)
def ingest_gridscan(
self, *, sample, raster_result, raster_request, geom, com, beam_mark_pxl
self,
*,
sample: SampleShortInfo | None,
raster_result: ScanResult,
raster_request: RasterGridRequest,
analysis_image_scores: list[float | None] | None,
geom: SampleGeometryModel,
com: CenterOfMassModel,
beam_mark_pxl: tuple[float, float],
decision: GridScanDecision | None,
) -> None:
self.ingestor.ingest_gridscan(
sample=sample,
raster_result=raster_result,
raster_request=raster_request,
analysis_image_scores=analysis_image_scores,
geom=geom,
com=com,
beam_mark_pxl=beam_mark_pxl,
decision=decision,
)
+191 -234
View File
@@ -1,8 +1,10 @@
import copy
import re
import time
from math import ceil, floor
from aarecommon.config.beamline import cfg_get
from aarecommon.config.config_models import GridscanAnalysisMode
from aarecommon.config.logger_events import (
geom_log_context,
log_ml_bundle_meta,
@@ -13,22 +15,29 @@ from aarecommon.config.logger_events import (
from aarecommon.errors.exception_handler import RasterScanException
from aarecommon.math.coordinate import AerotechCoordinate, Coordinate, SmargonCoordinate
from aarecommon.math.find_xtal import (
center_of_grid,
compute_crystal_score_array,
get_best_b_factor,
get_best_res,
get_xtal_size,
raster_highest_score,
)
from aarecommon.math.jfjoch_gridscan_union import GridScanResult, Thresholds, analyse
from aarecommon.math.raster_grid import grid_to_image_id
from aarecommon.models.models import BeamlineStateEnum, MLBoxType
from aarecommon.models.raster_grid import (
CenterOfMassModel,
CompletedRasterGrid,
CompletedRasterGridElem,
RasterGridRequest,
)
from aareDB import SampleEventType
from bec_lib.redis_connector.streams import dataclass
from jfjoch_client import ScanResult
from jfjoch_client.exceptions import NotFoundException
from jfjoch_client.models.grid_scan import GridScan
from aare.daq.daq import DAQStatusModel
from aare.daq.mlbox import MLBoxPredictionResult
from aare.daq.operations.common.ml_bounding_box import (
MLRasterPlan,
@@ -36,13 +45,103 @@ from aare.daq.operations.common.ml_bounding_box import (
get_ml_bounding_box,
)
from aare.daq.operations.raster.models import RasterContext
from aare.devices.area_detector import AutoEnum
@dataclass
class _AnalysisResults:
center_smargon_coord: SmargonCoordinate
com: CenterOfMassModel
scores: list[float | None] | None
v2_results: GridScanResult | None = None
class RasterService:
def __init__(self, *, context: RasterContext, logger):
self.ctx = context
self.logger = logger
self.ingestor = self.ctx.deps.aare
self.send_sample_event = (
self.ctx.services.events.send
if self.ctx.services.events is not None
else self.ctx.deps.aare.send_sample_event
)
def _get_anaylsis_results(
self,
mode: GridscanAnalysisMode,
scan_result: ScanResult,
request: RasterGridRequest,
*,
daq: DAQStatusModel | None = None,
thresholds: Thresholds | None = None,
z_um: float | None = None,
):
if mode == GridscanAnalysisMode.FindXtal:
com = raster_highest_score(scan_result.images)
_scores = compute_crystal_score_array(scan_result.images)
scores = [
float(_scores[img.nx, img.ny])
if img.nx is not None and img.ny is not None
else None
for img in scan_result.images
]
smargon_coord = self._smargon_offset_from_scan_result(scan_result, request, com)
v2_results = None
else:
step_x_um = (request.grid_size_mm.x / request.n_x) * 1000
step_y_um = (request.grid_size_mm.y / request.n_y) * 1000
grid_scan = GridScan(
n_fast=request.n_x, step_x_um=step_x_um, step_y_um=step_y_um, snake=True
)
v2_results = analyse(scan_result, grid_scan, daq=daq, thresholds=thresholds, z_um=z_um)
self.logger.info(
f"experimental gridscan analaysis results: \n {v2_results.model_dump_json(indent=2)}"
)
if v2_results.centre is not None:
com = CenterOfMassModel(
n_x=v2_results.centre.nx,
n_y=v2_results.centre.ny,
max_image=v2_results.centre.image_number,
)
smargon_coord = SmargonCoordinate(
sh_mm=request.smargon_top_left.sh_mm + v2_results.centre.offset_mm(),
phi_deg=request.smargon_top_left.phi_deg,
chi_deg=request.smargon_top_left.chi_deg,
)
else:
com = center_of_grid(request.n_x, request.n_y)
smargon_coord = self._smargon_offset_from_scan_result(scan_result, request, com)
scores = None
return _AnalysisResults(smargon_coord, com, scores, v2_results)
def _smargon_offset_from_scan_result(
self, scan_result: ScanResult, request: RasterGridRequest, com: CenterOfMassModel
):
target_coor = com.get_com_mm(request)
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(target_coor)
self.logger.info(
"Calculated raster centre offset",
extra=merge_log_context(
sample_log_context(self.ctx.sample),
raster_request_log_context(request),
{
"centre_offset_x_mm": target_coor_offset.x,
"centre_offset_y_mm": target_coor_offset.y,
"centre_offset_z_mm": target_coor_offset.z,
"grid_half_width_x_mm": x,
"grid_half_height_y_mm": y,
"top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None),
"top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None),
"top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None),
},
),
)
self.logger.info(f"moving Smargon to grid centre offset {target_coor_offset}")
return SmargonCoordinate(
sh_mm=request.smargon_top_left.sh_mm + target_coor_offset,
phi_deg=request.smargon_top_left.phi_deg,
chi_deg=request.smargon_top_left.chi_deg,
)
@staticmethod
def _scan_result_image_count(scan_result) -> int | None:
@@ -165,61 +264,6 @@ class RasterService:
x1, y1, x2, y2 = box
return x1 <= margin or y1 <= margin or x2 >= width - margin or y2 >= height - margin
def _zoom_to_fit_box(self, plan: MLRasterPlan) -> tuple | None:
"""Pick the box that drives zoom-to-fit: loop_all if detected and not
clipped at the frame edge, otherwise loop_face."""
if plan.loop_all_box is not None and not self._box_touches_frame_edge(
plan.loop_all_box, plan.image_width, plan.image_height
):
return plan.loop_all_box
return plan.loop_face_box
def _apply_zoom_to_fit(self, plan: MLRasterPlan) -> None:
"""Zoom so the chosen loop box covers at most 1/3 of the frame."""
box = self._zoom_to_fit_box(plan)
if box is None or not plan.image_width or not plan.image_height:
return
x1, y1, x2, y2 = box
box_px = max(x2 - x1, y2 - y1)
if box_px <= 0:
return
screen_px = min(plan.image_width, plan.image_height)
current_zoom = self.ctx.deps.devs.zoom
pixel_in_mm_now = self.ctx.deps.cfg.pixel_to_mm(current_zoom)
box_mm = box_px * pixel_in_mm_now
# Target mm-per-pixel so the box spans exactly screen/3.
target_pixel_in_mm = 3.0 * box_mm / screen_px
try:
target_zoom = self.ctx.deps.cfg.zoom_for_pixel_to_mm(target_pixel_in_mm)
except (ValueError, ZeroDivisionError) as e:
self.logger.warning(f"Skipping auto-center zoom-to-fit: {e}")
return
zoom_min = float(cfg_get("daq.hardware.zoom_min", 1.0))
zoom_max = float(cfg_get("daq.hardware.zoom_max", 1000.0))
target_zoom = max(zoom_min, min(zoom_max, target_zoom))
self.logger.info(
"Auto-center zoom-to-fit",
extra=merge_log_context(
sample_log_context(self.ctx.sample),
{
"box_px": box_px,
"screen_px": screen_px,
"current_zoom": current_zoom,
"target_zoom": target_zoom,
"used_loop_all": box is plan.loop_all_box,
},
),
)
self.ctx.deps.devs.samcam_auto(AutoEnum.AUTO)
self.ctx.deps.devs.set_zoom(target_zoom, wait=True)
time.sleep(0.2)
self.ctx.deps.devs.samcam_auto(AutoEnum.ONCE)
def auto_center_line_scan_top_left(
self,
*,
@@ -386,17 +430,72 @@ class RasterService:
return top_left, n_y
def execute(
self, request: RasterGridRequest, wait_for_screenshot: float | None = None
) -> CompletedRasterGridElem:
def _do_gridscan_move_on_aerotech(self, request: RasterGridRequest, expected_time: float):
self.ctx.deps.devs.set_dtz(request.dtz, wait=True)
self.ctx.deps.devs.aerotech.grid_scan(
grid_elem_size_y_um=request.grid_size_mm.y * 1000,
grid_elem_size_x_um=request.grid_size_mm.x * 1000,
grid_elem_count_x=request.n_x,
grid_elem_count_y=request.n_y,
time_sec=request.exp_time_s,
run_async=True,
)
self.ctx.deps.devs.aerotech.wait_till_done(
timeout=int(round(expected_time + expected_time * 0.1 + 60, 0))
)
if isinstance(self.ctx.deps.cfg.abr_meas_pos, Coordinate):
coord = self.ctx.deps.cfg.abr_meas_pos
else:
coord = self.ctx.deps.cfg.abr_meas_pos.at_mm
self.ctx.deps.devs.aerotech_pos = AerotechCoordinate(
at_mm=coord, omega_deg=self.ctx.deps.devs.aerotech_omega
)
self.ctx.deps.devs.aerotech.wait_till_done(timeout=60)
def _check_setup(self, request: RasterGridRequest, wait_for_screenshot_s: float | None):
total_time = request.exp_time_s * request.n_x * request.n_y + request.n_y * 0.3
if wait_for_screenshot is None:
wait_for_screenshot = self.ctx.settings.wait_for_screenshot_s
if wait_for_screenshot_s is None:
wait_for_screenshot_s = self.ctx.settings.wait_for_screenshot_s
if total_time > 1200:
raise RasterScanException(f"Raster scan is too long {total_time}s > 20min")
return total_time, wait_for_screenshot_s
def _initial_gridscan_ingestion(
self,
request: RasterGridRequest,
scan_result: ScanResult,
wait_for_screenshot_s: float,
sample_id: int,
):
self.logger.debug("moving to XtalSnapshot to take a screenshot of the sample")
if self.ctx.services.state is None:
raise RuntimeError("RasterService requires services.state")
self.ctx.services.state.set_state(BeamlineStateEnum.XtalSnapshot)
time.sleep(wait_for_screenshot_s)
self.ctx.services.screenshots.save_to_db(
sample_id, f"{sample_id}_post_raster_{int(request.omega_deg)}deg"
)
self.ingestor.ingest_gridscan(
sample=self.ctx.sample,
raster_result=scan_result,
raster_request=request,
geom=self.ctx.sample_geometry,
com=None,
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
)
def _additional_analysis_data_ingestion(self, results: GridScanResult): ...
def execute(
self, request: RasterGridRequest, wait_for_screenshot: float | None = None
) -> CompletedRasterGridElem:
total_time, wait_for_screenshot = self._check_setup(request, wait_for_screenshot)
status = self.ctx.status
smargon_top_left = request.smargon_top_left
@@ -421,43 +520,11 @@ class RasterService:
if self.ctx.sample is not None and self.ctx.sample.db_id is not None:
self.ctx.deps.aare.create_gridscan_run(self.ctx.sample, request, status)
if not self.ctx.deps.cfg.simulated_detector:
self.ctx.deps.jfjoch.wait_till_running(timeout=60.0)
else:
self.logger.info("Simulated detector mode enabled; faking jfjoch intilalisation.")
self.ctx.deps.jfjoch.wait_till_running(timeout=60.0)
self.logger.debug(
f"Starting grid scan with {request.n_x}x{request.n_y} points, exp time {request.exp_time_s}s"
)
if not self.ctx.deps.cfg.simulated_detector:
# The detector move (sa2dc) is non-blocking; ensure it has reached
# the requested distance before triggering the scan, so data is
# collected at the dtz already reported to JFJoch/DB.
self.ctx.deps.devs.set_dtz(request.dtz, wait=True)
self.ctx.deps.devs.aerotech.grid_scan(
grid_elem_size_y_um=request.grid_size_mm.y * 1000,
grid_elem_size_x_um=request.grid_size_mm.x * 1000,
grid_elem_count_x=request.n_x,
grid_elem_count_y=request.n_y,
time_sec=request.exp_time_s,
run_async=True,
)
self.ctx.deps.devs.aerotech.wait_till_done(
timeout=int(round(total_time + total_time * 0.1 + 60, 0))
)
if isinstance(self.ctx.deps.cfg.abr_meas_pos, Coordinate):
coord = self.ctx.deps.cfg.abr_meas_pos
else:
coord = self.ctx.deps.cfg.abr_meas_pos.at_mm
self.ctx.deps.devs.aerotech_pos = AerotechCoordinate(
at_mm=coord, omega_deg=self.ctx.deps.devs.aerotech_omega
)
self.ctx.deps.devs.aerotech.wait_till_done(timeout=60)
x = None
y = None
self._do_gridscan_move_on_aerotech(request, total_time)
scan_result = self.ctx.deps.jfjoch.wait_till_done(60)
if scan_result is None:
@@ -474,64 +541,10 @@ class RasterService:
self.logger.info(
f"Raster scan results from JFJoch: file {scan_result.file_prefix} with images {scan_result.images}"
)
com = raster_highest_score(scan_result.images)
if com is None:
self.logger.info("Calcualted COM is None -> using centre image")
if request.n_x == 1:
x = request.grid_size_mm.x / 2.0
else:
x = ((request.n_x - 1) * request.grid_size_mm.x) / 2.0
y = ((request.n_y - 1) * request.grid_size_mm.y) / 2.0
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(Coordinate(x=x, y=y))
self.logger.info(
"Calculated raster centre offset",
extra=merge_log_context(
sample_log_context(self.ctx.sample),
raster_request_log_context(request),
{
"centre_offset_x_mm": target_coor_offset.x,
"centre_offset_y_mm": target_coor_offset.y,
"centre_offset_z_mm": target_coor_offset.z,
"grid_half_width_x_mm": x,
"grid_half_height_y_mm": y,
"top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None),
"top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None),
"top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None),
},
),
)
else:
self.logger.info("Calcualted COM is not None, proceeding")
target_coor = com.get_com_mm(request)
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(target_coor)
self.logger.info(
"Calculated raster centre offset",
extra=merge_log_context(
sample_log_context(self.ctx.sample),
raster_request_log_context(request),
{
"centre_offset_x_mm": target_coor_offset.x,
"centre_offset_y_mm": target_coor_offset.y,
"centre_offset_z_mm": target_coor_offset.z,
"grid_half_width_x_mm": x,
"grid_half_height_y_mm": y,
"top_left_x_mm": getattr(request.smargon_top_left.sh_mm, "x", None),
"top_left_y_mm": getattr(request.smargon_top_left.sh_mm, "y", None),
"top_left_z_mm": getattr(request.smargon_top_left.sh_mm, "z", None),
},
),
)
self.logger.info(f"moving Smargon to grid centre offset {target_coor_offset}")
target_smargon = SmargonCoordinate(
sh_mm=request.smargon_top_left.sh_mm + target_coor_offset,
phi_deg=request.smargon_top_left.phi_deg,
chi_deg=request.smargon_top_left.chi_deg,
analysis_results = self._get_anaylsis_results(
self.ctx.deps.cfg.gridscan_analysis_mode, scan_result, request, daq=self.ctx.status
)
self.ctx.deps.devs.smargon_pos = target_smargon
self.ctx.deps.devs.smargon_pos = analysis_results.center_smargon_coord
self.ctx.deps.devs.smargon_wait(timeout=180)
self.logger.info(
@@ -555,68 +568,30 @@ class RasterService:
else None
)
if sample_id:
self.logger.debug("moving to XtalSnapshot to take a screenshot of the sample")
if self.ctx.services.state is None:
raise RuntimeError("RasterService requires services.state")
self.ctx.services.state.set_state(BeamlineStateEnum.XtalSnapshot)
self._initial_gridscan_ingestion(
request, scan_result, wait_for_screenshot, sample_id
)
if analysis_results.v2_results is not None:
self._additional_analysis_data_ingestion(analysis_results.v2_results)
if wait_for_screenshot and wait_for_screenshot > 0:
time.sleep(wait_for_screenshot)
self.ctx.services.screenshots.save_to_db(
sample_id, f"{sample_id}_post_raster_{int(request.omega_deg)}deg"
diffraction_image_id = analysis_results.com.max_image
diffraction_image_filename = (
f"{sample_id}_best_diffraction_from_raster_image_{diffraction_image_id}"
)
if self.ctx.services.ingestion is not None:
self.ctx.services.ingestion.ingest_gridscan(
sample=self.ctx.sample,
raster_result=scan_result,
raster_request=request,
geom=self.ctx.sample_geometry,
com=None,
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
)
else:
self.ctx.deps.aare.ingest_gridscan(
sample=self.ctx.sample,
raster_result=scan_result,
raster_request=request,
geom=self.ctx.sample_geometry,
com=None,
beam_mark_pxl=self.ctx.deps.cfg.get_beam_mark(self.ctx.deps.devs.zoom),
)
if com is not None and com.max_image is not None:
diffraction_image_filename = f"{self.ctx.sample.db_id}_best_diffraction_from_raster_image_{com.max_image}"
diffraction_image_id = com.max_image
else:
diffraction_image_filename = (
f"{self.ctx.sample.db_id}_diffraction_image_near_grid_scan"
)
diffraction_image_id = self._grid_image_id_from_centre_offset(
x_mm=x, y_mm=y, request=request
)
self._upload_raster_diffraction_preview(
sample_id=self.ctx.sample.db_id,
sample_id=sample_id,
filename=diffraction_image_filename,
image_id=diffraction_image_id,
scan_result=scan_result,
request=request,
)
if self.ctx.services.events is not None:
self.ctx.services.events.send(
self.ctx.sample.db_id,
SampleEventType.RASTERED,
comment=f"Raster completed at {request.omega_deg:.1f} deg",
)
else:
self.ctx.deps.aare.send_sample_event(
self.ctx.sample.db_id,
event_type=SampleEventType.RASTERED,
comment=f"Raster completed at {request.omega_deg:.1f} deg",
)
self.send_sample_event(
sample_id,
SampleEventType.RASTERED,
comment=f"Raster completed at {request.omega_deg:.1f} deg",
)
self.logger.info(
"Raster finished",
@@ -671,18 +646,9 @@ class RasterService:
),
)
if self.ctx.services.events is not None:
self.ctx.services.events.send(
sample.db_id,
SampleEventType.RASTERING,
comment=f"Raster at {geom.omega_deg:.1f} deg",
)
else:
self.ctx.deps.aare.send_sample_event(
sample.db_id,
SampleEventType.RASTERING,
comment=f"Raster at {geom.omega_deg:.1f} deg",
)
self.send_sample_event(
sample.db_id, SampleEventType.RASTERING, comment=f"Raster at {geom.omega_deg:.1f} deg"
)
plan = self.ml_raster_plan(sample.db_id, f"ml_{geom.omega_deg:.2f}deg")
@@ -773,18 +739,12 @@ class RasterService:
res1 = self.execute(grid)
grid.omega_deg += 90
if self.ctx.services.events is not None:
self.ctx.services.events.send(
self.ctx.sample.db_id,
SampleEventType.RASTERING,
comment=f"Raster at {geom.omega_deg:.1f} deg",
)
else:
self.ctx.deps.aare.send_sample_event(
self.ctx.sample.db_id,
SampleEventType.RASTERING,
comment=f"Raster at {geom.omega_deg:.1f} deg",
)
self.send_sample_event(
self.ctx.sample.db_id,
SampleEventType.RASTERING,
comment=f"Raster at {geom.omega_deg:.1f} deg",
)
self.ctx.deps.devs.aerotech_omega = grid.omega_deg
grid.n_x = 1
@@ -813,12 +773,9 @@ class RasterService:
)
status = self.ctx.status
if not self.ctx.deps.cfg.simulated_detector:
self.logger.info(f"initialise detector for raster at {grid.omega_deg}")
self.ctx.deps.jfjoch.measure_raster(grid, status)
self.logger.info("detector initialised")
else:
self.logger.info("Simulated detector mode enabled; using fake raster result.")
self.logger.info(f"initialise detector for raster at {grid.omega_deg}")
self.ctx.deps.jfjoch.measure_raster(grid, status)
self.logger.info("detector initialised")
if self.ctx.services.state is None:
raise RuntimeError("RasterService requires services.state")
+34
View File
@@ -10,6 +10,7 @@ from typing import Any, ClassVar
import uvicorn
from aarecommon.config.beamline import mx_beamline
from aarecommon.config.config_models import GridscanAnalysisMode
from aarecommon.config.logger import get_uvicorn_logging_config, setup_logger
from aarecommon.errors.codes import AareErrorCode, export_error_codes_grouped
from aarecommon.errors.exception_handler import (
@@ -846,6 +847,39 @@ async def local_contact_restart_device(device: str, token: str = Depends(oauth2_
return result
@app.get("/local_contact/gridscan_analysis_mode")
async def get_gridscan_mode(token: str = Depends(oauth2_scheme)) -> GridscanAnalysisMode:
"""
Get the gridscan analysis mode
Args:
token: OAuth2 access token.
Returns:
Dictionary with status and message.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
return cfg.gridscan_analysis_mode
@app.put("/local_contact/gridscan_analysis_mode")
async def set_gridscan_mode(val: GridscanAnalysisMode, token: str = Depends(oauth2_scheme)) -> str:
"""
Set the gridscan analysis mode
Args:
token: OAuth2 access token.
Returns:
Dictionary with status and message.
"""
data = auth.parse_token(token)
auth.check_jwt_staff_only(data)
cfg.gridscan_analysis_mode = val
return "OK"
@app.post("/local_contact/resync/detector_metadata")
async def local_contact_resync_detector_metadata(token: str = Depends(oauth2_scheme)) -> dict:
"""
@@ -3,6 +3,7 @@ from __future__ import annotations
from collections.abc import Callable
from typing import ClassVar
from aarecommon.config.config_models import GridscanAnalysisMode
from aarecommon.config.logger import setup_logger
from aarecommon.models.models import DAQStatusModel
from PySide6.QtCore import QUrl, Slot
@@ -616,8 +617,34 @@ class LocalContactPanel(QFrame):
button_layout.addWidget(reload_button)
button_layout.addStretch(1)
analysis_settings = QGroupBox("Analysis settings", tab)
analysis_settings_layout = QGridLayout(analysis_settings)
form_layout.setContentsMargins(10, 12, 10, 10)
form_layout.setHorizontalSpacing(8)
form_layout.setVerticalSpacing(8)
analysis_choice_label = QLabel("Gridscan analysis mode:", analysis_settings)
find_xtal_button = QPushButton('classic "find_xtal.py"', analysis_settings)
jfj_gs_u_button = QPushButton('Meitian\'s "jfjoch_gridscan_union.py"', analysis_settings)
current_label = QLabel("Current:", analysis_settings)
current_value_label = QLabel("Unknown/default", analysis_settings)
analysis_settings_layout.addWidget(analysis_choice_label, 1, 0)
analysis_settings_layout.addWidget(find_xtal_button, 2, 0)
analysis_settings_layout.addWidget(jfj_gs_u_button, 2, 1)
analysis_settings_layout.addWidget(current_label, 2, 2)
analysis_settings_layout.addWidget(current_value_label, 2, 3)
find_xtal_button.clicked.connect(
lambda: self._daq.set_gridscan_analysis_mode(GridscanAnalysisMode.FindXtal)
)
jfj_gs_u_button.clicked.connect(
lambda: self._daq.set_gridscan_analysis_mode(GridscanAnalysisMode.JfjochGridscanUnion)
)
self._daq.gridscan_analysis_mode.connect(current_value_label.setText)
self._daq.refresh_gridscan_analysis_mode()
layout.addWidget(form_box)
layout.addWidget(button_row)
layout.addWidget(analysis_settings)
layout.addStretch(1)
return tab
+21
View File
@@ -10,6 +10,7 @@ from dataclasses import dataclass
from datetime import datetime
from typing import ClassVar, Literal, cast
from aarecommon.config.config_models import GridscanAnalysisMode
from aarecommon.config.logger import setup_logger
from aarecommon.errors.codes import AareErrorCode, AuthErrorCode, export_error_codes
from aarecommon.math.coordinate import AerotechCoordinate, SmargonCoordinate
@@ -136,6 +137,8 @@ class DAQWorker(QObject):
steer_beam_available = Signal(bool)
gridscan_analysis_mode = Signal(str)
def __init__(self, base_url: str | None, token: str, parent=None):
"""
Initialize the DAQWorker.
@@ -1694,6 +1697,24 @@ class DAQWorker(QObject):
logger.exception(message)
self.local_contact_transfer_error.emit(message)
def refresh_gridscan_analysis_mode(self):
request = QNetworkRequest(QUrl(f"{self._base_url}/local_contact/gridscan_analysis_mode"))
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
reply = self._net_manager.get(request)
reply.finished.connect(
lambda: self.gridscan_analysis_mode.emit(self.handle_response(reply))
)
def set_gridscan_analysis_mode(self, value: GridscanAnalysisMode):
request = QNetworkRequest(
QUrl(f"{self._base_url}/local_contact/gridscan_analysis_mode?val={value!s}")
)
request.setRawHeader(b"Authorization", f"Bearer {self._token}".encode())
request.setRawHeader(b"Content-Type", b"application/json")
body = QByteArray(str(value).encode("utf-8"))
reply = self._net_manager.put(request, body)
reply.finished.connect(lambda: self.refresh_gridscan_analysis_mode())
@Slot(str, bool)
def set_local_contact_simulation(self, device: str, enabled: bool):
self.generic_post(f"local_contact/simulate/{device}?enabled={str(enabled).lower()}")