feat: math core v3 engine upgrade
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
"""Property-based tests for v3 correlation-aware clustering.
|
||||
|
||||
Feature: math-core-v3-engine
|
||||
|
||||
Uses Hypothesis to validate correctness properties of the v3 clustering
|
||||
layer: effective evidence count n_eff is bounded by cluster size, and
|
||||
cluster LLR is clamped to [-2.5, 2.5].
|
||||
|
||||
Validates: Requirements 4.2, 4.3, 4.4, 4.5, 21.4
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
from services.aggregation.worker import (
|
||||
EvidenceCluster,
|
||||
cluster_evidence,
|
||||
compute_cluster_llr,
|
||||
compute_n_eff,
|
||||
)
|
||||
from services.aggregation.scoring import EvidenceUnit
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hypothesis strategies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# LLR values for testing
|
||||
llr_values = st.floats(min_value=-5.0, max_value=5.0, allow_nan=False, allow_infinity=False)
|
||||
llr_lists = st.lists(llr_values, min_size=1, max_size=20)
|
||||
|
||||
# Correlation values
|
||||
rho_values = st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
|
||||
|
||||
|
||||
def _symmetric_correlation_matrix(n: int) -> st.SearchStrategy[list[list[float]]]:
|
||||
"""Generate an NxN symmetric correlation matrix with values in [0, 1].
|
||||
|
||||
Diagonal is 1.0, off-diagonal entries are symmetric rho in [0, 1].
|
||||
"""
|
||||
if n <= 1:
|
||||
return st.just([[1.0]])
|
||||
|
||||
# Generate upper-triangle entries (n*(n-1)/2 values)
|
||||
n_pairs = n * (n - 1) // 2
|
||||
upper_triangle = st.lists(rho_values, min_size=n_pairs, max_size=n_pairs)
|
||||
|
||||
@st.composite
|
||||
def build_matrix(draw: st.DrawFn) -> list[list[float]]:
|
||||
entries = draw(upper_triangle)
|
||||
matrix = [[0.0] * n for _ in range(n)]
|
||||
idx = 0
|
||||
for i in range(n):
|
||||
matrix[i][i] = 1.0
|
||||
for j in range(i + 1, n):
|
||||
matrix[i][j] = entries[idx]
|
||||
matrix[j][i] = entries[idx]
|
||||
idx += 1
|
||||
return matrix
|
||||
|
||||
return build_matrix()
|
||||
|
||||
|
||||
# n_eff positive floats for property 6
|
||||
n_eff_values = st.floats(min_value=0.1, max_value=20.0, allow_nan=False, allow_infinity=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 5: Effective evidence count n_eff is bounded by cluster size
|
||||
# Feature: math-core-v3-engine, Property 5: Effective evidence count n_eff is bounded by cluster size
|
||||
# Validates: Requirements 4.2, 4.3, 21.4
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(llrs=llr_lists)
|
||||
def test_property_5_n_eff_bounded_by_cluster_size_default_correlations(
|
||||
llrs: list[float],
|
||||
) -> None:
|
||||
"""Property 5: n_eff is bounded by cluster size (default correlations).
|
||||
|
||||
For any cluster of N signals using default pairwise correlations (rho=0.80),
|
||||
the computed n_eff SHALL satisfy 0 < n_eff <= N.
|
||||
|
||||
**Validates: Requirements 4.2, 4.3**
|
||||
"""
|
||||
n = len(llrs)
|
||||
n_eff = compute_n_eff(llrs)
|
||||
|
||||
assert n_eff > 0.0, f"n_eff={n_eff} must be positive"
|
||||
assert n_eff <= n + 1e-9, f"n_eff={n_eff} exceeds cluster size N={n}"
|
||||
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
data=st.data(),
|
||||
llrs=st.lists(llr_values, min_size=2, max_size=10),
|
||||
)
|
||||
def test_property_5_n_eff_bounded_with_explicit_correlations(
|
||||
data: st.DataObject,
|
||||
llrs: list[float],
|
||||
) -> None:
|
||||
"""Property 5: n_eff is bounded by cluster size (explicit correlation matrix).
|
||||
|
||||
For any cluster of N signals with a symmetric NxN correlation matrix
|
||||
with values in [0, 1], the computed n_eff SHALL satisfy 0 < n_eff <= N.
|
||||
|
||||
**Validates: Requirements 4.2, 4.3, 21.4**
|
||||
"""
|
||||
n = len(llrs)
|
||||
corr_matrix = data.draw(_symmetric_correlation_matrix(n))
|
||||
|
||||
n_eff = compute_n_eff(llrs, correlations=corr_matrix)
|
||||
|
||||
assert n_eff > 0.0, f"n_eff={n_eff} must be positive"
|
||||
assert n_eff <= n + 1e-9, f"n_eff={n_eff} exceeds cluster size N={n}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Property 6: Cluster LLR is clamped to [-2.5, 2.5]
|
||||
# Feature: math-core-v3-engine, Property 6: Cluster LLR is clamped to [-2.5, 2.5]
|
||||
# Validates: Requirements 4.4, 4.5
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(
|
||||
llrs=llr_lists,
|
||||
n_eff=n_eff_values,
|
||||
)
|
||||
def test_property_6_cluster_llr_clamped(
|
||||
llrs: list[float],
|
||||
n_eff: float,
|
||||
) -> None:
|
||||
"""Property 6: Cluster LLR is clamped to [-2.5, 2.5].
|
||||
|
||||
For any cluster configuration with any number of signals and any LLR values,
|
||||
the computed cluster LLR_c SHALL be in [-2.5, 2.5].
|
||||
|
||||
**Validates: Requirements 4.4, 4.5**
|
||||
"""
|
||||
cluster_llr = compute_cluster_llr(llrs, n_eff)
|
||||
|
||||
assert -2.5 <= cluster_llr <= 2.5, (
|
||||
f"cluster_llr={cluster_llr} out of [-2.5, 2.5]"
|
||||
)
|
||||
|
||||
|
||||
@settings(max_examples=100)
|
||||
@given(llrs=llr_lists)
|
||||
def test_property_6_cluster_llr_clamped_with_computed_n_eff(
|
||||
llrs: list[float],
|
||||
) -> None:
|
||||
"""Property 6: Cluster LLR is clamped when using computed n_eff.
|
||||
|
||||
End-to-end: compute n_eff from the LLRs, then compute cluster LLR.
|
||||
The result SHALL still be in [-2.5, 2.5].
|
||||
|
||||
**Validates: Requirements 4.4, 4.5**
|
||||
"""
|
||||
n_eff = compute_n_eff(llrs)
|
||||
cluster_llr = compute_cluster_llr(llrs, n_eff)
|
||||
|
||||
assert -2.5 <= cluster_llr <= 2.5, (
|
||||
f"cluster_llr={cluster_llr} out of [-2.5, 2.5]"
|
||||
)
|
||||
Reference in New Issue
Block a user