feat: math core v3 engine upgrade

This commit is contained in:
Celes Renata
2026-06-27 12:21:41 +00:00
parent 365bc5d4b7
commit b4bf0f2361
34 changed files with 11693 additions and 3 deletions
+176
View File
@@ -0,0 +1,176 @@
"""Unit tests for v3 LLR entropy contradiction score.
Tests compute_v3_contradiction from services.aggregation.contradiction.
Requirements validated: 7.17.7
"""
from __future__ import annotations
import math
import pytest
from services.aggregation.contradiction import compute_v3_contradiction
from services.aggregation.worker import EvidenceCluster
def _make_cluster(cluster_llr: float) -> EvidenceCluster:
"""Helper to create a minimal EvidenceCluster with a given cluster_llr."""
return EvidenceCluster(
cluster_id="test", units=[], llrs=[], n_eff=1.0, cluster_llr=cluster_llr
)
# ---------------------------------------------------------------------------
# 1. All bullish (unidirectional positive) → contradiction = 0.0
# ---------------------------------------------------------------------------
class TestUnidirectionalPositive:
"""When all cluster LLRs are positive, there is no contradiction."""
def test_all_bullish(self):
clusters = [_make_cluster(1.0), _make_cluster(0.5), _make_cluster(0.8)]
assert compute_v3_contradiction(clusters) == 0.0
def test_single_bullish(self):
clusters = [_make_cluster(2.0)]
assert compute_v3_contradiction(clusters) == 0.0
# ---------------------------------------------------------------------------
# 2. All bearish (unidirectional negative) → contradiction = 0.0
# ---------------------------------------------------------------------------
class TestUnidirectionalNegative:
"""When all cluster LLRs are negative, there is no contradiction."""
def test_all_bearish(self):
clusters = [_make_cluster(-1.0), _make_cluster(-0.5)]
assert compute_v3_contradiction(clusters) == 0.0
def test_single_bearish(self):
clusters = [_make_cluster(-2.5)]
assert compute_v3_contradiction(clusters) == 0.0
# ---------------------------------------------------------------------------
# 3. Equal split → high contradiction near 1.0
# ---------------------------------------------------------------------------
class TestEqualSplit:
"""Equal positive and negative evidence produces maximum entropy (H=1.0),
modulated by the volume factor."""
def test_equal_split_moderate_mass(self):
"""LLR +2.0 and -2.0: E_pos=2, E_neg=2, E_total=4.
H_conflict = 1.0 (50/50 split).
volume_factor = 1 - exp(-4/3) ≈ 0.7364.
contradiction ≈ 0.7364.
"""
clusters = [_make_cluster(2.0), _make_cluster(-2.0)]
result = compute_v3_contradiction(clusters)
expected_volume = 1.0 - math.exp(-4.0 / 3.0)
expected = 1.0 * expected_volume # H_conflict = 1.0 for equal split
assert result == pytest.approx(expected, abs=1e-4)
def test_equal_split_large_mass(self):
"""LLR +5.0 and -5.0: E_total=10, volume_factor → ~0.964.
contradiction near 1.0.
"""
clusters = [_make_cluster(5.0), _make_cluster(-5.0)]
result = compute_v3_contradiction(clusters)
expected_volume = 1.0 - math.exp(-10.0 / 3.0)
expected = 1.0 * expected_volume
assert result == pytest.approx(expected, abs=1e-4)
assert result > 0.9 # Near 1.0 due to large evidence mass
# ---------------------------------------------------------------------------
# 4. E_total = 0 → contradiction = 0.0
# ---------------------------------------------------------------------------
class TestZeroEvidence:
"""When all cluster LLRs are zero, E_total=0, contradiction is 0."""
def test_all_zero_llrs(self):
clusters = [_make_cluster(0.0), _make_cluster(0.0), _make_cluster(0.0)]
assert compute_v3_contradiction(clusters) == 0.0
def test_single_zero_llr(self):
clusters = [_make_cluster(0.0)]
assert compute_v3_contradiction(clusters) == 0.0
# ---------------------------------------------------------------------------
# 5. Empty clusters → contradiction = 0.0
# ---------------------------------------------------------------------------
class TestEmptyClusters:
"""Empty cluster list returns 0.0."""
def test_empty_list(self):
assert compute_v3_contradiction([]) == 0.0
# ---------------------------------------------------------------------------
# 6. Small evidence mass → suppressed score (volume_factor effect)
# ---------------------------------------------------------------------------
class TestSmallEvidenceMass:
"""Small E_total leads to a small volume_factor that suppresses the score."""
def test_tiny_equal_split(self):
"""LLR +0.1 and -0.1: E_total=0.2.
H_conflict = 1.0 (equal split).
volume_factor = 1 - exp(-0.2/3) ≈ 0.0645.
contradiction ≈ 0.0645 (suppressed).
"""
clusters = [_make_cluster(0.1), _make_cluster(-0.1)]
result = compute_v3_contradiction(clusters)
expected_volume = 1.0 - math.exp(-0.2 / 3.0)
expected = 1.0 * expected_volume
assert result == pytest.approx(expected, abs=1e-4)
# Confirm suppression: score well below 0.1
assert result < 0.1
# ---------------------------------------------------------------------------
# 7. Large evidence mass → volume_factor approaches 1.0
# ---------------------------------------------------------------------------
class TestLargeEvidenceMass:
"""Large E_total pushes volume_factor near 1.0, so score ≈ H_conflict."""
def test_large_equal_split(self):
"""LLR +5.0 and -5.0: E_total=10, volume_factor ≈ 0.964.
contradiction ≈ 0.964 (near maximum).
"""
clusters = [_make_cluster(5.0), _make_cluster(-5.0)]
result = compute_v3_contradiction(clusters)
# Volume factor should be very close to 1.0
volume_factor = 1.0 - math.exp(-10.0 / 3.0)
assert volume_factor > 0.95
assert result > 0.95
def test_asymmetric_large_mass(self):
"""LLR +4.0 and -1.0: E_pos=4, E_neg=1, E_total=5.
f_pos=0.8, f_neg=0.2.
H_conflict = -0.8×log2(0.8) - 0.2×log2(0.2) ≈ 0.7219.
volume_factor = 1 - exp(-5/3) ≈ 0.8111.
contradiction ≈ 0.586.
"""
clusters = [_make_cluster(4.0), _make_cluster(-1.0)]
result = compute_v3_contradiction(clusters)
f_pos = 4.0 / 5.0
f_neg = 1.0 / 5.0
h_conflict = -f_pos * math.log2(f_pos) - f_neg * math.log2(f_neg)
volume_factor = 1.0 - math.exp(-5.0 / 3.0)
expected = h_conflict * volume_factor
assert result == pytest.approx(expected, abs=1e-4)