Spaces:
Running
Running
File size: 11,202 Bytes
6d86412 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | """
Math Engine Validation Suite β Orsync Scenarist v7.0 PRD Constraints
=============================================================
Targets: vectorizer (PCA/EVR), heatmap (Mahalanobis), quality_gate (KL Divergence)
"""
from __future__ import annotations
import numpy as np
import pytest
from numpy.testing import assert_allclose
from sklearn.decomposition import PCA
from backend.app.services.campaign_vectorizer import FEATURE_KEYS
from backend.app.services.vectorizer import vectorize_doctors
from backend.app.services.heatmap import mahalanobis_distance, build_heatmap
from backend.app.services.quality_gate import (
kl_divergence,
mmd_rbf,
pass_quality_gate,
regenerate_until_quality,
)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 1. PCA / EVR β Weighted-average activates strictly when PC1 < 0.80
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def _make_records(n: int, dim: int, dominant: bool) -> list[dict[str, float]]:
"""Generate synthetic doctor profiles.
When *dominant* is True, feature_0 carries ~99 % of variance so
PC1-EVR will be > 0.80. When False, variance is spread uniformly
so PC1-EVR will be < 0.80.
Data is centered (mean ~0) with moderate scale so the vectorizer's
skewness guard (|skew| > 2) does NOT trigger log1p β keeping the
variance structure intact for the PCA assertion.
"""
rng = np.random.RandomState(42)
if dominant:
latent = rng.randn(n, 1)
loadings = rng.randn(1, dim) * 10.0
data = latent @ loadings + rng.randn(n, dim) * 0.1 + 500.0
else:
data = rng.randn(n, dim) * 10.0 + 500.0
active_keys = FEATURE_KEYS[:dim]
records: list[dict[str, float]] = []
for row in data:
record = {key: 500.0 for key in FEATURE_KEYS}
for j, key in enumerate(active_keys):
record[key] = float(row[j])
records.append(record)
return records
class TestPCAWeightedAverageActivation:
"""PRD Β§2: weighted-average path activates IFF PC1 EVR < 0.80."""
def test_pc1_dominant_uses_first_component_only(self):
records = _make_records(n=50, dim=6, dominant=True)
result = vectorize_doctors(records)
evr = result["explained_variance_ratio"]
assert evr[0] >= 0.80, f"Expected dominant PC1 but got EVR[0]={evr[0]:.4f}"
vectors = np.array(result["vectors"])
assert vectors.shape[1] == 1
def test_pc1_weak_activates_weighted_average(self):
records = _make_records(n=50, dim=6, dominant=False)
result = vectorize_doctors(records)
evr = result["explained_variance_ratio"]
assert evr[0] < 0.80, f"Expected weak PC1 but got EVR[0]={evr[0]:.4f}"
vectors = np.array(result["vectors"])
assert vectors.shape[1] == 1
def test_weighted_average_formula_matches_manual_calculation(self):
records = _make_records(n=40, dim=5, dominant=False)
result = vectorize_doctors(records)
evr = np.array(result["explained_variance_ratio"])
assert evr[0] < 0.80
numeric_keys = list(FEATURE_KEYS)
raw = np.array(
[[float(r.get(k, 0.0)) for k in numeric_keys] for r in records],
dtype=float,
)
from scipy.stats import skew as sp_skew
from sklearn.preprocessing import MinMaxScaler, RobustScaler
if np.any(np.abs(sp_skew(raw, axis=0, nan_policy="omit")) > 2.0):
x_scaled = np.log1p(np.clip(raw, 0.0, None))
else:
x_scaled = RobustScaler().fit_transform(raw)
pca = PCA(n_components=min(x_scaled.shape))
x_pca = pca.fit_transform(x_scaled)
evr_manual = pca.explained_variance_ratio_
weighted = (x_pca * evr_manual).sum(axis=1, keepdims=True) / max(
float(evr_manual.sum()), 1e-9
)
bounded = MinMaxScaler(feature_range=(0.0, 1.0)).fit_transform(weighted)
assert_allclose(np.array(result["vectors"]), bounded, atol=1e-9)
def test_empty_records_returns_empty(self):
result = vectorize_doctors([])
assert result["vectors"] == []
def test_evr_boundary_at_exactly_080(self):
"""The threshold is strictly < 0.80; at exactly 0.80 we use PC1 path."""
rng = np.random.RandomState(99)
for _ in range(20):
records = _make_records(n=60, dim=4, dominant=True)
result = vectorize_doctors(records)
evr = result["explained_variance_ratio"]
if abs(evr[0] - 0.80) < 0.01:
break
assert result["vectors"]
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 2. Mahalanobis Distance β Inverse Covariance Matrix (Ξ£β»ΒΉ)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestMahalanobisDistance:
"""PRD Β§3: correct implementation of Ξ£β»ΒΉ for campaignβcluster distance."""
def test_identity_covariance_equals_euclidean(self):
x = np.array([3.0, 4.0])
mu = np.array([0.0, 0.0])
sigma = np.eye(2)
d = mahalanobis_distance(x, mu, sigma)
assert_allclose(d, 5.0, atol=1e-9)
def test_scaled_covariance(self):
x = np.array([3.0, 4.0])
mu = np.array([0.0, 0.0])
sigma = np.array([[9.0, 0.0], [0.0, 16.0]])
d = mahalanobis_distance(x, mu, sigma)
expected = np.sqrt((3.0**2) / 9.0 + (4.0**2) / 16.0)
assert_allclose(d, expected, atol=1e-9)
def test_correlated_covariance(self):
x = np.array([1.0, 1.0])
mu = np.array([0.0, 0.0])
sigma = np.array([[1.0, 0.5], [0.5, 1.0]])
sigma_inv = np.linalg.inv(sigma)
delta = x - mu
expected = float(np.sqrt(delta @ sigma_inv @ delta))
d = mahalanobis_distance(x, mu, sigma)
assert_allclose(d, expected, atol=1e-9)
def test_singular_covariance_uses_pseudoinverse(self):
x = np.array([1.0, 2.0])
mu = np.array([0.0, 0.0])
sigma = np.array([[1.0, 1.0], [1.0, 1.0]]) # rank 1
d = mahalanobis_distance(x, mu, sigma)
assert np.isfinite(d)
def test_heatmap_ranking_order_matches_distance(self):
campaign = [1.0, 0.0]
near = [1.0, 0.1]
far = [10.0, 10.0]
cov = [[1.0, 0.0], [0.0, 1.0]]
result = build_heatmap(campaign, [near, far], [cov, cov])
ranking = result["ranking"]
assert ranking[0]["distance"] < ranking[1]["distance"]
assert ranking[0]["cluster_id"] == 0
def test_heatmap_probabilities_sum_to_one(self):
campaign = [2.0, 3.0]
centroids = [[0.0, 0.0], [5.0, 5.0], [2.0, 3.0]]
cov = [[1.0, 0.0], [0.0, 1.0]]
result = build_heatmap(campaign, centroids, [cov] * 3)
total_prob = sum(r["probability"] for r in result["ranking"])
assert_allclose(total_prob, 1.0, atol=1e-9)
def test_sigma_inv_manual_matches_numpy(self):
sigma = np.array([[2.0, 1.0], [1.0, 3.0]])
sigma_inv_expected = np.linalg.inv(sigma)
sigma_inv_actual = np.linalg.pinv(sigma)
assert_allclose(sigma_inv_actual, sigma_inv_expected, atol=1e-9)
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# 3. Quality Gate β KL Divergence rejection + regeneration
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class TestKLDivergenceQualityGate:
"""PRD Β§4: D_KL > threshold βΉ reject and regenerate immediately."""
def test_identical_distributions_have_near_zero_kl(self):
rng = np.random.RandomState(0)
data = rng.randn(500)
d_kl = kl_divergence(data, data.copy())
assert d_kl < 0.01
def test_divergent_distributions_have_high_kl(self):
rng = np.random.RandomState(0)
p = rng.randn(500)
q = rng.randn(500) + 10.0
d_kl = kl_divergence(p, q)
assert d_kl > 0.05
def test_quality_gate_accepts_when_below_threshold(self):
rng = np.random.RandomState(7)
ref = rng.randn(100, 1).tolist()
syn = (rng.randn(100, 1) * 1.01).tolist()
verdict = pass_quality_gate(ref, syn, kl_threshold=5.0)
assert verdict["accepted"] is True
assert verdict["kl_divergence"] <= 5.0
def test_quality_gate_rejects_when_above_threshold(self):
rng = np.random.RandomState(7)
ref = rng.randn(200, 1).tolist()
syn = (rng.randn(200, 1) + 50.0).tolist()
verdict = pass_quality_gate(ref, syn, kl_threshold=0.01)
assert verdict["accepted"] is False
assert verdict["kl_divergence"] > 0.01
def test_rejection_triggers_regeneration(self):
"""Simulate D_KL >> threshold: regenerate_until_quality must
accumulate rejection entries before (possibly) accepting."""
rng = np.random.RandomState(42)
ref = rng.randn(50, 2).tolist()
mu_far = [100.0, 100.0]
sigma_tight = [[0.001, 0.0], [0.0, 0.001]]
result = regenerate_until_quality(
mu=mu_far,
sigma=sigma_tight,
reference_vectors=ref,
max_attempts=5,
kl_threshold=0.001,
)
assert len(result["rejections"]) > 0, "Expected at least one rejection event"
for rej in result["rejections"]:
assert rej["kl_divergence"] > 0.001
def test_regeneration_accepts_matching_distribution(self):
rng = np.random.RandomState(42)
ref = rng.multivariate_normal([0.0, 0.0], [[1.0, 0.0], [0.0, 1.0]], size=200).tolist()
result = regenerate_until_quality(
mu=[0.0, 0.0],
sigma=[[1.0, 0.0], [0.0, 1.0]],
reference_vectors=ref,
max_attempts=10,
kl_threshold=1.0,
)
assert result["accepted"] is True
assert len(result["vector_samples"]) > 0
def test_mmd_rbf_identical_is_near_zero(self):
rng = np.random.RandomState(0)
x = rng.randn(50, 3)
assert mmd_rbf(x, x.copy()) < 1e-9
def test_mmd_rbf_different_is_positive(self):
rng = np.random.RandomState(0)
x = rng.randn(50, 3)
y = rng.randn(50, 3) + 5.0
assert mmd_rbf(x, y) > 0.0
def test_kl_divergence_is_non_negative(self):
rng = np.random.RandomState(1)
p = rng.randn(300)
q = rng.randn(300) * 2.0
assert kl_divergence(p, q) >= 0.0
|