From f5602a736ac61e3a01038af820e547c6d5524ef0 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Jun 2026 08:43:56 +0700 Subject: [PATCH 1/3] test: boman edge cases and phase3 vs pipeline safety threshold contract (#23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - boman_index: all-non-canonical (XXX/BBBBB) → 0.0 via .get(aa, 0.0) fallback - boman_activity_score: verify tanh formula constant (0.5*(1+tanh(bi/2)) ≈ 0.7311 at bi=1.0) - test_phase3_config_has_stricter_safety_filter_than_pipeline: asserts phase3.yaml max_safety_risk=0.40 (safety≥0.60) is stricter than pipeline.yaml 0.70 (safety≥0.30); a candidate with safety=0.50 passes pipeline but is blocked by phase3 --- tests/test_boman_scorer.py | 20 ++++++++++++++++++++ tests/test_pipeline_filters.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/test_boman_scorer.py b/tests/test_boman_scorer.py index 021fd58e..f9f447f3 100644 --- a/tests/test_boman_scorer.py +++ b/tests/test_boman_scorer.py @@ -71,6 +71,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 +109,21 @@ def test_rounding(self): score = boman_activity_score("KWK") assert score == round(score, 4) + def test_amp_threshold_bi_maps_to_expected_score(self): + # _AMP_THRESHOLD = 1.0 → boman_activity_score at bi=1.0 + # score = 0.5 * (1 + tanh(1.0/2)) = 0.5 * (1 + tanh(0.5)) ≈ 0.7311 + # K has boman_index 2.465; we need bi=1.0 via a mixed sequence. + # Verify the formula directly: boman_activity_score of any seq with bi=1.0 + # "KG" → (2.465 + 0.0) / 2 = 1.2325; need to use the formula knowledge. + # Instead verify monotone: bi=1.0 is above neutral (0.5) and below bi=2 score. + import math + expected = round(0.5 * (1.0 + math.tanh(1.0 / 2.0)), 4) + assert abs(expected - 0.7311) < 0.001 + # A 2-residue sequence "KG" has bi=1.2325; score must exceed expected + score_kg = boman_activity_score("KG") + assert score_kg > 0.5 # above neutral + assert score_kg > expected # K/G bi=1.2325 > 1.0 → higher score + class TestGravyScore: def test_empty_returns_zero(self): diff --git a/tests/test_pipeline_filters.py b/tests/test_pipeline_filters.py index 2a57b0d9..39fe6eea 100644 --- a/tests/test_pipeline_filters.py +++ b/tests/test_pipeline_filters.py @@ -127,3 +127,35 @@ 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. + """ + from pathlib import Path + from openamp_foundry.config import load_config + + 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" From 069cc1bbfc13089503126117f5e408fd71666837 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Jun 2026 08:48:31 +0700 Subject: [PATCH 2/3] fix: address code review issues in boman edge / config switching tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move `import math` to module top level (ruff PLC0415) - Replace vacuous tanh assertion (tested math.tanh locally) with a test that actually calls boman_activity_score("K") and compares to formula-derived expected value — any change to bi/2.0 divisor will now be caught - Move `from pathlib import Path` and `from openamp_foundry.config import load_config` from inside test function to module top level (ruff PLC0415) --- tests/test_boman_scorer.py | 24 ++++++++++-------------- tests/test_pipeline_filters.py | 6 ++---- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/tests/test_boman_scorer.py b/tests/test_boman_scorer.py index f9f447f3..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 ( @@ -109,20 +111,14 @@ def test_rounding(self): score = boman_activity_score("KWK") assert score == round(score, 4) - def test_amp_threshold_bi_maps_to_expected_score(self): - # _AMP_THRESHOLD = 1.0 → boman_activity_score at bi=1.0 - # score = 0.5 * (1 + tanh(1.0/2)) = 0.5 * (1 + tanh(0.5)) ≈ 0.7311 - # K has boman_index 2.465; we need bi=1.0 via a mixed sequence. - # Verify the formula directly: boman_activity_score of any seq with bi=1.0 - # "KG" → (2.465 + 0.0) / 2 = 1.2325; need to use the formula knowledge. - # Instead verify monotone: bi=1.0 is above neutral (0.5) and below bi=2 score. - import math - expected = round(0.5 * (1.0 + math.tanh(1.0 / 2.0)), 4) - assert abs(expected - 0.7311) < 0.001 - # A 2-residue sequence "KG" has bi=1.2325; score must exceed expected - score_kg = boman_activity_score("KG") - assert score_kg > 0.5 # above neutral - assert score_kg > expected # K/G bi=1.2325 > 1.0 → higher score + 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: diff --git a/tests/test_pipeline_filters.py b/tests/test_pipeline_filters.py index 39fe6eea..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 @@ -134,9 +135,6 @@ def test_phase3_config_has_stricter_safety_filter_than_pipeline(): A candidate with safety=0.50 passes pipeline.yaml but is blocked by phase3.yaml. """ - from pathlib import Path - from openamp_foundry.config import load_config - repo_root = Path(__file__).parents[1] pipeline_cfg = load_config(repo_root / "configs" / "pipeline.yaml") phase3_cfg = load_config(repo_root / "configs" / "phase3.yaml") From 32daf1a71317921dc3c7cc5fb2795f9992486f85 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Jun 2026 08:49:52 +0700 Subject: [PATCH 3/3] fix: remove unused pytest import in test_diversity.py (ruff F401) --- tests/test_diversity.py | 1 - 1 file changed, 1 deletion(-) 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,