|
| 1 | +import numpy as np |
| 2 | +import pytest |
| 3 | + |
| 4 | +from batchstats.stats.weighted_mean import BatchWeightedMean |
| 5 | +from batchstats.stats.weighted_sum import BatchWeightedSum |
| 6 | + |
| 7 | + |
| 8 | +@pytest.fixture |
| 9 | +def data_3d(): |
| 10 | + m, n, p = 100, 50, 10 |
| 11 | + return 1e1 * np.random.randn(m, n, p) + 1e3 |
| 12 | + |
| 13 | + |
| 14 | +axis_weight_scenarios = [ |
| 15 | + (0, "full"), |
| 16 | + (0, "broadcast_row"), |
| 17 | + (1, "full"), |
| 18 | + (1, "broadcast_col"), |
| 19 | + (2, "full"), |
| 20 | + ((1, 2), "full"), |
| 21 | + ((1, 2), "broadcast_plane"), |
| 22 | +] |
| 23 | + |
| 24 | + |
| 25 | +@pytest.fixture(params=axis_weight_scenarios) |
| 26 | +def scenario(request): |
| 27 | + return request.param |
| 28 | + |
| 29 | + |
| 30 | +@pytest.fixture |
| 31 | +def axis(scenario): |
| 32 | + return scenario[0] |
| 33 | + |
| 34 | + |
| 35 | +@pytest.fixture |
| 36 | +def weights_type(scenario): |
| 37 | + return scenario[1] |
| 38 | + |
| 39 | + |
| 40 | +@pytest.fixture |
| 41 | +def weights_3d(data_3d, axis, weights_type): |
| 42 | + shape = data_3d.shape |
| 43 | + axis_tuple = (axis,) if isinstance(axis, int) else axis |
| 44 | + |
| 45 | + if weights_type == "full": |
| 46 | + return np.random.rand(*shape) |
| 47 | + |
| 48 | + w_shape = list(shape) |
| 49 | + # This logic is a bit naive, but covers the test cases |
| 50 | + if weights_type == "broadcast_row": # axis 0 |
| 51 | + w_shape = [shape[0], 1, 1] |
| 52 | + elif weights_type == "broadcast_col": # axis 1 |
| 53 | + w_shape = [1, shape[1], 1] |
| 54 | + elif weights_type == "broadcast_plane": # axis (1,2) |
| 55 | + w_shape = [1, shape[1], shape[2]] |
| 56 | + |
| 57 | + return np.random.rand(*w_shape) |
| 58 | + |
| 59 | + |
| 60 | +@pytest.fixture |
| 61 | +def n_batches(): |
| 62 | + return 13 |
| 63 | + |
| 64 | + |
| 65 | +@pytest.mark.parametrize("klass", [BatchWeightedSum, BatchWeightedMean]) |
| 66 | +def test_weighted_stats_3d(data_3d, n_batches, axis, weights_3d, klass): |
| 67 | + if klass == BatchWeightedSum: |
| 68 | + true_stat = np.sum(data_3d * weights_3d, axis=axis) |
| 69 | + else: |
| 70 | + broadcasted_weights = np.broadcast_to(weights_3d, data_3d.shape) |
| 71 | + true_stat = np.sum(data_3d * weights_3d, axis=axis) / np.sum(broadcasted_weights, axis=axis) |
| 72 | + |
| 73 | + batch_op = klass(axis=axis) |
| 74 | + |
| 75 | + data_batches = np.array_split(data_3d, n_batches, axis=0) |
| 76 | + |
| 77 | + if weights_3d.shape[0] > 1: |
| 78 | + weights_batches = np.array_split(weights_3d, n_batches, axis=0) |
| 79 | + else: |
| 80 | + weights_batches = [weights_3d] * n_batches |
| 81 | + |
| 82 | + for batch_data, batch_weights in zip(data_batches, weights_batches): |
| 83 | + batch_op.update_batch(batch=batch_data, weights=batch_weights) |
| 84 | + |
| 85 | + batch_stat = batch_op() |
| 86 | + assert np.allclose(true_stat, batch_stat) |
| 87 | + |
| 88 | + |
| 89 | +@pytest.mark.parametrize("klass", [BatchWeightedSum, BatchWeightedMean]) |
| 90 | +def test_weighted_merge_3d(data_3d, axis, weights_3d, klass): |
| 91 | + if klass == BatchWeightedSum: |
| 92 | + true_stat = np.sum(data_3d * weights_3d, axis=axis) |
| 93 | + else: |
| 94 | + broadcasted_weights = np.broadcast_to(weights_3d, data_3d.shape) |
| 95 | + true_stat = np.sum(data_3d * weights_3d, axis=axis) / np.sum(broadcasted_weights, axis=axis) |
| 96 | + |
| 97 | + # Split data and weights into two halves |
| 98 | + d1, d2 = np.array_split(data_3d, 2, axis=0) |
| 99 | + if weights_3d.shape[0] > 1: |
| 100 | + w1, w2 = np.array_split(weights_3d, 2, axis=0) |
| 101 | + else: |
| 102 | + w1, w2 = weights_3d, weights_3d |
| 103 | + |
| 104 | + # Create and update two separate objects |
| 105 | + op1 = klass(axis=axis) |
| 106 | + op1.update_batch(d1, w1) |
| 107 | + |
| 108 | + op2 = klass(axis=axis) |
| 109 | + op2.update_batch(d2, w2) |
| 110 | + |
| 111 | + # Merge them |
| 112 | + merged_op = op1 + op2 |
| 113 | + |
| 114 | + merged_stat = merged_op() |
| 115 | + assert np.allclose(true_stat, merged_stat) |
| 116 | + |
| 117 | + |
| 118 | +def test_inconsistent_weights_shape_raises_error(data_3d): |
| 119 | + bws = BatchWeightedSum(axis=1) # Sum over a non-batch axis |
| 120 | + |
| 121 | + # First batch with per-column weights |
| 122 | + batch1 = data_3d[:10] |
| 123 | + weights1 = np.random.rand(1, data_3d.shape[1], 1) |
| 124 | + bws.update_batch(batch1, weights1) |
| 125 | + |
| 126 | + # Second batch with per-plane weights |
| 127 | + batch2 = data_3d[10:20] |
| 128 | + weights2 = np.random.rand(1, data_3d.shape[1], data_3d.shape[2]) |
| 129 | + with pytest.raises(ValueError, match="Inconsistent weights shape pattern"): |
| 130 | + bws.update_batch(batch2, weights2) |
0 commit comments