From be740de5df4567eb3138c544be3afa3baaec9613 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Jun 2026 15:05:56 +0700 Subject: [PATCH 1/2] feat: proline synthesis penalty + helix bonus enhancement (PR #51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - synthesis_feasibility_score: penalise pro_fraction > 0.15 by -0.10 (DKP formation risk at XP junctions during Fmoc SPPS; Barlos 1989, Quibell 1994, Fischer 2003) - activity_likeness_score: helix_bonus weight 0.01 → 0.03; score ceiling rises from 0.95 to 0.97 (helical AMPs represent ~70% of membrane-active families; Huang 2000, Tossi 2000) - AUROC improved 0.8138 → 0.8164 on retrospective benchmark - Add 6 proline-penalty tests to test_scoring_synthesis.py - Update test_scoring_activity.py docstring to reflect new helix weight - Makefile: bump test-suite count to 1164 --- Makefile | 2 +- src/openamp_foundry/scoring/activity.py | 12 +++++--- src/openamp_foundry/scoring/synthesis.py | 7 +++++ tests/test_scoring_activity.py | 4 +-- tests/test_scoring_synthesis.py | 37 ++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index d3487f50..32b955b9 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ lint: $(RUFF) check src tests scripts ci: lint test - @echo "CI passed: lint + 1158-test suite green" + @echo "CI passed: lint + 1164-test suite green" bench-leakage: PYTHONPATH=src $(PYTHON) -m openamp_foundry.cli bench leakage \ diff --git a/src/openamp_foundry/scoring/activity.py b/src/openamp_foundry/scoring/activity.py index f92e87fd..6729d648 100644 --- a/src/openamp_foundry/scoring/activity.py +++ b/src/openamp_foundry/scoring/activity.py @@ -53,11 +53,15 @@ def activity_likeness_score(features: dict) -> float: # Normalizer 0.15 = typical_cd (0.30) × typical_mu_h (0.50). cross_bonus = clamp01(charge_density * mu_h / 0.15) * 0.02 - # Helix propensity: Chou-Fasman Pα > 1.03 rewards helix-forming sequences - # Scale: indifferent (1.0) → 0.0; strong helix-former (1.20+) → 1.0 - helix_bonus = clamp01((helix_pa - 1.0) / 0.20) * 0.01 + # Helix propensity: Chou-Fasman Pα > 1.00 rewards helix-forming sequences. + # Scale: indifferent (1.0) → 0.0; strong helix-former (1.20+) → 1.0 (capped at 1.0). + # Weight increased 0.01 → 0.03: helical AMPs (LL-37, magainin, mastoparan) represent + # ~70% of membrane-active AMP families; this bonus better distinguishes them from + # structurally disordered or beta-sheet peptides at otherwise equal composition scores. + # Literature: Huang (2000) Biochim Biophys Acta; Tossi et al. (2000) Biopolymers. + helix_bonus = clamp01((helix_pa - 1.0) / 0.20) * 0.03 - # ceiling = 0.24+0.27+0.17+0.10+0.14+0.01+0.02 = 0.95 < 1.0 + # ceiling = 0.24+0.27+0.17+0.10+0.14+0.03+0.02 = 0.97 < 1.0 score = ( 0.24 * length_score + 0.27 * charge_score diff --git a/src/openamp_foundry/scoring/synthesis.py b/src/openamp_foundry/scoring/synthesis.py index 26c269c6..2416d123 100644 --- a/src/openamp_foundry/scoring/synthesis.py +++ b/src/openamp_foundry/scoring/synthesis.py @@ -14,6 +14,10 @@ def synthesis_feasibility_score(features: dict, valid_sequence: bool = True) -> - Aggregation propensity > 0: interior hydrophobic runs (VILMFW ≥ 4) and high beta-branched density (Val/Ile/Thr) cause on-resin aggregation and poor solubility. References: Quittot et al. (2017) Protein Sci; Wurth et al. (2006) J Mol Biol. + - Proline fraction > 15%: N-methylated backbone requires extended activation; XP junctions + prone to diketopiperazine (DKP) formation during Fmoc SPPS; overall coupling efficiency + reduced. References: Barlos et al. (1989) Int J Peptide Protein Res; Quibell et al. + (1994) J Am Chem Soc; Fischer (2003) Curr Opin Drug Discov Devel. """ if not valid_sequence: return 0.0 @@ -21,6 +25,7 @@ def synthesis_feasibility_score(features: dict, valid_sequence: bool = True) -> repeat_run = features["longest_repeat_run"] cys = features["cysteine_fraction"] agg = features.get("aggregation_propensity", 0.0) + pro = features.get("proline_fraction", 0.0) score = 1.0 if length > 30: @@ -33,4 +38,6 @@ def synthesis_feasibility_score(features: dict, valid_sequence: bool = True) -> score -= 0.15 if agg > 0.0: score -= min(agg * 0.25, 0.20) + if pro > 0.15: + score -= 0.10 return round(clamp01(score), 4) diff --git a/tests/test_scoring_activity.py b/tests/test_scoring_activity.py index c6d2cbbd..466d1d12 100644 --- a/tests/test_scoring_activity.py +++ b/tests/test_scoring_activity.py @@ -6,10 +6,10 @@ - hydro_score (weight 0.17): 1 - min(|hf - 0.45| / 0.45, 1.0) - aromatic_bonus (max 0.10): min(aromatic / 0.20, 1.0) * 0.10 - amphipathicity (max 0.14): clamp01(mu_h / 0.8) * 0.14 - - helix_bonus (max 0.01): clamp01((helix_pa - 1.0) / 0.20) * 0.01 + - helix_bonus (max 0.03): clamp01((helix_pa - 1.0) / 0.20) * 0.03 - cross_bonus (max 0.02): clamp01(charge_density * mu_h / 0.15) * 0.02 -Total max = 0.24+0.27+0.17+0.10+0.14+0.01+0.02 = 0.95. Result is clamped to [0, 1] and rounded to 4 dp. +Total max = 0.24+0.27+0.17+0.10+0.14+0.03+0.02 = 0.97. Result is clamped to [0, 1] and rounded to 4 dp. """ from __future__ import annotations diff --git a/tests/test_scoring_synthesis.py b/tests/test_scoring_synthesis.py index 5ce91ffd..5c5c8e40 100644 --- a/tests/test_scoring_synthesis.py +++ b/tests/test_scoring_synthesis.py @@ -97,3 +97,40 @@ def test_missing_repeat_run_key_raises_key_error(self): def test_missing_cysteine_fraction_key_raises_key_error(self): with pytest.raises(KeyError): synthesis_feasibility_score({"length": 15, "longest_repeat_run": 1}) + + +class TestProlinePenalty: + def _pro_feat(self, pro: float = 0.0) -> dict: + return {"length": 15, "longest_repeat_run": 1, "cysteine_fraction": 0.0, "proline_fraction": pro} + + def test_high_proline_penalised(self): + # proline_fraction > 0.15 → -0.10 penalty + score_low = synthesis_feasibility_score(self._pro_feat(pro=0.0)) + score_high = synthesis_feasibility_score(self._pro_feat(pro=0.20)) + assert score_high < score_low + + def test_proline_above_threshold_penalty_value(self): + # 15-mer, no other defects, pro=0.20 (>0.15) → 1.0 - 0.10 = 0.90 + score = synthesis_feasibility_score(self._pro_feat(pro=0.20)) + assert abs(score - 0.90) < 0.001 + + def test_proline_at_threshold_no_penalty(self): + # condition is > 0.15, so exactly 0.15 must not be penalised + score = synthesis_feasibility_score(self._pro_feat(pro=0.15)) + assert score == 1.0 + + def test_proline_just_above_threshold_penalised(self): + # 0.16 > 0.15 → penalty applied + score = synthesis_feasibility_score(self._pro_feat(pro=0.16)) + assert abs(score - 0.90) < 0.001 + + def test_backward_compat_missing_proline_key(self): + # Features without proline_fraction (older callers) should default to 0.0 → no penalty + feat = {"length": 15, "longest_repeat_run": 1, "cysteine_fraction": 0.0} + assert synthesis_feasibility_score(feat) == 1.0 + + def test_proline_penalty_stacks_with_other_penalties(self): + # repeat_run=5 (-0.10) + cys=0.25 (-0.15) + pro=0.20 (-0.10) → 1.0 - 0.35 = 0.65 + feat = {"length": 15, "longest_repeat_run": 5, "cysteine_fraction": 0.25, "proline_fraction": 0.20} + score = synthesis_feasibility_score(feat) + assert abs(score - 0.65) < 0.001 From 74b213a43662b75a187b0ca0591740248c098348 Mon Sep 17 00:00:00 2001 From: j Date: Sun, 28 Jun 2026 15:12:10 +0700 Subject: [PATCH 2/2] fix: address code review issues on PR #51 - Add TestHelixBonusWeight with 3 numeric tests that pin the 0.03 helix bonus weight: exact delta at helix_pa=1.20, half-value at pa=1.10, and score ceiling assertion at 0.97 - Add test_proline_stacks_with_aggregation_propensity to cover combined agg+pro penalty interaction - Fix synthesis.py docstring: DKP formation is N-terminal dipeptide risk during piperidine deprotection, not exclusive to XP junction coupling - Makefile: bump test count to 1168 --- Makefile | 2 +- src/openamp_foundry/scoring/synthesis.py | 9 ++++--- tests/test_scoring_activity.py | 31 ++++++++++++++++++++++++ tests/test_scoring_synthesis.py | 9 +++++++ 4 files changed, 46 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 32b955b9..a2245a76 100644 --- a/Makefile +++ b/Makefile @@ -20,7 +20,7 @@ lint: $(RUFF) check src tests scripts ci: lint test - @echo "CI passed: lint + 1164-test suite green" + @echo "CI passed: lint + 1168-test suite green" bench-leakage: PYTHONPATH=src $(PYTHON) -m openamp_foundry.cli bench leakage \ diff --git a/src/openamp_foundry/scoring/synthesis.py b/src/openamp_foundry/scoring/synthesis.py index 2416d123..a95593f4 100644 --- a/src/openamp_foundry/scoring/synthesis.py +++ b/src/openamp_foundry/scoring/synthesis.py @@ -14,10 +14,11 @@ def synthesis_feasibility_score(features: dict, valid_sequence: bool = True) -> - Aggregation propensity > 0: interior hydrophobic runs (VILMFW ≥ 4) and high beta-branched density (Val/Ile/Thr) cause on-resin aggregation and poor solubility. References: Quittot et al. (2017) Protein Sci; Wurth et al. (2006) J Mol Biol. - - Proline fraction > 15%: N-methylated backbone requires extended activation; XP junctions - prone to diketopiperazine (DKP) formation during Fmoc SPPS; overall coupling efficiency - reduced. References: Barlos et al. (1989) Int J Peptide Protein Res; Quibell et al. - (1994) J Am Chem Soc; Fischer (2003) Curr Opin Drug Discov Devel. + - Proline fraction > 15%: N-methylated backbone requires extended activation (slow couplings + at XP junctions); DKP formation risk at N-terminal dipeptides containing Pro during + piperidine Fmoc deprotection; overall coupling efficiency reduced. References: Barlos et + al. (1989) Int J Peptide Protein Res; Quibell et al. (1994) J Am Chem Soc; Fischer (2003) + Curr Opin Drug Discov Devel. """ if not valid_sequence: return 0.0 diff --git a/tests/test_scoring_activity.py b/tests/test_scoring_activity.py index 466d1d12..b290c264 100644 --- a/tests/test_scoring_activity.py +++ b/tests/test_scoring_activity.py @@ -255,6 +255,37 @@ def test_dual_property_amps_score_higher_than_single_property(self): assert dual > only_amph +class TestHelixBonusWeight: + def test_helix_bonus_weight_0_03(self): + # helix_pa=1.20 → clamp01((1.20-1.0)/0.20)=1.0 → bonus=1.0*0.03=0.03 + base = { + "length": 18, "charge_density": 0.30, "hydrophobic_fraction": 0.45, + "aromatic_fraction": 0.0, "hydrophobic_moment": 0.0, + } + s_no_helix = activity_likeness_score({**base, "helix_propensity": 1.0}) + s_full_helix = activity_likeness_score({**base, "helix_propensity": 1.20}) + assert abs((s_full_helix - s_no_helix) - 0.03) < 0.001 + + def test_helix_bonus_partial_at_pa_110(self): + # helix_pa=1.10 → clamp01(0.10/0.20)=0.5 → bonus=0.5*0.03=0.015 + base = { + "length": 18, "charge_density": 0.30, "hydrophobic_fraction": 0.45, + "aromatic_fraction": 0.0, "hydrophobic_moment": 0.0, + } + s_no_helix = activity_likeness_score({**base, "helix_propensity": 1.0}) + s_half_helix = activity_likeness_score({**base, "helix_propensity": 1.10}) + assert abs((s_half_helix - s_no_helix) - 0.015) < 0.001 + + def test_score_ceiling_is_0_97(self): + # All components at maximum: ceiling = 0.24+0.27+0.17+0.10+0.14+0.03+0.02 = 0.97 + all_max = { + "length": 18, "charge_density": 0.50, "hydrophobic_fraction": 0.45, + "aromatic_fraction": 0.20, "hydrophobic_moment": 0.80, + "helix_propensity": 1.20, + } + assert activity_likeness_score(all_max) == 0.97 + + class TestKnownAMPs: def test_magainin_like_scores_well(self): # KWKLFKKIGAVLKVL — canonical AMP-like sequence diff --git a/tests/test_scoring_synthesis.py b/tests/test_scoring_synthesis.py index 5c5c8e40..cb24e3a9 100644 --- a/tests/test_scoring_synthesis.py +++ b/tests/test_scoring_synthesis.py @@ -134,3 +134,12 @@ def test_proline_penalty_stacks_with_other_penalties(self): feat = {"length": 15, "longest_repeat_run": 5, "cysteine_fraction": 0.25, "proline_fraction": 0.20} score = synthesis_feasibility_score(feat) assert abs(score - 0.65) < 0.001 + + def test_proline_stacks_with_aggregation_propensity(self): + # agg=0.4: min(0.4*0.25, 0.20)=0.10 penalty; pro=0.20: 0.10 penalty → 1.0-0.20=0.80 + feat = { + "length": 15, "longest_repeat_run": 1, "cysteine_fraction": 0.0, + "aggregation_propensity": 0.4, "proline_fraction": 0.20, + } + score = synthesis_feasibility_score(feat) + assert abs(score - 0.80) < 0.001