|
1 |
| -import numpy as np |
2 |
| -import numpy.typing as npt |
3 | 1 | import pytest
|
4 | 2 |
|
5 | 3 | from ibex_bluesky_core.callbacks.fitting import PeakStats
|
6 | 4 |
|
7 |
| -# Tests: |
8 |
| -# Test with normal scan with gaussian data |
9 |
| -# Check that asymmetrical data does not skew CoM |
10 |
| -# Check that having a background on data does not skew CoM |
11 |
| -# Check that order of documents does not skew CoM |
12 |
| -# Check that point spacing does not skew CoM |
13 |
| - |
14 |
| - |
15 |
| -def gaussian( |
16 |
| - x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float |
17 |
| -) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: |
18 |
| - return (x, amp * np.exp(-((x - x0) ** 2) / (2 * sigma**2)) + bg) |
19 |
| - |
20 |
| - |
21 |
| -def simulate_run_and_return_com(xy: tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]): |
22 |
| - ps = PeakStats("x", "y") |
23 |
| - |
24 |
| - ps.start({}) # pyright: ignore |
25 |
| - |
26 |
| - for x, y in np.vstack(xy).T: |
27 |
| - ps.event({"data": {"x": x, "y": y}}) # pyright: ignore |
28 |
| - |
29 |
| - ps.stop({}) # pyright: ignore |
30 |
| - |
31 |
| - return ps["com"] |
32 |
| - |
33 |
| - |
34 |
| -@pytest.mark.parametrize( |
35 |
| - ("x", "amp", "sigma", "x0", "bg"), |
36 |
| - [ |
37 |
| - (np.arange(-2, 3), 1, 1, 0, 0), |
38 |
| - (np.arange(-4, 1), 1, 1, -2, 0), |
39 |
| - ], |
40 |
| -) |
41 |
| -def test_normal_scan(x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float): |
42 |
| - xy = gaussian(x, amp, sigma, x0, bg) |
43 |
| - com = simulate_run_and_return_com(xy) |
44 |
| - assert com == pytest.approx(x0, abs=1e-4) |
45 |
| - |
46 | 5 |
|
47 | 6 | @pytest.mark.parametrize(
|
48 |
| - ("x", "amp", "sigma", "x0", "bg"), |
| 7 | + ("data", "expected_com"), |
49 | 8 | [
|
50 |
| - (np.arange(-4, 10), 1, 1, 0, 0), |
51 |
| - (np.arange(-6, 20), 1, 1, -2, 0), |
| 9 | + # Simplest case: |
| 10 | + # - Flat, non-zero Y data |
| 11 | + # - Evenly spaced, monotonically increasing X data |
| 12 | + ([(0, 1), (2, 1), (4, 1), (6, 1), (8, 1), (10, 1)], 5.0), |
| 13 | + # Simple triangular peak |
| 14 | + ([(0, 0), (1, 1), (2, 2), (3, 1), (4, 0), (5, 0)], 2.0), |
| 15 | + # Simple triangular peak with non-zero base |
| 16 | + ([(0, 10), (1, 11), (2, 12), (3, 11), (4, 10), (5, 10)], 2.0), |
| 17 | + # No data at all |
| 18 | + ([], None), |
| 19 | + # Only one point, com should be at that one point regardless whether it |
| 20 | + # measured zero or some other y value. |
| 21 | + ([(5, 0)], 5.0), |
| 22 | + ([(5, 50)], 5.0), |
| 23 | + # Two points, flat data, com should be in the middle |
| 24 | + ([(0, 5), (10, 5)], 5.0), |
| 25 | + # Flat, logarithmically spaced data - CoM should be in centre of measured range. |
| 26 | + ([(1, 3), (10, 3), (100, 3), (1000, 3), (10000, 3)], 5000.5), |
| 27 | + # "triangle" defined by area under two points |
| 28 | + # (CoM of a right triangle is 1/3 along x from right angle) |
| 29 | + ([(0, 0), (3, 6)], 2.0), |
| 30 | + ([(0, 6), (3, 0)], 1.0), |
| 31 | + # Cases with the first/last points not having equal spacings with each other |
| 32 | + ([(0, 1), (0.1, 1), (4, 1), (5, 0), (6, 1), (10, 1)], 5.0), |
| 33 | + ([(0, 1), (4, 1), (5, 0), (6, 1), (9.9, 1), (10, 1)], 5.0), |
| 34 | + # Two triangular peaks next to each other, with different point spacings |
| 35 | + # but same shapes, over a base of zero. |
| 36 | + ([(0, 0), (1, 1), (2, 2), (3, 1), (4, 0), (6, 2), (8, 0), (10, 0)], 4.0), |
| 37 | + ([(0, 0), (2, 2), (4, 0), (5, 1), (6, 2), (7, 1), (8, 0), (10, 0)], 4.0), |
| 38 | + # Two triangular peaks next to each other, with different point spacings |
| 39 | + # but same shapes, over a base of 10. |
| 40 | + ([(0, 10), (1, 11), (2, 12), (3, 11), (4, 10), (6, 12), (8, 10), (10, 10)], 4.0), |
| 41 | + ([(0, 10), (2, 12), (4, 10), (5, 11), (6, 12), (7, 11), (8, 10), (10, 10)], 4.0), |
| 42 | + # "Narrow" peak over a base of 0 |
| 43 | + ([(0, 0), (4.999, 0), (5.0, 10), (5.001, 0)], 5.0), |
| 44 | + # "Narrow" peak as above, over a base of 10 (y translation should not |
| 45 | + # affect CoM) |
| 46 | + ([(0, 10), (4.999, 10), (5.0, 20), (5.001, 10)], 5.0), |
| 47 | + # Non-monotonically increasing x data (e.g. from adaptive scan) |
| 48 | + ([(0, 0), (2, 2), (1, 1), (3, 1), (4, 0)], 2.0), |
| 49 | + # Overscanned data (all measurements duplicated, e.g. there-and-back scan) |
| 50 | + ([(0, 0), (1, 1), (2, 0), (2, 0), (1, 1), (0, 0)], 1.0), |
| 51 | + # Mixed positive/negative data. This explicitly calculates area *under* curve, |
| 52 | + # so CoM should still be the CoM of the positive peak in this data. |
| 53 | + ([(0, -1), (1, 0), (2, -1), (3, -1)], 1.0), |
| 54 | + # Y data with a single positive peak, which happens |
| 55 | + # to sum to zero but never contains zero. |
| 56 | + ([(0, -1), (1, 3), (2, -1), (3, -1)], 1.0), |
| 57 | + # Y data which happens to sum to *nearly* zero |
| 58 | + ([(0, -1), (1, 3.000001), (2, -1), (3, -1)], 1.0), |
52 | 59 | ],
|
53 | 60 | )
|
54 |
| -def test_asymmetrical_scan( |
55 |
| - x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float |
56 |
| -): |
57 |
| - xy = gaussian(x, amp, sigma, x0, bg) |
58 |
| - com = simulate_run_and_return_com(xy) |
59 |
| - assert com == pytest.approx(x0, abs=1e-4) |
60 |
| - |
61 |
| - |
62 |
| -@pytest.mark.parametrize( |
63 |
| - ("x", "amp", "sigma", "x0", "bg"), |
64 |
| - [ |
65 |
| - (np.arange(-2, 3), 1, 1, 0, 3), |
66 |
| - (np.arange(-4, 1), 1, 1, -2, -0.5), |
67 |
| - (np.arange(-4, 1), 1, 1, -2, -3), |
68 |
| - ], |
69 |
| -) |
70 |
| -def test_background_gaussian_scan( |
71 |
| - x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float |
72 |
| -): |
73 |
| - xy = gaussian(x, amp, sigma, x0, bg) |
74 |
| - com = simulate_run_and_return_com(xy) |
75 |
| - assert com == pytest.approx(x0, abs=1e-4) |
76 |
| - |
77 |
| - |
78 |
| -@pytest.mark.parametrize( |
79 |
| - ("x", "amp", "sigma", "x0", "bg"), |
80 |
| - [ |
81 |
| - (np.array([0, -2, 2, -1, 1]), 1, 1, 0, 0), |
82 |
| - (np.array([-4, 0, -2, -3, -1]), 1, 1, -2, 0), |
83 |
| - ], |
84 |
| -) |
85 |
| -def test_non_continuous_scan( |
86 |
| - x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float |
87 |
| -): |
88 |
| - xy = gaussian(x, amp, sigma, x0, bg) |
89 |
| - com = simulate_run_and_return_com(xy) |
90 |
| - assert com == pytest.approx(x0, abs=1e-4) |
| 61 | +def test_compute_com(data: list[tuple[float, float]], expected_com): |
| 62 | + ps = PeakStats("x", "y") |
| 63 | + ps.start({}) # pyright: ignore |
91 | 64 |
|
| 65 | + for x, y in data: |
| 66 | + ps.event({"data": {"x": x, "y": y}}) # pyright: ignore |
92 | 67 |
|
93 |
| -@pytest.mark.parametrize( |
94 |
| - ("x", "amp", "sigma", "x0", "bg"), |
95 |
| - [ |
96 |
| - (np.append(np.arange(-10, -2, 0.05), np.arange(-2, 4, 0.5)), 1, 0.5, 0, 0), |
97 |
| - ( |
98 |
| - np.concatenate( |
99 |
| - (np.arange(-5, -2.0, 0.5), np.arange(-2.5, -1.45, 0.05), np.arange(-1.5, 1, 0.5)), |
100 |
| - axis=0, |
101 |
| - ), |
102 |
| - 1, |
103 |
| - 0.25, |
104 |
| - 0, |
105 |
| - 0, |
106 |
| - ), |
107 |
| - ], |
108 |
| -) |
109 |
| -def test_non_constant_point_spacing_scan( |
110 |
| - x: npt.NDArray[np.float64], amp: float, sigma: float, x0: float, bg: float |
111 |
| -): |
112 |
| - xy = gaussian(x, amp, sigma, x0, bg) |
113 |
| - com = simulate_run_and_return_com(xy) |
114 |
| - assert com == pytest.approx(x0, abs=1e-3) |
| 68 | + ps.stop({}) # pyright: ignore |
| 69 | + assert ps["com"] == pytest.approx(expected_com) |
0 commit comments