Spaces:
Running
Running
github-actions[bot]
Sync backend to Hugging Face Space (commit: 39b5c807918249fa80049d49f4b6a74d6a0ed1fc)
6d86412 | """ | |
| 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 | |