Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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 + 1168-test suite green"

bench-leakage:
PYTHONPATH=src $(PYTHON) -m openamp_foundry.cli bench leakage \
Expand Down
12 changes: 8 additions & 4 deletions src/openamp_foundry/scoring/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions src/openamp_foundry/scoring/synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,19 @@ 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 (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
length = features["length"]
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:
Expand All @@ -33,4 +39,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)
35 changes: 33 additions & 2 deletions tests/test_scoring_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/test_scoring_synthesis.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,49 @@ 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

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
Loading