From 3b39bb2144df5542eccaf6a7e05b58cba18fa11a Mon Sep 17 00:00:00 2001 From: appleb_m Date: Fri, 24 Apr 2026 14:19:36 +0200 Subject: [PATCH] raster: now do auto_centre_line_scan based off of target_y instead of just above and below beam cente, if target avaialble, now submit iamges to databse, reduced number of significant figures inr aster message to db, grid_to_image and iamge_to_grid fucntions --- src/aare/common/raster_grid.py | 49 ++++++++ src/aare/daq/daq.py | 210 ++++++++++++++++++++++++++++++--- 2 files changed, 244 insertions(+), 15 deletions(-) diff --git a/src/aare/common/raster_grid.py b/src/aare/common/raster_grid.py index 7c4327ff..082141c5 100644 --- a/src/aare/common/raster_grid.py +++ b/src/aare/common/raster_grid.py @@ -88,3 +88,52 @@ class RasterPayloadModel(BaseModel): beam_mark_pxl: tuple[float, float] beam_size_mm: Annotated[Coordinate, AfterValidator(positive_coords)] +def grid_to_image_id(grid_x: int, grid_y: int, number_of_cols: int) -> int: + """ + Get the associated image id for a given grid position (grid_x, grid_y) + using serpentine row ordering. + + grid_x: 1-based column index + grid_y: 1-based row index + number_of_cols: total number of columns in the grid + """ + if number_of_cols < 1: + raise ValueError("number_of_cols must be >= 1") + if grid_x < 1: + raise ValueError("grid_x must be >= 1") + if grid_y < 1: + raise ValueError("grid_y must be >= 1") + if grid_x > number_of_cols: + raise ValueError("grid_x cannot be greater than number_of_cols") + + col = grid_x - 1 + row = grid_y - 1 + + if row % 2 == 0: # left -> right + return row * number_of_cols + col + else: # right -> left + return row * number_of_cols + (number_of_cols - 1 - col) + + +def image_id_to_grid(image_id: int, number_of_cols: int) -> tuple[int, int]: + """ + Get the associated grid position (grid_x, grid_y) for a given image id + using serpentine row ordering. + """ + if number_of_cols < 1: + raise ValueError("number_of_cols must be >= 1") + if image_id < 0: + raise ValueError("image_id must be >= 0") + + row = image_id // number_of_cols + pos_in_row = image_id % number_of_cols + + if row % 2 == 0: # left -> right + col = pos_in_row + else: # right -> left + col = number_of_cols - 1 - pos_in_row + + grid_x = col + 1 + grid_y = row + 1 + + return grid_x, grid_y \ No newline at end of file diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 7db37005..f73a3fc0 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -4,7 +4,7 @@ import time import traceback from datetime import datetime -from math import ceil +from math import ceil, floor from pathlib import Path from typing import List, Tuple, Optional, Callable @@ -39,7 +39,7 @@ from aare.common.automation_models import ( StepStatus, WorkflowStateKind, ) -from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid, CompletedRasterGridElem +from aare.common.raster_grid import RasterGridRequest, CompletedRasterGrid, CompletedRasterGridElem, grid_to_image_id from aare.common.rotation_scan import RotationScanRequest, CompletedRotationScan from aare.common.sample_geometry import SampleGeometryModel from aare.daq.spreadsheetupdater import beamline @@ -1090,26 +1090,33 @@ class AareDAQ: self.__set_state(BeamlineStateEnum.DataCollection) grid.n_x = 1 - # TODO generate y scan rather than had code for 1x50 - grid.n_y = 50 grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg" - grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25) - offset = Coordinate(x=-grid.grid_size_mm.x / 2.0, y=-(grid.n_y + 0.5) * grid.grid_size_mm.y / 2.0) geom = self.sample_geometry - logger.debug(f"set offset y scan {offset}") - grid.smargon_top_left = geom.translate_smargon(offset) + grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25) + grid.smargon_top_left, grid.n_y = self._auto_center_line_scan_top_left( + omega_deg=grid.omega_deg, + file_prefix=grid.file_prefix, + grid_size_mm=grid.grid_size_mm, + default_n_y=50, + y_retarget_threshold_mm=max( + geom.beam_size_mm.y * 2.0, + grid.grid_size_mm.y * 4.0, + ), + y_padding_fraction_each_side=0.10, + ) logger.info( "Prepared second auto-center raster", extra={ "file_prefix": grid.file_prefix, "omega_deg": grid.omega_deg, - "offset_x_mm": offset.x, - "offset_y_mm": offset.y, - "offset_z_mm": offset.z, "top_left_x_mm": getattr(grid.smargon_top_left.sh_mm, "x", None), "top_left_y_mm": getattr(grid.smargon_top_left.sh_mm, "y", None), "top_left_z_mm": getattr(grid.smargon_top_left.sh_mm, "z", None), + "n_x": grid.n_x, + "n_y": grid.n_y, + "grid_size_x_mm": getattr(grid.grid_size_mm, "x", None), + "grid_size_y_mm": getattr(grid.grid_size_mm, "y", None), }, ) @@ -1241,6 +1248,30 @@ class AareDAQ: centre_of_mass=None, ) + @staticmethod + def _grid_image_id_from_centre_offset( + *, + x_mm: float, + y_mm: float, + request: RasterGridRequest, + ) -> int: + if request.grid_size_mm.x <= 0 or request.grid_size_mm.y <= 0: + raise ValueError("grid_size_mm must be positive") + if request.n_x < 1 or request.n_y < 1: + raise ValueError("Raster grid dimensions must be >= 1") + + grid_x = floor(x_mm / request.grid_size_mm.x) + 1 + grid_y = floor(y_mm / request.grid_size_mm.y) + 1 + + grid_x = min(max(1, grid_x), request.n_x) + grid_y = min(max(1, grid_y), request.n_y) + + return grid_to_image_id( + grid_x=grid_x, + grid_y=grid_y, + number_of_cols=request.n_x, + ) + def __raster(self, request: RasterGridRequest, wait_for_screenshot: float | None = 0.2) -> CompletedRasterGridElem: smargon_top_left =request.smargon_top_left self.__devs.set_smargon_pos( @@ -1273,7 +1304,7 @@ class AareDAQ: try: if self.sample is not None and self.sample.db_id is not None: self.__aare.send_sample_event(self.sample, SampleEventType.RASTERING, - comment=f"Raster at {request.omega_deg} deg") + comment=f"Raster at {request.omega_deg:.1f} deg") self.__aare.create_gridscan_run(self.sample, request, status) if not self.__cfg.simulated_detector: @@ -1291,7 +1322,6 @@ class AareDAQ: time_sec=request.exp_time_s, run_async=True, ) - self.__devs.aerotech.wait_till_done(timeout=int(round(total_time * 2, 0))) # go back to aerotech x,y,z home not U home (0 degrees). if isinstance(self.__cfg.abr_meas_pos, Coordinate): @@ -1352,8 +1382,10 @@ class AareDAQ: file_prefix=request.file_prefix, image_count=request.n_x * request.n_y, ) + com = None else: scan_result = self.__jfjoch.wait_till_done(60) + com = None if scan_result is None: logger.error( "JFJoch returned no ScanResult for raster", @@ -1374,7 +1406,7 @@ class AareDAQ: self.__set_state(BeamlineStateEnum.XtalSnapshot) if wait_for_screenshot and wait_for_screenshot > 0: time.sleep(wait_for_screenshot) - self.save_screenshot_db(sample_id, f"{sample_id}_post_raster_{request.omega_deg}deg") + self.save_screenshot_db(sample_id, f"{sample_id}_post_raster_{int(request.omega_deg)}deg") self.__aare.ingest_gridscan( sample=self.sample, raster_result=scan_result, @@ -1383,10 +1415,22 @@ class AareDAQ: com=None, beam_mark_pxl=self.__cfg.get_beam_mark(self.zoom), ) + if com is not None and com.max_image is not None: + diffraction_image_filename = f"{self.sample.db_id}_best_diffraction_from_raster_image_{com.max_image}" + diffraction_image_id = com.max_image + else: + diffraction_image_filename = f"{self.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, + ) + diffraction_image = self.__jfjoch.get_diffraction_image(diffraction_image_id) + self.__aare.upload_jpg(self.sample.db_id, diffraction_image_filename, diffraction_image) self.__aare.send_sample_event( self.sample, event_type=SampleEventType.RASTERED, - comment=f"Raster completed at {request.omega_deg} deg" + comment=f"Raster completed at {request.omega_deg:.1f} deg" ) logger.info( @@ -2048,6 +2092,142 @@ class AareDAQ: ) return calculated_target + def _auto_center_line_scan_top_left( + self, + *, + omega_deg: float, + file_prefix: str | None, + grid_size_mm: Coordinate, + default_n_y: int = 50, + y_retarget_threshold_mm: float | None = None, + y_padding_fraction_each_side: float = 0.10, + ) -> tuple[SmargonCoordinate, int]: + geom = self.sample_geometry + beam_x_pxl = geom.beam_location_pxl.x + beam_y_pxl = geom.beam_location_pxl.y + + line_scan_centre = geom.picture_to_smargon( + Coordinate(x=beam_x_pxl, y=beam_y_pxl) + ) + n_y = default_n_y + + prediction_result: MLBoxPredictionResult = self.__mlbox.predict( + preferred_class=(3, 0), + return_image=False, + return_bundle_meta=True, + ) + self._log_ml_bundle_meta( + f"auto_center_line_scan_{omega_deg:.2f}deg", + target_point=prediction_result.target_point, + focus=prediction_result.focus, + ) + + target_point = prediction_result.target_point + prediction_box = prediction_result.box + + if target_point is not None: + target_y_pxl = target_point[1] + y_delta_mm = abs(target_y_pxl - beam_y_pxl) * geom.pixel_in_mm + + if y_retarget_threshold_mm is None: + y_retarget_threshold_mm = geom.beam_size_mm.y * 2.0 + + if y_delta_mm > y_retarget_threshold_mm: + line_scan_centre = geom.picture_to_smargon( + Coordinate(x=beam_x_pxl, y=target_y_pxl) + ) + logger.info( + "Using ML target y for second auto-center raster", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "beam_y_pxl": beam_y_pxl, + "target_y_pxl": target_y_pxl, + "y_delta_mm": y_delta_mm, + "threshold_mm": y_retarget_threshold_mm, + "target_sh_x_mm": line_scan_centre.x, + "target_sh_y_mm": line_scan_centre.y, + "target_sh_z_mm": line_scan_centre.z, + }, + ) + else: + logger.info( + "Keeping beam-centred y line scan because ML target y shift is small", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "beam_y_pxl": beam_y_pxl, + "target_y_pxl": target_y_pxl, + "y_delta_mm": y_delta_mm, + "threshold_mm": y_retarget_threshold_mm, + }, + ) + else: + logger.info( + "No ML target point for second auto-center raster; using beam-centred line scan", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "beam_x_pxl": beam_x_pxl, + "beam_y_pxl": beam_y_pxl, + }, + ) + + if prediction_box is not None and prediction_box.box is not None and grid_size_mm.y > 0: + box_height_pxl = abs(prediction_box.box.bottom_y - prediction_box.box.top_y) + padded_height_mm = box_height_pxl * geom.pixel_in_mm * (1.0 + 2.0 * y_padding_fraction_each_side) + n_y = max(1, int(ceil(padded_height_mm / grid_size_mm.y))) + logger.info( + "Computed second auto-center raster y size from ML box height", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "box_height_pxl": box_height_pxl, + "pixel_in_mm": geom.pixel_in_mm, + "grid_size_y_mm": grid_size_mm.y, + "padding_fraction_each_side": y_padding_fraction_each_side, + "padded_height_mm": padded_height_mm, + "computed_n_y": n_y, + }, + ) + else: + logger.info( + "Using default y size for second auto-center raster", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "default_n_y": default_n_y, + "has_prediction_box": prediction_box is not None and prediction_box.box is not None, + }, + ) + + offset = Coordinate( + x=-grid_size_mm.x / 2.0, + y=-(n_y - 1) * grid_size_mm.y / 2.0, + ) + top_left = SmargonCoordinate( + sh_mm=line_scan_centre + geom.smargon_nudge(offset), + phi_deg=geom.smargon.phi_deg, + chi_deg=geom.smargon.chi_deg, + ) + + logger.info( + "Prepared second auto-center raster top-left from helper", + extra={ + "file_prefix": file_prefix, + "omega_deg": omega_deg, + "n_y": n_y, + "offset_x_mm": offset.x, + "offset_y_mm": offset.y, + "offset_z_mm": offset.z, + "top_left_x_mm": getattr(top_left.sh_mm, "x", None), + "top_left_y_mm": getattr(top_left.sh_mm, "y", None), + "top_left_z_mm": getattr(top_left.sh_mm, "z", None), + }, + ) + + return top_left, n_y + def __loop_center_sequence( self, sample_id: Optional[int] = None,