diff --git a/tests/test_boman_scorer.py b/tests/test_boman_scorer.py index 021fd58e..fa08d97f 100644 --- a/tests/test_boman_scorer.py +++ b/tests/test_boman_scorer.py @@ -6,6 +6,8 @@ """ from __future__ import annotations +import math + import pytest from openamp_foundry.scoring.boman import ( @@ -71,6 +73,11 @@ def test_mixed_canonical_and_unknown(self): # "KB" → K(2.465) + B(0.0) → mean 1.2325 assert boman_index("KB") == pytest.approx(1.2325, abs=1e-4) + def test_all_non_canonical_returns_zero(self): + # Every residue maps to 0.0 via .get(aa, 0.0) → mean = 0.0 + assert boman_index("XXX") == pytest.approx(0.0, abs=1e-4) + assert boman_index("BBBBB") == pytest.approx(0.0, abs=1e-4) + class TestBomanActivityScore: def test_returns_between_zero_and_one(self): @@ -104,6 +111,15 @@ def test_rounding(self): score = boman_activity_score("KWK") assert score == round(score, 4) + def test_tanh_formula_pinned_against_scorer(self): + # Pin the formula 0.5*(1+tanh(bi/2)) against the actual scorer output. + # K → boman_index = 2.465 (verified in TestBomanIndex::test_single_lysine) + # expected = 0.5 * (1 + tanh(2.465/2)) ≈ 0.9845 + # If the divisor in the scorer were changed from 2.0 to 3.0 this test fails. + k_bi = boman_index("K") + expected = round(0.5 * (1.0 + math.tanh(k_bi / 2.0)), 4) + assert boman_activity_score("K") == pytest.approx(expected, abs=1e-4) + class TestGravyScore: def test_empty_returns_zero(self): diff --git a/tests/test_diversity.py b/tests/test_diversity.py index 1d0f292c..76c31f14 100644 --- a/tests/test_diversity.py +++ b/tests/test_diversity.py @@ -1,5 +1,4 @@ """Tests for src/openamp_foundry/analysis/diversity.py""" -import pytest from openamp_foundry.analysis.diversity import ( cluster_panel, diversity_stats, diff --git a/tests/test_pipeline_filters.py b/tests/test_pipeline_filters.py index 2a57b0d9..12ee3d5b 100644 --- a/tests/test_pipeline_filters.py +++ b/tests/test_pipeline_filters.py @@ -2,8 +2,9 @@ from __future__ import annotations import json +from pathlib import Path - +from openamp_foundry.config import load_config from openamp_foundry.pipeline import _passes_length_filter, run_ranking_pipeline, score_candidates @@ -127,3 +128,32 @@ def test_novelty_filter_excludes_near_duplicates(tmp_path): # But should still be scored and appear in output assert len(rows) == 1 assert rows[0]["scores"]["novelty"] == 0.0 + + +def test_phase3_config_has_stricter_safety_filter_than_pipeline(): + """phase3.yaml max_safety_risk=0.40 (safety>=0.60) vs pipeline.yaml 0.70 (safety>=0.30). + + A candidate with safety=0.50 passes pipeline.yaml but is blocked by phase3.yaml. + """ + repo_root = Path(__file__).parents[1] + pipeline_cfg = load_config(repo_root / "configs" / "pipeline.yaml") + phase3_cfg = load_config(repo_root / "configs" / "phase3.yaml") + + pipeline_min_safety = 1.0 - float(pipeline_cfg["selection"]["max_safety_risk"]) + phase3_min_safety = 1.0 - float(phase3_cfg["selection"]["max_safety_risk"]) + + # Phase3 must be stricter + assert phase3_min_safety > pipeline_min_safety + + # Verify the expected concrete thresholds (max_safety_risk 0.70 → 0.30, 0.40 → 0.60) + assert abs(pipeline_min_safety - 0.30) < 0.01, ( + f"pipeline.yaml min_safety expected ~0.30, got {pipeline_min_safety}" + ) + assert abs(phase3_min_safety - 0.60) < 0.01, ( + f"phase3.yaml min_safety expected ~0.60, got {phase3_min_safety}" + ) + + # A candidate with safety=0.50 straddles the two configs + candidate_safety = 0.50 + assert candidate_safety >= pipeline_min_safety, "Should pass pipeline filter" + assert candidate_safety < phase3_min_safety, "Should fail phase3 filter"