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
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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user