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:
co-authored by
Claude Opus 4.8
parent
0ac948746a
commit
5377e7d489
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user