chore: checkpoint PROTEAN research planning and architecture
This commit is contained in:
commit
2cc6a9859e
22 files changed
+2138
No files matched your search
@@ -0,0 +1,126 @@
|
||||
"""Unit tests for the server-side prototype aggregation math.
|
||||
|
||||
Covers the dependency-free subset of the testing strategy in
|
||||
``docs/research/IMPLEMENTATION_PLAN.md`` (items 1-7). Items requiring the
|
||||
future data/federated modules (partition determinism, manifest reload,
|
||||
baseline invariance, validation/test separation) are deferred.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from protean_research.aggregation import (
|
||||
AggregationMode,
|
||||
aggregate_prototypes,
|
||||
aggregation_weights,
|
||||
beta_weights,
|
||||
)
|
||||
|
||||
BETAS = [0.0, 0.25, 0.5, 0.75, 1.0]
|
||||
|
||||
|
||||
def mean(vectors):
|
||||
dim = len(vectors[0])
|
||||
return [sum(v[d] for v in vectors) / len(vectors) for d in range(dim)]
|
||||
|
||||
|
||||
def test_beta_zero_equals_arithmetic_mean() -> None:
|
||||
prototypes = [[0.0, 2.0, -1.0], [2.0, 0.0, 1.0], [4.0, 4.0, 4.0]]
|
||||
supports = [1.0, 3.0, 10.0]
|
||||
out = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=0.0, supports=supports
|
||||
)
|
||||
assert out == pytest.approx(mean(prototypes))
|
||||
|
||||
|
||||
def test_beta_one_gives_sample_count_weighting() -> None:
|
||||
prototypes = [[0.0, 2.0], [2.0, 0.0], [4.0, 4.0]]
|
||||
supports = [1.0, 3.0, 10.0]
|
||||
out = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=1.0, supports=supports
|
||||
)
|
||||
total = sum(supports)
|
||||
expected = [
|
||||
sum(s * v[d] for s, v in zip(supports, prototypes)) / total
|
||||
for d in range(2)
|
||||
]
|
||||
assert out == pytest.approx(expected)
|
||||
|
||||
|
||||
def test_single_contributor_invariant_across_betas() -> None:
|
||||
prototypes = [[1.0, -2.0, 3.0]]
|
||||
supports = [7.0]
|
||||
for beta in BETAS:
|
||||
out = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=beta, supports=supports
|
||||
)
|
||||
assert out == pytest.approx(prototypes[0])
|
||||
|
||||
|
||||
def test_equal_supports_invariant_across_betas() -> None:
|
||||
prototypes = [[0.0, 2.0], [2.0, 0.0], [4.0, 4.0]]
|
||||
supports = [5.0, 5.0, 5.0]
|
||||
baseline = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=0.0, supports=supports
|
||||
)
|
||||
for beta in BETAS:
|
||||
out = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=beta, supports=supports
|
||||
)
|
||||
assert out == pytest.approx(baseline)
|
||||
|
||||
|
||||
def test_missing_class_not_in_s_j() -> None:
|
||||
# A client with N_ij == 0 must never be passed into S_j.
|
||||
with pytest.raises(ValueError):
|
||||
beta_weights([1.0, 0.0], beta=0.5)
|
||||
with pytest.raises(ValueError):
|
||||
beta_weights([1.0, -3.0], beta=0.5)
|
||||
with pytest.raises(ValueError):
|
||||
aggregate_prototypes(
|
||||
[[1.0], [2.0]], AggregationMode.SUPPORT_WEIGHTED, beta=0.5, supports=[1.0, 0.0]
|
||||
)
|
||||
|
||||
|
||||
def test_normalized_weights_sum_to_one() -> None:
|
||||
prototypes = [[0.0, 2.0], [2.0, 0.0], [4.0, 4.0]]
|
||||
for beta in BETAS:
|
||||
w = aggregation_weights(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=beta, supports=[1.0, 3.0, 10.0]
|
||||
)
|
||||
assert sum(w) == pytest.approx(1.0)
|
||||
w_eq = aggregation_weights(prototypes, AggregationMode.EQUAL_AVAILABLE)
|
||||
assert sum(w_eq) == pytest.approx(1.0)
|
||||
assert w_eq == pytest.approx([1 / 3, 1 / 3, 1 / 3])
|
||||
|
||||
|
||||
def test_valid_inputs_never_nan_or_inf() -> None:
|
||||
prototypes = [[0.0, 2.0], [2.0, 0.0], [4.0, 4.0]]
|
||||
for beta in BETAS:
|
||||
out = aggregate_prototypes(
|
||||
prototypes, AggregationMode.SUPPORT_WEIGHTED, beta=beta, supports=[1.0, 3.0, 10.0]
|
||||
)
|
||||
assert all(math.isfinite(v) for v in out)
|
||||
|
||||
|
||||
def test_original_protean_is_not_implemented() -> None:
|
||||
with pytest.raises(NotImplementedError):
|
||||
aggregate_prototypes([[1.0, 2.0]], AggregationMode.ORIGINAL_PROTEAN)
|
||||
|
||||
|
||||
def test_validation_errors() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
beta_weights([1.0], beta=1.5)
|
||||
with pytest.raises(ValueError):
|
||||
beta_weights([1.0], beta=None) # type: ignore[arg-type]
|
||||
with pytest.raises(ValueError):
|
||||
aggregate_prototypes([[1.0, 2.0], [1.0]], AggregationMode.SUPPORT_WEIGHTED, beta=0.5, supports=[1.0, 2.0])
|
||||
with pytest.raises(ValueError):
|
||||
aggregate_prototypes(
|
||||
[[1.0, 2.0]], AggregationMode.SUPPORT_WEIGHTED, beta=0.5, supports=[1.0, 2.0]
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
aggregate_prototypes([[1.0, float("nan")]], AggregationMode.EQUAL_AVAILABLE)
|
||||
Reference in new issue
Block a user