From 47d93620aaa9f9cb89e9284d884c9f34b62f590d Mon Sep 17 00:00:00 2001 From: j Date: Mon, 29 Jun 2026 01:39:22 +0700 Subject: [PATCH] feat(qc): detect N-terminal pyroglutamate cyclization risk (Q1 only) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add PYROGLUTAMATE_RISK flag to presynth_check.py for N-terminal Q: - Q at position 1 cyclises to pGlu at pH 7.4 (t½ hours–days at 37°C), blocks N-terminus, typical 5–50× MIC loss - E1 excluded: requires acid catalysis (pH 2–4), negligible at physiological pH and not supported by AMP-specific quantitative data - Remediation: Nα-acetylation (zero extra cost) or Q1 → K1/R1 substitution (K/R adds trypsin site; only when serum stability is not gated) - Literature: Chelius et al. 2006 Anal Chem 78:2370; Goolcharran & Borchardt 1998 J Pharm Sci 87:425 Guard against empty-sequence edge case (len > 0 before seq[0] access). Add pyroglutamate_risk to SequenceQC.to_dict() output. 7 tests: Q triggers, E does not, all other 18 non-Q AAs do not, Q+QG double-flag interaction, flag content (pGlu, Nα-acetylation, hours–days), to_dict() field present. --- src/openamp_foundry/qc/presynth_check.py | 20 +++++++++ tests/test_presynth_qc.py | 53 ++++++++++++++++++++++++ 2 files changed, 73 insertions(+) diff --git a/src/openamp_foundry/qc/presynth_check.py b/src/openamp_foundry/qc/presynth_check.py index aa2a45fd..266fc3d5 100644 --- a/src/openamp_foundry/qc/presynth_check.py +++ b/src/openamp_foundry/qc/presynth_check.py @@ -118,6 +118,9 @@ class SynthQC: deamidation_sites: list[str] = field(default_factory=list) # e.g. ["N3G", "Q5S"] isomerization_sites: list[str] = field(default_factory=list) # e.g. ["D2G", "D9S"] + # N-terminal pyroglutamate cyclization + pyroglutamate_risk: bool = False # N-terminal Q or E → pGlu (activity loss) + # Tryptophan photolability tryptophan_count: int = 0 trp_photolability_risk: bool = False # ≥ 3 Trp → photooxidation risk under assay lighting @@ -169,6 +172,7 @@ def to_dict(self) -> dict: "chymotrypsin_sites": self.chymotrypsin_sites, "deamidation_sites": self.deamidation_sites, "isomerization_sites": self.isomerization_sites, + "pyroglutamate_risk": self.pyroglutamate_risk, "tryptophan_count": self.tryptophan_count, "trp_photolability_risk": self.trp_photolability_risk, "has_uv_chromophore": self.has_uv_chromophore, @@ -237,6 +241,15 @@ def check_sequence(candidate_id: str, seq: str, mu_h: float = 0.0) -> SynthQC: for m in _ISOMERIZATION_RE.finditer(seq) ] + # N-terminal pyroglutamate cyclization: Q at position 1 cyclises spontaneously to + # pyroglutamic acid (pGlu) at physiological pH (t½ hours–days at 37°C, 5–50× MIC loss). + # The same ring-closure occurs for E1 but requires acid catalysis (pH 2–4); at pH 7.4 the + # E1 rate is negligible (t½ months–years) and is not flagged here to avoid overcalling + # synthesis_difficulty. Nα-acetylation (zero synthesis cost) locks the free amine and + # completely blocks Q1 cyclisation. Literature: Chelius et al. (2006) Anal Chem 78:2370; + # Goolcharran & Borchardt (1998) J Pharm Sci 87:425. + qc.pyroglutamate_risk = len(seq) > 0 and seq[0] == "Q" + # Tryptophan photolability: ≥3 Trp residues → UV-sensitive under assay lighting. # Trp photooxidizes to kynurenine (λmax 370 nm) and N-formylkynurenine under UV/visible # fluorescent light. Peptides with ≥3 Trp lose >20% activity within 4h under standard @@ -359,6 +372,13 @@ def check_sequence(candidate_id: str, seq: str, mu_h: float = 0.0) -> SynthQC: qc.flags.append(f"DEAMIDATION_RISK: {', '.join(qc.deamidation_sites)} — avoid >pH 7.5 storage; use pH 5–6 lyophilization buffer") if qc.isomerization_sites: qc.flags.append(f"ISOMERIZATION_RISK: {', '.join(qc.isomerization_sites)} — Asp→β-Asp rearrangement at neutral pH; store lyophilized at −20°C") + if qc.pyroglutamate_risk: + qc.flags.append( + "PYROGLUTAMATE_RISK: N-terminal Q cyclises to pGlu (t½ hours–days at 37°C, pH 7.4); " + "blocks N-terminus, typically 5–50× MIC loss — specify N-terminal Ac (Nα-acetylation) " + "in synthesis order (zero extra cost) to prevent cyclisation; or substitute Q1 → K1/R1 " + "(note: K1/R1 adds a trypsin site; only use if serum stability is not gated)" + ) if qc.charge_ph74 < 2.0: qc.flags.append(f"LOW_CHARGE (pH7.4={qc.charge_ph74:.1f}): reduced membrane affinity") if qc.c_amidation_recommended: diff --git a/tests/test_presynth_qc.py b/tests/test_presynth_qc.py index 439d5b91..6d48f1d8 100644 --- a/tests/test_presynth_qc.py +++ b/tests/test_presynth_qc.py @@ -107,6 +107,59 @@ def test_met_flag_in_flags_list(self): assert "Nle" in met_flags[0], "Met flag must recommend Nle substitution (regression guard for PR #102)" +# --------------------------------------------------------------------------- +# Pyroglutamate risk +# --------------------------------------------------------------------------- + +class TestPyroglutamate: + def test_q_terminal_sets_risk(self): + qc = check_sequence("q", "QRLMKKIGSAIKFL") + assert qc.pyroglutamate_risk + + def test_no_risk_for_all_non_q_starts(self): + # All canonical AAs except Q — includes E (negligible at pH 7.4, not flagged) + non_q = "ACDEFGHIKLMNPRSTUVWY" + for aa in non_q: + if aa == "U": + continue # selenocysteine not canonical here + qc = check_sequence("x", aa + "RLMKKIGSAIKFL") + assert not qc.pyroglutamate_risk, f"False positive for N-terminal {aa}" + + def test_e_terminal_does_not_set_risk(self): + # E→pGlu requires acid catalysis (pH 2–4); negligible at physiological pH + qc = check_sequence("e", "ERLMKKIGSAIKFL") + assert not qc.pyroglutamate_risk + + def test_pyroglutamate_flag_emitted_for_q(self): + qc = check_sequence("q", "QRLMKKIGSAIKFL") + pglu_flags = [f for f in qc.flags if "PYROGLUTAMATE" in f] + assert len(pglu_flags) == 1 + assert "pGlu" in pglu_flags[0] + assert "Nα-acetylation" in pglu_flags[0] + assert "hours–days" in pglu_flags[0] + + def test_pyroglutamate_not_flagged_for_internal_q(self): + # Internal Q triggers deamidation (if followed by G/S), not pyroglutamate + qc = check_sequence("x", "KQRLMKKIGSAIKFL") + assert not qc.pyroglutamate_risk + + def test_q_with_qg_motif_gets_both_pyroglutamate_and_deamidation_flags(self): + # QGKLRK: Q1 → pyroglutamate risk AND Q1G deamidation (two independent mechanisms) + qc = check_sequence("qq", "QGKLRK") + assert qc.pyroglutamate_risk + assert any("Q1G" in s for s in qc.deamidation_sites) + pglu_flags = [f for f in qc.flags if "PYROGLUTAMATE" in f] + deam_flags = [f for f in qc.flags if "DEAMIDATION" in f] + assert len(pglu_flags) == 1 + assert len(deam_flags) == 1 + + def test_pyroglutamate_in_to_dict(self): + qc = check_sequence("q", "QRLMKKIGSAIKFL") + d = qc.to_dict() + assert "pyroglutamate_risk" in d + assert d["pyroglutamate_risk"] is True + + # --------------------------------------------------------------------------- # Trypsin/chymotrypsin sites # ---------------------------------------------------------------------------