DAQ/GUI: raster score as its own metric + score-based targeting
Build and Publish / test (pull_request) Successful in 2m7s
Build and Publish / build (pull_request) Successful in 17s
Build and Publish / Build and Deploy Docs (pull_request) Successful in 41s
Build and Publish / build (pull_request_target) Cancelled after 0s
Build and Publish / Build and Deploy Docs (pull_request_target) Cancelled after 0s
Build and Publish / test (pull_request_target) Cancelled after 16s
Build and Publish / test (pull_request) Successful in 2m7s
Build and Publish / build (pull_request) Successful in 17s
Build and Publish / Build and Deploy Docs (pull_request) Successful in 41s
Build and Publish / build (pull_request_target) Cancelled after 0s
Build and Publish / Build and Deploy Docs (pull_request_target) Cancelled after 0s
Build and Publish / test (pull_request_target) Cancelled after 16s
- promote crystal score to its own RasterGridMetric.RASTER_SCORE; first and default metric in the GUI dropdown; restore PR to original "Profile Radius" - send per-cell raster score to AareDB via RasterPayloadModel - split raster_highest_score (compute_crystal_score_array) from raster_centre_of_mass (restored to ndimage.center_of_mass); use score-based targeting for where-to-collect in both simulation and real mode - add tests covering compute_crystal_score_array and raster_highest_score Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ec45bf3729
commit
f860e47914
7 files changed
+100
-52
No files matched your search
@@ -280,32 +280,35 @@ def get_com_image_number(com, images):
|
||||
return image.number
|
||||
return None
|
||||
|
||||
def raster_centre_of_mass(result_array, images) -> CenterOfMassModel | None:
|
||||
# grid_mm_x and grid_mm_y are relative to the top left corner of raster grid
|
||||
# com = ndimage.center_of_mass(result_array)
|
||||
# if com and not np.isnan(com[0]) and not np.isnan(com[1]):
|
||||
# logger.info(f"Center of mass: {com}")
|
||||
# max_image = get_com_image_number(com, images)
|
||||
# return CenterOfMassModel(n_x=com[0], n_y=com[1], max_image=max_image)
|
||||
# elif com and np.isnan(com[0]) and np.isnan(com[1]):
|
||||
# logger.warning(f"Center of mass is nan: {com[0]}, {com[1]}")
|
||||
# return None
|
||||
# else:
|
||||
# logger.warning("No valid center of mass found")
|
||||
# return None
|
||||
score_array = compute_crystal_score_array(images)
|
||||
com = _max_cell(score_array)
|
||||
if com and not np.isnan(com[0]) and not np.isnan(com[1]):
|
||||
logger.info(f"Highest score: {com}")
|
||||
max_image = get_com_image_number(com, images)
|
||||
return CenterOfMassModel(n_x=com[0], n_y=com[1], max_image=max_image)
|
||||
elif com and np.isnan(com[0]) and np.isnan(com[1]):
|
||||
logger.warning(f"Highest score is nan: {com[0]}, {com[1]}")
|
||||
def _to_com_model(coords, images, label: str) -> CenterOfMassModel | None:
|
||||
"""Validate (n_x, n_y) grid coords and wrap them in a CenterOfMassModel.
|
||||
|
||||
Shared by raster_centre_of_mass and raster_highest_score, which differ only
|
||||
in how they pick the target cell.
|
||||
"""
|
||||
if coords and not np.isnan(coords[0]) and not np.isnan(coords[1]):
|
||||
logger.info(f"{label}: {coords}")
|
||||
max_image = get_com_image_number(coords, images)
|
||||
return CenterOfMassModel(n_x=coords[0], n_y=coords[1], max_image=max_image)
|
||||
elif coords and np.isnan(coords[0]) and np.isnan(coords[1]):
|
||||
logger.warning(f"{label} is nan: {coords[0]}, {coords[1]}")
|
||||
return None
|
||||
else:
|
||||
logger.warning("No valid score found")
|
||||
logger.warning(f"No valid {label} found")
|
||||
return None
|
||||
|
||||
|
||||
def raster_centre_of_mass(result_array, images) -> CenterOfMassModel | None:
|
||||
# grid_mm_x and grid_mm_y are relative to the top left corner of raster grid
|
||||
com = ndimage.center_of_mass(result_array)
|
||||
return _to_com_model(com, images, "Center of mass")
|
||||
|
||||
|
||||
def raster_highest_score(images) -> CenterOfMassModel | None:
|
||||
"""Target the grid cell with the highest crystal score (compute_crystal_score_array)."""
|
||||
score_array = compute_crystal_score_array(images)
|
||||
return _to_com_model(_max_cell(score_array), images, "Highest score")
|
||||
|
||||
def has_sufficient_low_res_spots(
|
||||
result_array: np.ndarray,
|
||||
min_spots_low_res: float,
|
||||
|
||||
@@ -82,6 +82,7 @@ class RasterPayloadModel(BaseModel):
|
||||
sample_id: int
|
||||
attach_image: bool = True
|
||||
centre_of_mass: CenterOfMassModel | None = None
|
||||
raster_score: List[float | None] | None = None
|
||||
center_pxl: Coordinate | None
|
||||
start_pxl: Coordinate
|
||||
cell_size_pxl: Annotated[Coordinate, AfterValidator(positive_coords)]
|
||||
|
||||
@@ -25,6 +25,7 @@ from aareDB import (
|
||||
from pydantic import StrictInt
|
||||
|
||||
from aare.common.coordinate import Coordinate
|
||||
from aare.common.find_xtal import compute_crystal_score_array
|
||||
from aare.common.logger_config import setup_logger
|
||||
from aare.common.logger_events import log_timing
|
||||
from aare.common.models import (
|
||||
@@ -358,12 +359,21 @@ class AareWrapper:
|
||||
else:
|
||||
center_pxl = None
|
||||
|
||||
try:
|
||||
score_arr = compute_crystal_score_array(raster_result.images)
|
||||
score = [float(score_arr[img.nx, img.ny]) if img.nx is not None and img.ny is not None else None
|
||||
for img in raster_result.images]
|
||||
except Exception as e:
|
||||
logger.warning(f"raster score computation failed, sending null score: {e}")
|
||||
score = None
|
||||
|
||||
payload = RasterPayloadModel(
|
||||
request = raster_request,
|
||||
result = raster_result,
|
||||
sample_id = sample.db_id,
|
||||
attach_image = True,
|
||||
centre_of_mass = com,
|
||||
raster_score = score, # TODO: Check whether AareDB needs to change for taking this input
|
||||
start_pxl = start_pxl,
|
||||
center_pxl = center_pxl,
|
||||
cell_size_pxl = cell_size_pxl,
|
||||
|
||||
@@ -8,7 +8,7 @@ from jfjoch_client.exceptions import NotFoundException
|
||||
|
||||
from aare.common.coordinate import AerotechCoordinate, Coordinate, SmargonCoordinate
|
||||
from aare.common.exception_handler import AutoRasterSampleSkipped, RasterScanException
|
||||
from aare.common.find_xtal import create_quality_filtered_array, raster_centre_of_mass
|
||||
from aare.common.find_xtal import raster_highest_score
|
||||
from aare.common.logger_events import (
|
||||
geom_log_context,
|
||||
log_ml_bundle_meta,
|
||||
@@ -351,16 +351,7 @@ class RasterService:
|
||||
if self.ctx.deps.cfg.simulated_detector:
|
||||
self.logger.info("Simulated detector mode enabled; using fake raster result.")
|
||||
scan_result = generate_no_beam_scan_result(request)
|
||||
result_array = create_quality_filtered_array(
|
||||
scan_result.images,
|
||||
"spots_low_res",
|
||||
min_spots=None,
|
||||
min_efficiency=1.0,
|
||||
min_background=None,
|
||||
min_low_res_spots=None,
|
||||
)
|
||||
|
||||
com = raster_centre_of_mass(result_array, scan_result.images)
|
||||
com = raster_highest_score(scan_result.images)
|
||||
target_coor = com.get_com_mm(request)
|
||||
target_coor_offset = self.ctx.sample_geometry.smargon_nudge(target_coor)
|
||||
target_smargon = SmargonCoordinate(
|
||||
@@ -381,15 +372,7 @@ class RasterService:
|
||||
)
|
||||
raise RasterScanException("JFJoch returned no ScanResult for raster")
|
||||
|
||||
result_array = create_quality_filtered_array(
|
||||
scan_result.images,
|
||||
"spots_low_res",
|
||||
min_spots=None,
|
||||
min_efficiency=1.0,
|
||||
min_background=None,
|
||||
min_low_res_spots=None,
|
||||
)
|
||||
com = raster_centre_of_mass(result_array, scan_result.images)
|
||||
com = raster_highest_score(scan_result.images)
|
||||
if com is None:
|
||||
self.logger.info("Calcualted COM is None using centre image")
|
||||
if request.n_x == 1:
|
||||
|
||||
@@ -86,6 +86,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
self.metric_combo = QComboBox()
|
||||
self._layout.addWidget(QLabel("Metric", parent=self), 7, 0)
|
||||
|
||||
self.metric_combo.addItem("Raster score", RasterGridMetric.RASTER_SCORE)
|
||||
self.metric_combo.addItem("Spot count (low res.)", RasterGridMetric.SPOTS_LOW_RES)
|
||||
self.metric_combo.addItem("Spot count", RasterGridMetric.SPOTS)
|
||||
self.metric_combo.addItem("Spot count (indexed)", RasterGridMetric.SPOTS_INDEXED)
|
||||
@@ -93,11 +94,11 @@ class RasterDataCollectionPanel(ScanSettingsPanel):
|
||||
self.metric_combo.addItem("Spot ratio (ice/low res.)", RasterGridMetric.SPOTS_ICE_LOW_RES)
|
||||
self.metric_combo.addItem("Background estimate", RasterGridMetric.BKG)
|
||||
self.metric_combo.addItem("Indexing result", RasterGridMetric.INDEXING)
|
||||
self.metric_combo.addItem("Profile Radius (Score)", RasterGridMetric.PR) # JD: repurposing PR for testing scoring
|
||||
self.metric_combo.addItem("Profile Radius", RasterGridMetric.PR)
|
||||
self.metric_combo.addItem("B-factor", RasterGridMetric.BFACTOR)
|
||||
self.metric_combo.addItem("Resolution (ML est.)", RasterGridMetric.RES)
|
||||
|
||||
self.metric_combo.setCurrentIndex(self.metric_combo.findData(RasterGridMetric.PR)) # JD: repurposing PR for testing scoring
|
||||
self.metric_combo.setCurrentIndex(self.metric_combo.findData(RasterGridMetric.RASTER_SCORE))
|
||||
self.metric_combo.currentIndexChanged.connect(self.metric_changed)
|
||||
|
||||
self._layout.addWidget(self.metric_combo, 7, 1, 1, 3)
|
||||
|
||||
@@ -30,6 +30,7 @@ class RasterGridMetric(Enum):
|
||||
SPOTS_ICE = 7
|
||||
SPOTS_INDEXED = 8
|
||||
SPOTS_ICE_LOW_RES = 9
|
||||
RASTER_SCORE = 10
|
||||
|
||||
# Viridis colormap colors (from matplotlib), dark purple -> yellow
|
||||
_VIRIDIS_COLORS = [
|
||||
@@ -122,8 +123,7 @@ class RasterGridManager(QObject):
|
||||
self.__beam_size_mm = geom.beam_size_mm
|
||||
self.__geom = geom
|
||||
|
||||
# self.__metric = RasterGridMetric.SPOTS_LOW_RES
|
||||
self.__metric = RasterGridMetric.PR # JD: repurposing PR for testing scoring
|
||||
self.__metric = RasterGridMetric.RASTER_SCORE
|
||||
|
||||
self.__loaded_image_prefix = None
|
||||
self.__loaded_image_index = None
|
||||
@@ -375,11 +375,11 @@ class RasterGridManager(QObject):
|
||||
if grid.result.images[cell].b is not None and grid.result.images[cell].b >= 0:
|
||||
txt += f"B-factor {grid.result.images[cell].b:.2f}<br/>"
|
||||
|
||||
# if grid.result.images[cell].pr is not None and grid.result.images[cell].pr >= 0: # JD: repurposing PR for testing scoring
|
||||
# txt += f"Profile Radius {grid.result.images[cell].pr:.2f}"
|
||||
if grid.result.images[cell].pr is not None and grid.result.images[cell].pr >= 0:
|
||||
txt += f"Profile Radius {grid.result.images[cell].pr:.2f}<br/>"
|
||||
if grid.result.images[cell].nx is not None and grid.result.images[cell].ny is not None:
|
||||
score = compute_crystal_score_array(grid.result.images)
|
||||
txt += f"Profile Radius (Score) {score[grid.result.images[cell].nx, grid.result.images[cell].ny]:.2f}"
|
||||
txt += f"Raster score {score[grid.result.images[cell].nx, grid.result.images[cell].ny]:.2f}"
|
||||
|
||||
return txt
|
||||
return None
|
||||
@@ -466,8 +466,9 @@ class RasterGridManager(QObject):
|
||||
v = [obj.bkg for obj in i.result.images]
|
||||
case RasterGridMetric.INDEXING:
|
||||
v = [obj.index for obj in i.result.images]
|
||||
case RasterGridMetric.PR: # JD: repurposing PR for testing scoring
|
||||
# v = [obj.index / max(obj.spots_low_res, 1) for obj in i.result.images]
|
||||
case RasterGridMetric.PR:
|
||||
v = [obj.index / max(obj.spots_low_res, 1) for obj in i.result.images]
|
||||
case RasterGridMetric.RASTER_SCORE:
|
||||
score = compute_crystal_score_array(i.result.images)
|
||||
v = [score[obj.nx, obj.ny] if obj.nx is not None and obj.ny is not None else None for obj in i.result.images]
|
||||
case RasterGridMetric.BFACTOR:
|
||||
|
||||
@@ -4,7 +4,8 @@ from unittest.mock import MagicMock
|
||||
from aare.common.find_xtal import (
|
||||
identify_crystal_raster, rebuild_array_from_scan_results,
|
||||
create_quality_filtered_array, get_xtal_size, get_best_b_factor,
|
||||
get_best_res, raster_centre_of_mass, has_sufficient_low_res_spots
|
||||
get_best_res, raster_centre_of_mass, has_sufficient_low_res_spots,
|
||||
compute_crystal_score_array, raster_highest_score
|
||||
)
|
||||
from aare.common.raster_grid import RasterGridRequest, CenterOfMassModel
|
||||
from aare.common.models import CrystalSize, Coordinate
|
||||
@@ -95,3 +96,51 @@ def test_has_sufficient_low_res_spots():
|
||||
assert has_sufficient_low_res_spots(arr, 3.0) is True
|
||||
assert has_sufficient_low_res_spots(arr, 5.0) is False
|
||||
assert has_sufficient_low_res_spots(None, 1.0) is False
|
||||
|
||||
@pytest.fixture
|
||||
def mock_score_results():
|
||||
# 2x2 grid; cell (1, 1) is the strongest crystal across bkg, spots_low_res
|
||||
# and spots_indexed (the three inputs to compute_crystal_score_array).
|
||||
data = [
|
||||
# nx, ny, bkg, spots_low_res, spots_indexed
|
||||
(0, 0, 1.0, 1.0, 0.0),
|
||||
(1, 0, 2.0, 2.0, 0.0),
|
||||
(0, 1, 2.0, 2.0, 0.0),
|
||||
(1, 1, 10.0, 20.0, 5.0),
|
||||
]
|
||||
results = []
|
||||
for i, (nx, ny, bkg, low, idx) in enumerate(data):
|
||||
res = MagicMock()
|
||||
res.nx, res.ny, res.number = nx, ny, i
|
||||
res.bkg, res.spots_low_res, res.spots_indexed = bkg, low, idx
|
||||
results.append(res)
|
||||
return results
|
||||
|
||||
def test_compute_crystal_score_array(mock_score_results):
|
||||
score = compute_crystal_score_array(mock_score_results)
|
||||
assert score.shape == (2, 2)
|
||||
# weights sum to 1.0 and each input is min-max normalised to [0, 100]
|
||||
assert score.min() >= 0.0 and score.max() <= 100.0
|
||||
# (1, 1) is max in all three inputs -> 100; weak corner (0, 0) is min in all -> 0
|
||||
assert score[1, 1] == pytest.approx(100.0)
|
||||
assert score[0, 0] == pytest.approx(0.0)
|
||||
assert np.unravel_index(np.argmax(score), score.shape) == (1, 1)
|
||||
|
||||
def test_compute_crystal_score_array_weights():
|
||||
# Cell A is the sole max in spots_low_res (weight 0.55); cell B is the sole
|
||||
# max in spots_indexed (weight 0.20). A must outscore B.
|
||||
def _cell(nx, n, low, idx):
|
||||
r = MagicMock()
|
||||
r.nx, r.ny, r.number = nx, 0, n
|
||||
r.bkg, r.spots_low_res, r.spots_indexed = 0.0, low, idx
|
||||
return r
|
||||
score = compute_crystal_score_array([_cell(0, 0, 10.0, 0.0), _cell(1, 1, 0.0, 10.0)])
|
||||
assert score[0, 0] > score[1, 0]
|
||||
assert score[0, 0] == pytest.approx(55.0)
|
||||
assert score[1, 0] == pytest.approx(20.0)
|
||||
|
||||
def test_raster_highest_score(mock_score_results):
|
||||
com = raster_highest_score(mock_score_results)
|
||||
assert com.n_x == 1.0
|
||||
assert com.n_y == 1.0
|
||||
assert com.max_image == 3
|
||||
Reference in new issue
Block a user