Files
AareCommon/tests/test_find_xtal.py
2026-07-02 12:52:45 +02:00

167 lines
5.3 KiB
Python

from unittest.mock import MagicMock
import numpy as np
import pytest
from aarecommon.math.find_xtal import (
compute_crystal_score_array,
create_quality_filtered_array,
get_best_b_factor,
get_best_res,
get_xtal_size,
has_sufficient_low_res_spots,
identify_crystal_raster,
raster_centre_of_mass,
raster_highest_score,
rebuild_array_from_scan_results,
)
from aarecommon.models.models import Coordinate, CrystalSize
from aarecommon.models.raster_grid import CenterOfMassModel, RasterGridRequest
@pytest.fixture
def mock_raster_results():
results = []
for i in range(9):
res = MagicMock()
res.nx = i % 3
res.ny = i // 3
res.number = i
res.spots_low_res = float(i)
res.spots = i + 10
res.spots_ice = 1
res.index = False
res.efficiency = 1.0
res.bkg = 5.0
res.b = 20.0 + i
res.res = 2.0 - i * 0.1
results.append(res)
return results
@pytest.fixture
def raster_request():
return RasterGridRequest(
exp_time_s=0.1, n_x=3, n_y=3, grid_size_mm=Coordinate(x=0.02, y=0.02), smargon_top_left=None
)
def test_identify_crystal_raster(mock_raster_results, raster_request):
mock_result = MagicMock()
mock_result.images = mock_raster_results
com = identify_crystal_raster(mock_result, raster_request)
assert isinstance(com, CenterOfMassModel)
# The max spots_low_res is 8.0 at (2, 2)
assert com.n_x == 2
assert com.n_y == 2
assert com.max_image == 8
def test_rebuild_array_from_scan_results(mock_raster_results):
arr = rebuild_array_from_scan_results(mock_raster_results, "spots_low_res", array_shape=(3, 3))
assert arr.shape == (3, 3)
assert arr[0, 0] == 0.0
assert arr[2, 2] == 8.0
def test_create_quality_filtered_array(mock_raster_results):
# Testing create_quality_filtered_array with a more lenient filter
arr = create_quality_filtered_array(
mock_raster_results, "spots", min_low_res_spots=0.0, min_background=0.0, array_shape=(3, 3)
)
# i=8 has nx=2, ny=2 -> row=2, col=2
assert arr[2, 2] == 18.0
def test_get_xtal_size(raster_request):
# 3x3 array where only center is 1
arr = np.zeros((3, 3))
arr[1, 1] = 1
size = CrystalSize(x=0, y=0, z=0)
new_size = get_xtal_size(size, arr, raster_request)
# row_max-row_min = 0, so size 0? Wait, it should probably be at least 1 grid unit if present.
# The code does (row_max - row_min) * grid_size_mm.x * 1000
# If row_min=1, row_max=1, then size is 0.
# This might be a bug in find_xtal.py if it doesn't account for the pixel itself.
assert new_size.x == 0
def test_get_best_b_factor(mock_raster_results):
assert get_best_b_factor(mock_raster_results) == 20.0
assert get_best_b_factor([]) is None
def test_get_best_res(mock_raster_results):
# min res. i=8 -> res = 2.0 - 0.8 = 1.2
assert pytest.approx(get_best_res(mock_raster_results)) == 1.2
assert get_best_res([]) is None
def test_raster_centre_of_mass(mock_raster_results):
arr = np.zeros((3, 3))
arr[1, 1] = 10.0
com = raster_centre_of_mass(arr, mock_raster_results)
assert com.n_x == 1.0
assert com.n_y == 1.0
def test_has_sufficient_low_res_spots():
arr = np.array([[1, 2], [3, 4]])
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