raster auto-center: unified first-grid padding in x and y (shifts top-left)

Replace the loop_face-only box padding with a single grid padding applied to
the first auto-center grid scan for any loop box (loop_all or loop_face). It
pads each side by a fraction of the grid size (daq.auto_raster.grid_padding_
fraction_x / _y, default 0.15), with a minimum of one cell per side, and
extends *before* cell 0 too so smargon_top_left shifts outward. Manual grid
(get_ml_bounding_box) is unchanged (grid_padding defaults off).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
appleb_m
2026-06-26 10:43:12 +02:00
co-authored by Claude Opus 4.8
parent 0ac948746a
commit 5377e7d489
3 changed files with 67 additions and 21 deletions
+2 -1
View File
@@ -54,6 +54,7 @@ daq:
maximum_flux: 4e11
auto_raster:
loop_face_padding_fraction: 0.15 # pad loop_face raster boxes by 15% per side
grid_padding_fraction_x: 0.15 # pad the 1st grid scan by this fraction of its size per side in x (min 1 cell)
grid_padding_fraction_y: 0.15 # ... and in y; shifts smargon_top_left outward (adds cells before cell 0)
include_crystal: false # extend the grid to cover crystals outside the loop box
line_scan_y_padding_fraction: 0.15 # pad the 2nd-stage vertical line scan height by this per side (10-20%)
@@ -188,13 +188,36 @@ def _box_to_raster_request(
max_images: int,
min_cell_size_mm: float,
skip_if_exceed_max_image_threshold: bool,
grid_padding: bool = False,
) -> RasterGridRequest:
geom = sample_geometry
start_coord = geom.picture_to_smargon(Coordinate(x=x1, y=y1))
grid_size = Coordinate(x=geom.beam_size_mm.x * 0.8, y=geom.beam_size_mm.y * 0.8)
n_x = max(1, abs(ceil((x2 - x1) * geom.pixel_in_mm / grid_size.x)))
n_y = max(1, abs(ceil((y2 - y1) * geom.pixel_in_mm / grid_size.y)))
if grid_padding:
# Pad the grid by a fraction of its size on each side (at least one cell),
# extending *before* cell 0 as well, so the top-left moves outward and
# smargon_top_left shifts with it.
frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15))
frac_y = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15))
pad_x = max(1, int(ceil(frac_x * n_x)))
pad_y = max(1, int(ceil(frac_y * n_y)))
x1 = x1 - pad_x * grid_size.x / geom.pixel_in_mm
y1 = y1 - pad_y * grid_size.y / geom.pixel_in_mm
n_x = n_x + 2 * pad_x
n_y = n_y + 2 * pad_y
logger.info(
"Padded auto-center raster grid",
extra=merge_log_context(
sample_log_context(sample),
{"sample_id": sample_id, "ml_image_name": filename,
"pad_cells_x": pad_x, "pad_cells_y": pad_y, "n_x": n_x, "n_y": n_y},
),
)
start_coord = geom.picture_to_smargon(Coordinate(x=x1, y=y1))
original_n_x = n_x
original_n_y = n_y
original_grid_size = grid_size
@@ -326,14 +349,6 @@ def build_ml_raster_plan(
x1, y1, x2, y2 = (grid_model.box.top_x, grid_model.box.top_y,
grid_model.box.bottom_x, grid_model.box.bottom_y)
is_loop_face = grid_model is loop_face
# Pad loop_face boxes a little so the grid isn't cut tight to the face.
if is_loop_face:
frac = float(cfg_get("daq.auto_raster.loop_face_padding_fraction", 0.15))
pad_x = (x2 - x1) * frac
pad_y = (y2 - y1) * frac
x1, y1, x2, y2 = x1 - pad_x, y1 - pad_y, x2 + pad_x, y2 + pad_y
# Optionally extend the grid to cover crystals detected outside the loop box.
if cfg_get("daq.auto_raster.include_crystal", False) and predictions is not None:
@@ -362,6 +377,7 @@ def build_ml_raster_plan(
max_images=max_images,
min_cell_size_mm=min_cell_size_mm,
skip_if_exceed_max_image_threshold=skip_if_exceed_max_image_threshold,
grid_padding=True,
)
image_height = int(bundle_image.shape[0]) if bundle_image is not None else None
@@ -11,6 +11,7 @@ from aare.daq.mlbox import MLBoxPredictionsResult
from aare.daq.operations.common import ml_bounding_box as mlb
from aare.daq.operations.common.ml_bounding_box import (
_box_extends_beyond,
_box_to_raster_request,
_box_union,
build_ml_raster_plan,
)
@@ -81,27 +82,55 @@ def test_plan_none_when_no_loop():
assert _plan(_fake_mlbox(crystals=[(300, 300, 350, 350)])) is None
def test_loop_face_padding_grows_grid(monkeypatch):
box = (150, 150, 300, 300)
monkeypatch.setattr(mlb, "cfg_get", lambda key, default=None: 0.0 if "padding" in key else default)
unpadded = _plan(_fake_mlbox(loop_face=box)).grid_request
monkeypatch.setattr(mlb, "cfg_get", lambda key, default=None: 0.5 if "padding" in key else default)
padded = _plan(_fake_mlbox(loop_face=box)).grid_request
assert padded.n_x > unpadded.n_x and padded.n_y > unpadded.n_y
def _grid(box, *, grid_padding):
x1, y1, x2, y2 = box
return _box_to_raster_request(
x1=x1, y1=y1, x2=x2, y2=y2,
sample=None, sample_geometry=_geom(), logger=logger, filename=None,
sample_id=None, max_images=100000, min_cell_size_mm=0.0001,
skip_if_exceed_max_image_threshold=False, grid_padding=grid_padding,
)
def test_grid_padding_grows_and_shifts_top_left(monkeypatch):
# fraction 0 -> minimum one cell of padding per side
monkeypatch.setattr(mlb, "cfg_get", lambda k, d=None: 0.0 if "grid_padding_fraction" in k else d)
box = (150, 150, 400, 300)
nopad = _grid(box, grid_padding=False)
pad = _grid(box, grid_padding=True)
assert pad.n_x == nopad.n_x + 2 # 1 cell each side in x
assert pad.n_y == nopad.n_y + 2 # 1 cell each side in y
# top-left shifted outward (cells added before cell 0)
assert pad.smargon_top_left.sh_mm.x != nopad.smargon_top_left.sh_mm.x
assert pad.smargon_top_left.sh_mm.z != nopad.smargon_top_left.sh_mm.z
def test_grid_padding_fraction_scales(monkeypatch):
box = (150, 150, 520, 420)
monkeypatch.setattr(mlb, "cfg_get", lambda k, d=None: 0.0 if "grid_padding_fraction" in k else d)
small = _grid(box, grid_padding=True)
monkeypatch.setattr(mlb, "cfg_get", lambda k, d=None: 0.5 if "grid_padding_fraction" in k else d)
big = _grid(box, grid_padding=True)
assert big.n_x > small.n_x and big.n_y > small.n_y
def test_crystal_union_extends_grid_only_when_enabled(monkeypatch):
# crystal extends well beyond the loop_face box on +x
mlbox = lambda: _fake_mlbox(loop_face=(150, 150, 300, 300), crystals=[(350, 150, 520, 300)])
monkeypatch.setattr(mlb, "cfg_get", lambda key, default=None: False if "include_crystal" in key else (0.0 if "padding" in key else default))
off = _plan(mlbox()).grid_request
def cfg(enabled):
return lambda k, d=None: (
enabled if "include_crystal" in k
else (0.0 if "grid_padding_fraction" in k else d)
)
monkeypatch.setattr(mlb, "cfg_get", lambda key, default=None: True if "include_crystal" in key else (0.0 if "padding" in key else default))
monkeypatch.setattr(mlb, "cfg_get", cfg(False))
off = _plan(mlbox()).grid_request
monkeypatch.setattr(mlb, "cfg_get", cfg(True))
on = _plan(mlbox()).grid_request
assert on.n_x > off.n_x # grid widened to reach the crystal
assert on.n_y == off.n_y # crystal is within the loop's y-range
assert on.n_y == off.n_y # crystal is within the loop's y-range (same padding both)
def test_zoom_box_uses_loop_all_unless_clipped():