fix: Guillaume's fix for pin exclusion
CI / lint (push) Successful in 37s
Docs build and publish / docker (push) Successful in 17s
CI / test (3.12) (push) Canceled after 50s
CI / test (3.13) (push) Canceled after 46s
CI / test (3.14) (push) Canceled after 45s
CI / test-with-beamline-plugins (pxi_bec) (push) Canceled after 41s
CI / test-with-beamline-plugins (pxii_bec) (push) Canceled after 40s
CI / test-with-beamline-plugins (pxiii_bec) (push) Canceled after 36s
CI / test-with-coverage (push) Canceled after 35s
CI / coverage-analysis (push) Canceled after 0s
Build and Publish / release (push) Successful in 22s

This commit was merged in pull request #233.
This commit is contained in:
2026-09-17 14:24:25 +02:00
committed by perl_d
parent 1289851bb9
commit c545910f35
@@ -31,6 +31,7 @@ class MLRasterPlan:
loop_face_box: BoxTuple | None
image_width: int | None
image_height: int | None
pin_box: BoxTuple | None = None
def _box_tuple(model) -> BoxTuple | None:
@@ -47,6 +48,62 @@ def _box_union(a: BoxTuple, b: BoxTuple) -> BoxTuple:
return (min(a[0], b[0]), min(a[1], b[1]), max(a[2], b[2]), max(a[3], b[3]))
@dataclass(frozen=True)
class PinExclusion:
"""Outcome of :func:`exclude_pin_x_overlap`.
``box`` is the grid box with the pin's x-range removed. ``limit_x_min`` /
``limit_x_max`` is the pixel column the raster grid (padding included) must
not cross on the pin side. ``side`` is ``None`` when no exclusion was
applied, in which case ``reason`` says why.
"""
box: BoxTuple
side: str | None
limit_x_min: float | None
limit_x_max: float | None
clipped_px: float
reason: str | None
def exclude_pin_x_overlap(
box: BoxTuple, pin: BoxTuple, *, max_overlap_fraction: float
) -> PinExclusion:
"""Remove the x-range shared with the pin from a raster grid box.
The pin is metal: rastering it gives strong scattering that perturbs the
diffraction analysis. The pin sits on one side of the loop along x (decided
from the box centres); the grid box is cut at the pin's near edge and that
edge becomes a hard limit for grid padding on that side. The y-range of the
grid box is kept as is. With no x-overlap the box is untouched, but the pin
edge still caps the padding.
No exclusion is applied (``side`` is ``None``) when the pin's x-range covers
the whole box or the overlap exceeds ``max_overlap_fraction`` of the box
width: both point to a mis-detected pin rather than a stem running into it.
"""
x1, y1, x2, y2 = box
px1, _, px2, _ = pin
width = x2 - x1
if width <= 0:
return PinExclusion(box, None, None, None, 0.0, "grid box has no width")
if px1 <= x1 and px2 >= x2:
return PinExclusion(box, None, None, None, 0.0, "pin x-range covers the whole grid box")
pin_on_right = (px1 + px2) / 2.0 >= (x1 + x2) / 2.0
if pin_on_right:
overlap = max(0.0, x2 - max(px1, x1))
else:
overlap = max(0.0, min(px2, x2) - x1)
if overlap > max_overlap_fraction * width:
return PinExclusion(
box, None, None, None, 0.0, "pin x-overlap exceeds the allowed fraction of the grid box"
)
if pin_on_right:
return PinExclusion((x1, y1, min(x2, px1), y2), "right", None, px1, overlap, None)
return PinExclusion((max(x1, px2), y1, x2, y2), "left", px2, None, overlap, None)
def scale_auto_raster_grid(
*,
n_x: int,
@@ -187,26 +244,53 @@ def _box_to_raster_request(
min_cell_size_mm: float,
skip_if_exceed_max_image_threshold: bool,
grid_padding: bool = False,
pad_limit_x_min: float | None = None,
pad_limit_x_max: float | None = None,
) -> RasterGridRequest:
"""Convert a pixel box into a raster grid request.
``pad_limit_x_min`` / ``pad_limit_x_max`` is a pixel column the grid must
not cross (the near edge of the pin, see :func:`exclude_pin_x_overlap`):
the cell-rounding overshoot is moved away from that edge and the grid
padding on that side is capped so no cell lands beyond it.
"""
geom = sample_geometry
grid_size = Coordinate(x=geom.beam_size_mm.x * 0.8, y=geom.beam_size_mm.y * 0.8)
cell_px_x = grid_size.x / geom.pixel_in_mm
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 pad_limit_x_max is not None:
# ceil() lets the last cell reach up to one cell past x2; when the pin
# sits right after x2, shift the grid left so it ends on the pin edge.
# (A pin on the left needs no shift: the overshoot is on the far side.)
overshoot_px = x1 + n_x * cell_px_x - pad_limit_x_max
if overshoot_px > 0:
x1 = x1 - overshoot_px
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. Y is asymmetric: the bottom (far end
# of the n_y scan) can be padded more than the top.
# of the n_y scan) can be padded more than the top. X padding is capped
# on a side that has a pin limit.
frac_x = float(cfg_get("daq.auto_raster.grid_padding_fraction_x", 0.15))
frac_y_top = float(cfg_get("daq.auto_raster.grid_padding_fraction_y", 0.15))
frac_y_bottom = float(cfg_get("daq.auto_raster.grid_padding_fraction_y_bottom", frac_y_top))
pad_x = max(1, ceil(frac_x * n_x))
pad_x_left = pad_x
pad_x_right = pad_x
if pad_limit_x_min is not None:
room_px = x1 - pad_limit_x_min
pad_x_left = min(pad_x_left, max(0, floor(room_px / cell_px_x)))
if pad_limit_x_max is not None:
room_px = pad_limit_x_max - (x1 + n_x * cell_px_x)
pad_x_right = min(pad_x_right, max(0, floor(room_px / cell_px_x)))
pad_y_top = max(1, ceil(frac_y_top * n_y))
pad_y_bottom = max(1, ceil(frac_y_bottom * n_y))
x1 = x1 - pad_x * grid_size.x / geom.pixel_in_mm
x1 = x1 - pad_x_left * cell_px_x
y1 = y1 - pad_y_top * grid_size.y / geom.pixel_in_mm
n_x = n_x + 2 * pad_x
n_x = n_x + pad_x_left + pad_x_right
n_y = n_y + pad_y_top + pad_y_bottom
logger.info(
"Padded auto-center raster grid",
@@ -215,9 +299,12 @@ def _box_to_raster_request(
{
"sample_id": sample_id,
"ml_image_name": filename,
"pad_cells_x": pad_x,
"pad_cells_x_left": pad_x_left,
"pad_cells_x_right": pad_x_right,
"pad_cells_y_top": pad_y_top,
"pad_cells_y_bottom": pad_y_bottom,
"pad_limit_x_min": pad_limit_x_min,
"pad_limit_x_max": pad_limit_x_max,
"n_x": n_x,
"n_y": n_y,
},
@@ -319,7 +406,10 @@ def build_ml_raster_plan(
zoom-to-fit). The grid box is loop_face (else loop_all); a loop_face box is
padded by ``daq.auto_raster.loop_face_padding_fraction`` and, when
``daq.auto_raster.include_crystal`` is enabled, the box is extended to the
union of itself and any detected crystal boxes that lie beyond it.
union of itself and any detected crystal boxes that lie beyond it. When a
pin is detected and ``daq.auto_raster.exclude_pin_x_overlap`` is enabled
(default), the x-range shared with the pin is removed from the grid and no
padding cell is placed on the pin (see :func:`exclude_pin_x_overlap`).
"""
time.sleep(0.2)
sample_id = getattr(sample, "db_id", None)
@@ -339,6 +429,7 @@ def build_ml_raster_plan(
loop_all = predictions.get_best_for_class(MLBoxType.LOOP_ALL) if predictions else None
loop_face = predictions.get_best_for_class(MLBoxType.LOOP_FACE) if predictions else None
pin = predictions.get_best_for_class(MLBoxType.PIN) if predictions else None
# Grid box: prefer loop_face, else loop_all (matches the legacy (3, 0) order).
grid_model = loop_face if loop_face is not None else loop_all
@@ -376,6 +467,44 @@ def build_ml_raster_plan(
extra={"sample_id": sample_id, "ml_image_name": filename, "crystal_box": cbox},
)
# Keep the grid off the pin: drop the x-range shared with the pin box and
# stop the padding at the pin edge (metal scattering perturbs spot finding).
pin_box = _box_tuple(pin)
pad_limit_x_min: float | None = None
pad_limit_x_max: float | None = None
if pin_box is not None and cfg_get("daq.auto_raster.exclude_pin_x_overlap", True):
exclusion = exclude_pin_x_overlap(
(x1, y1, x2, y2),
pin_box,
max_overlap_fraction=float(
cfg_get("daq.auto_raster.exclude_pin_max_overlap_fraction", 0.75)
),
)
pin_log = {
"sample_id": sample_id,
"ml_image_name": filename,
"grid_box": (x1, y1, x2, y2),
"pin_box": pin_box,
}
if exclusion.side is None:
logger.warning(
"Kept ML raster grid unchanged despite a detected pin",
extra={**pin_log, "reason": exclusion.reason},
)
else:
x1, y1, x2, y2 = exclusion.box
pad_limit_x_min = exclusion.limit_x_min
pad_limit_x_max = exclusion.limit_x_max
logger.info(
"Excluded pin x-range from ML raster grid",
extra={
**pin_log,
"pin_side": exclusion.side,
"clipped_px": exclusion.clipped_px,
"clipped_grid_box": exclusion.box,
},
)
grid_request = _box_to_raster_request(
x1=x1,
y1=y1,
@@ -390,6 +519,8 @@ def build_ml_raster_plan(
min_cell_size_mm=min_cell_size_mm,
skip_if_exceed_max_image_threshold=skip_if_exceed_max_image_threshold,
grid_padding=True,
pad_limit_x_min=pad_limit_x_min,
pad_limit_x_max=pad_limit_x_max,
)
image_height = int(bundle_image.shape[0]) if bundle_image is not None else None
@@ -401,4 +532,5 @@ def build_ml_raster_plan(
loop_face_box=_box_tuple(loop_face),
image_width=image_width,
image_height=image_height,
pin_box=pin_box,
)