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