177 lines
6.4 KiB
Python
177 lines
6.4 KiB
Python
"""Unit tests for v3 LLR entropy contradiction score.
|
||
|
||
Tests compute_v3_contradiction from services.aggregation.contradiction.
|
||
|
||
Requirements validated: 7.1–7.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)
|