|
8 | 8 | bin_trace_samples, |
9 | 9 | ) |
10 | 10 |
|
11 | | -# Example data |
| 11 | +# ============================================================== |
| 12 | +# --- bin_trace_samples() |
| 13 | +# ============================================================== |
12 | 14 |
|
13 | | -SAMPLES_IN = [1, 2, 2, 3, 4] |
14 | | -NBINS = 4 |
15 | | -# NOTE: The x values in DENSITIES_OUT correspond to the centers of |
16 | | -# equally spaced bins over the range [1, 4]. |
17 | | -# This can be counterintuitive for count data, as the bins |
18 | | -# do not align with the integer sample values. |
19 | | -DENSITIES_OUT = [(1.375, 1), (2.125, 2), (2.875, 1), (3.625, 1)] |
20 | | -X_OUT, Y_OUT = zip(*DENSITIES_OUT, strict=True) |
21 | 15 |
|
22 | | -WEIGHTS = [1, 1, 1, 1, 9] |
| 16 | +# --- Basic functionality --- |
| 17 | + |
| 18 | + |
| 19 | +@pytest.mark.parametrize( |
| 20 | + ("samples", "nbins", "expected"), |
| 21 | + [ |
| 22 | + # Basic case with repeated values |
| 23 | + ([1, 2, 2, 3, 4], 4, [(1.375, 1), (2.125, 2), (2.875, 1), (3.625, 1)]), |
| 24 | + # Single bin aggregates all samples |
| 25 | + ([1, 2, 3], 1, [(2.0, 3)]), |
| 26 | + # Uniform distribution |
| 27 | + ([0, 1, 2, 3], 4, [(0.375, 1), (1.125, 1), (1.875, 1), (2.625, 1)]), |
| 28 | + # All identical samples go to rightmost bin |
| 29 | + ([3, 3, 3], 2, [(2.75, 0), (3.25, 3)]), |
| 30 | + # Negative values |
| 31 | + ([-2, -1, 0, 1], 2, [(-1.25, 2), (0.25, 2)]), |
| 32 | + ], |
| 33 | + ids=["basic", "single_bin", "uniform", "identical", "negative"], |
| 34 | +) |
| 35 | +def test_basic_binning( |
| 36 | + samples: list[float], nbins: int, expected: list[tuple[float, float]] |
| 37 | +) -> None: |
| 38 | + result = bin_trace_samples(samples, nbins=nbins) |
| 39 | + assert result == expected |
23 | 40 |
|
24 | | -# ============================================================== |
25 | | -# --- estimate_density_trace() |
26 | | -# ============================================================== |
| 41 | + |
| 42 | +def test_float_samples_binning() -> None: |
| 43 | + result = bin_trace_samples([0.1, 0.5, 0.9], nbins=3) |
| 44 | + x_vals, y_vals = zip(*result, strict=True) |
| 45 | + assert x_vals == pytest.approx((0.233, 0.5, 0.767), rel=1e-2) |
| 46 | + assert y_vals == (1.0, 1.0, 1.0) |
27 | 47 |
|
28 | 48 |
|
29 | | -def test_bin_trace_samples_simple() -> None: |
30 | | - density_trace = bin_trace_samples(trace_samples=SAMPLES_IN, nbins=NBINS) |
31 | | - x, y = zip(*density_trace, strict=True) |
32 | | - assert x == X_OUT |
33 | | - assert y == Y_OUT |
| 49 | +@pytest.mark.parametrize("nbins", [1, 2, 5, 10, 50]) |
| 50 | +def test_output_length_matches_nbins(nbins: int) -> None: |
| 51 | + result = bin_trace_samples([1, 2, 3, 4, 5], nbins=nbins) |
| 52 | + assert len(result) == nbins |
34 | 53 |
|
35 | 54 |
|
36 | | -@pytest.mark.parametrize("nbins", [2, 5, 8, 11]) |
37 | | -def test_bin_trace_samples_nbins(nbins: int) -> None: |
38 | | - density_trace = bin_trace_samples(trace_samples=SAMPLES_IN, nbins=nbins) |
39 | | - assert len(density_trace) == nbins |
| 55 | +@pytest.mark.parametrize( |
| 56 | + "input_type", |
| 57 | + [list, tuple, np.array], |
| 58 | + ids=["list", "tuple", "ndarray"], |
| 59 | +) |
| 60 | +def test_accepts_various_input_types(input_type: type) -> None: |
| 61 | + samples = input_type([1, 2, 3]) |
| 62 | + result = bin_trace_samples(samples, nbins=2) |
| 63 | + assert len(result) == 2 |
| 64 | + assert all(isinstance(x, float) and isinstance(y, float) for x, y in result) |
40 | 65 |
|
41 | 66 |
|
42 | | -@pytest.mark.parametrize("non_finite_value", [np.inf, np.nan, float("inf"), float("nan")]) |
43 | | -def test_bin_trace_samples_fails_for_non_finite_values(non_finite_value: float) -> None: |
44 | | - err_msg = "The samples array should not contain any infs or NaNs." |
45 | | - with pytest.raises(ValueError, match=err_msg): |
46 | | - bin_trace_samples(trace_samples=[*SAMPLES_IN[:-1], non_finite_value], nbins=NBINS) |
| 67 | +def test_counts_sum_to_sample_size() -> None: |
| 68 | + samples = list(range(100)) |
| 69 | + result = bin_trace_samples(samples, nbins=7) |
| 70 | + total_count = sum(y for _, y in result) |
| 71 | + assert total_count == len(samples) |
47 | 72 |
|
48 | 73 |
|
49 | | -def test_bin_trace_samples_weights() -> None: |
50 | | - density_trace = bin_trace_samples( |
51 | | - trace_samples=SAMPLES_IN, |
52 | | - nbins=NBINS, |
53 | | - weights=WEIGHTS, |
54 | | - ) |
55 | | - x, y = zip(*density_trace, strict=True) |
56 | | - assert x == X_OUT |
57 | | - assert np.argmax(y) == len(y) - 1 |
| 74 | +def test_bin_centers_within_data_range() -> None: |
| 75 | + samples = [10, 20, 30, 40, 50] |
| 76 | + result = bin_trace_samples(samples, nbins=5) |
| 77 | + centers = [x for x, _ in result] |
| 78 | + assert all(min(samples) <= c <= max(samples) for c in centers) |
58 | 79 |
|
59 | 80 |
|
60 | | -def test_bin_trace_samples_weights_not_same_length() -> None: |
61 | | - with pytest.raises( |
62 | | - ValueError, match="The weights array should have the same length as the samples array" |
63 | | - ): |
64 | | - bin_trace_samples(trace_samples=SAMPLES_IN, nbins=NBINS, weights=[1, 1, 1]) |
| 81 | +# --- Weights --- |
65 | 82 |
|
66 | 83 |
|
67 | | -@pytest.mark.parametrize("non_finite_value", [np.inf, np.nan, float("inf"), float("nan")]) |
68 | | -def test_bin_trace_samples_weights_fails_for_non_finite_values( |
69 | | - non_finite_value: float, |
| 84 | +@pytest.mark.parametrize( |
| 85 | + ("samples", "weights", "nbins", "expected_counts"), |
| 86 | + [ |
| 87 | + # Weights shift distribution |
| 88 | + ([1, 2, 3], [10, 1, 1], 3, [10, 1, 1]), |
| 89 | + # Zero weights effectively exclude samples |
| 90 | + ([1, 2, 3], [1, 0, 1], 3, [1, 0, 1]), |
| 91 | + # Fractional weights |
| 92 | + ([1, 2], [0.5, 1.5], 2, [0.5, 1.5]), |
| 93 | + ], |
| 94 | + ids=["heavy_first", "zero_weight", "fractional"], |
| 95 | +) |
| 96 | +def test_weights_affect_counts( |
| 97 | + samples: list[float], |
| 98 | + weights: list[float], |
| 99 | + nbins: int, |
| 100 | + expected_counts: list[float], |
70 | 101 | ) -> None: |
71 | | - err_msg = "The weights array should not contain any infs or NaNs." |
72 | | - with pytest.raises(ValueError, match=err_msg): |
73 | | - bin_trace_samples( |
74 | | - trace_samples=SAMPLES_IN, |
75 | | - nbins=NBINS, |
76 | | - weights=[*WEIGHTS[:-1], non_finite_value], |
77 | | - ) |
| 102 | + result = bin_trace_samples(samples, nbins=nbins, weights=weights) |
| 103 | + counts = [y for _, y in result] |
| 104 | + assert counts == pytest.approx(expected_counts) |
| 105 | + |
| 106 | + |
| 107 | +def test_weighted_counts_sum_to_weight_sum() -> None: |
| 108 | + samples = [1, 2, 3, 4, 5] |
| 109 | + weights = [2.0, 3.0, 1.5, 0.5, 4.0] |
| 110 | + result = bin_trace_samples(samples, nbins=3, weights=weights) |
| 111 | + assert sum(y for _, y in result) == pytest.approx(sum(weights)) |
| 112 | + |
| 113 | + |
| 114 | +# --- Error handling --- |
| 115 | + |
| 116 | + |
| 117 | +@pytest.mark.parametrize( |
| 118 | + "non_finite", |
| 119 | + [np.inf, -np.inf, np.nan, float("inf"), float("nan")], |
| 120 | + ids=["inf", "neg_inf", "nan", "float_inf", "float_nan"], |
| 121 | +) |
| 122 | +def test_rejects_non_finite_samples(non_finite: float) -> None: |
| 123 | + with pytest.raises(ValueError, match="samples array should not contain any infs or NaNs"): |
| 124 | + bin_trace_samples([1, 2, non_finite], nbins=2) |
| 125 | + |
| 126 | + |
| 127 | +@pytest.mark.parametrize( |
| 128 | + "non_finite", |
| 129 | + [np.inf, -np.inf, np.nan, float("inf"), float("nan")], |
| 130 | + ids=["inf", "neg_inf", "nan", "float_inf", "float_nan"], |
| 131 | +) |
| 132 | +def test_rejects_non_finite_weights(non_finite: float) -> None: |
| 133 | + with pytest.raises(ValueError, match="weights array should not contain any infs or NaNs"): |
| 134 | + bin_trace_samples([1, 2, 3], nbins=2, weights=[1, non_finite, 1]) |
| 135 | + |
| 136 | + |
| 137 | +@pytest.mark.parametrize( |
| 138 | + ("samples", "weights"), |
| 139 | + [ |
| 140 | + ([1, 2, 3], [1, 2]), |
| 141 | + ([1, 2], [1, 2, 3]), |
| 142 | + ([1], []), |
| 143 | + ], |
| 144 | + ids=["weights_short", "weights_long", "empty_weights"], |
| 145 | +) |
| 146 | +def test_rejects_mismatched_weights_length(samples: list[float], weights: list[float]) -> None: |
| 147 | + with pytest.raises(ValueError, match="weights array should have the same length"): |
| 148 | + bin_trace_samples(samples, nbins=2, weights=weights) |
78 | 149 |
|
79 | 150 |
|
80 | 151 | # ============================================================== |
81 | | -# --- estimate_densities() |
| 152 | +# --- bin_samples() |
82 | 153 | # ============================================================== |
83 | 154 |
|
84 | 155 |
|
85 | 156 | def test_bin_samples() -> None: |
86 | | - densities = bin_samples( |
87 | | - samples=[[SAMPLES_IN], [SAMPLES_IN]], |
88 | | - nbins=NBINS, |
89 | | - ) |
| 157 | + samples = [1, 2, 2, 3, 4] |
| 158 | + nbins = 4 |
| 159 | + expected = [(1.375, 1), (2.125, 2), (2.875, 1), (3.625, 1)] |
| 160 | + x_out, y_out = zip(*expected, strict=True) |
| 161 | + densities = bin_samples(samples=[[samples], [samples]], nbins=nbins) |
90 | 162 | assert len(densities) == 2 |
91 | 163 | for densities_row in densities: |
92 | 164 | assert len(densities_row) == 1 |
93 | 165 | density_trace = next(iter(densities_row)) |
94 | 166 | x, y = zip(*density_trace, strict=True) |
95 | | - assert x == X_OUT |
96 | | - assert y == Y_OUT |
| 167 | + assert x == x_out |
| 168 | + assert y == y_out |
0 commit comments