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
20 changes: 20 additions & 0 deletions src/openamp_foundry/qc/presynth_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
53 changes: 53 additions & 0 deletions tests/test_presynth_qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ---------------------------------------------------------------------------
Expand Down
Loading