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: 2 additions & 0 deletions docs/WET_LAB_HANDOFF.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ before ordering. The report flags:
| `MET×N` | Met oxidation to sulfoxide in air | Order with >95% purity; store at −80°C; re-check purity on thaw |
| `HYDROPHOBIC_RUN (XXXX)` | Aggregation; poor aqueous solubility | Test solubility at 1 mM in PBS before assay; add 0.1% DMSO if needed |
| `TRYPSIN_SITES×N` (N > 2) | Serum degradation < 2h | Use serum-free media for initial MIC; add protease inhibitors if needed |
| `WAVE2_D_AMINO` | Wave 2 D-amino acid sites identified | See `wave2_d_substitutions` field — substitute D-Lys or D-Arg at listed positions in Wave 2 synthesis to extend serum t½ by 3–10× |
| `N_ACETYLATION_RECOMMENDED` | N-terminal aminopeptidase exposure | **Request "N-terminal acetylation (Ac-)"** in synthesis order — zero cost, blocks aminopeptidase entry |
| `DEAMIDATION_RISK` | N→D conversion at pH > 7.5 | Use pH 7.0 buffer; avoid long storage at room temp |
| `LOW_CHARGE` | Reduced membrane affinity | Lower expected potency; may need higher test concentrations |
| `LONG_PEPTIDE (>30aa)` | SPPS yield risk | Expect lower crude purity; order extra crude for purification |
Expand Down
55 changes: 54 additions & 1 deletion src/openamp_foundry/qc/presynth_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,13 @@ class SynthQC:
c_amidation_recommended: bool = False # request NH₂ C-terminus instead of COOH
c_amidation_reason: str = ""

# N-terminal acetylation
n_acetylation_recommended: bool = False # request Nα-Ac when interior trypsin sites exist
n_acetylation_reason: str = ""

# Wave 2 D-amino acid substitution guidance
wave2_d_substitutions: list[str] = field(default_factory=list)

# Overall flag
flags: list[str] = field(default_factory=list)
synthesis_difficulty: str = "LOW" # LOW / MODERATE / HIGH
Expand Down Expand Up @@ -143,6 +150,9 @@ def to_dict(self) -> dict:
"hemolysis_start_conc": self.hemolysis_start_conc,
"c_amidation_recommended": self.c_amidation_recommended,
"c_amidation_reason": self.c_amidation_reason,
"n_acetylation_recommended": self.n_acetylation_recommended,
"n_acetylation_reason": self.n_acetylation_reason,
"wave2_d_substitutions": self.wave2_d_substitutions,
"flags": self.flags,
"synthesis_difficulty": self.synthesis_difficulty,
}
Expand Down Expand Up @@ -235,6 +245,35 @@ def check_sequence(candidate_id: str, seq: str, mu_h: float = 0.0) -> SynthQC:
"amidation optional — consider for serum stability improvement."
)

# N-terminal acetylation recommendation.
# Nα-Ac blocks aminopeptidase-mediated N-terminal degradation (exopeptidase entry
# blocked). Recommend whenever interior trypsin sites exist — after trypsin exposure,
# the resulting N-terminal fragments become substrates for aminopeptidases.
# Synthesis cost: zero — specify "N-terminal acetylation (Ac-)" in synthesis order.
# Literature: Creighton (1993); Dhankhar et al. (2023).
if len(qc.trypsin_sites) > 0:
qc.n_acetylation_recommended = True
site_positions = [p + 1 for p in qc.trypsin_sites]
qc.n_acetylation_reason = (
f"{len(qc.trypsin_sites)} interior trypsin site(s) detected at position(s) "
f"{site_positions}. N-terminal acetylation (Nα-Ac) blocks aminopeptidase-mediated "
"N-terminal degradation (exopeptidase entry blocked). Specify 'N-terminal acetylation "
"(Ac-)' in synthesis order — zero extra cost. "
"Literature: Creighton (1993); Dhankhar et al. (2023)."
)

# Wave 2 D-amino acid substitution guidance.
# For each interior trypsin cleavage site, D-Lys or D-Arg substitution blocks trypsin
# (requires L-stereochemistry) and extends serum t½ by an estimated 3–10×.
# Only the 3 most N-terminal sites are listed (highest-priority targets).
# Literature: Wade et al. 1990 PNAS.
top_sites = sorted(qc.trypsin_sites)[:3]
qc.wave2_d_substitutions = [
f"Position {p + 1} ({seq[p]} → D-{'Arg' if seq[p] == 'R' else 'Lys'}): "
"eliminates trypsin cleavage site; extend serum t½ by estimated 3–10×"
for p in top_sites
]

# Collect flags
if qc.cysteine_count > 0:
qc.flags.append(f"CYSTEINE×{qc.cysteine_count}: disulfide/oxidation risk — store under N₂")
Expand All @@ -256,7 +295,9 @@ def check_sequence(candidate_id: str, seq: str, mu_h: float = 0.0) -> SynthQC:
if qc.length > 30:
qc.flags.append(f"LONG_PEPTIDE ({qc.length}aa): SPPS yield risk; consider native chemical ligation")

# Synthesis difficulty rating
# Synthesis difficulty rating is based on synthesis-risk flags only.
# Guidance flags (N_ACETYLATION_RECOMMENDED, WAVE2_D_AMINO) are informational and
# carry zero synthesis cost — they are appended after the difficulty is set.
n_flags = len(qc.flags)
if n_flags == 0:
qc.synthesis_difficulty = "LOW"
Expand All @@ -265,6 +306,18 @@ def check_sequence(candidate_id: str, seq: str, mu_h: float = 0.0) -> SynthQC:
else:
qc.synthesis_difficulty = "HIGH"

# Append guidance flags after difficulty is computed (zero synthesis cost).
if qc.n_acetylation_recommended:
qc.flags.append(
"N_ACETYLATION_RECOMMENDED: specify 'Ac-' (N-terminal acetyl) in synthesis order "
"(blocks aminopeptidase entry; zero cost)"
)
if qc.wave2_d_substitutions:
qc.flags.append(
f"WAVE2_D_AMINO: {len(qc.wave2_d_substitutions)} trypsin site(s) identified for "
"D-amino acid substitution in Wave 2 (see wave2_d_substitutions)"
)

return qc


Expand Down
85 changes: 85 additions & 0 deletions tests/test_presynth_qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,3 +457,88 @@ def test_empty_sequence_no_crash(self):
qc = check_sequence("empty", "")
# Empty sequence: no C-term → c_amidation_recommended should be False (default)
assert isinstance(qc.c_amidation_recommended, bool)


# ---------------------------------------------------------------------------
# Wave 2 guidance (N-acetylation + D-amino substitution)
# ---------------------------------------------------------------------------

class TestWave2Guidance:
def test_n_acetylation_recommended_when_interior_trypsin_sites(self):
# K/R at 0-based pos 0,1,5,7,8 (regex excludes pos 10=C-terminal G; all K/R here are considered interior)
qc = check_sequence("w1", "RRWQWRMKKLG")
assert qc.n_acetylation_recommended is True

def test_n_acetylation_not_recommended_when_no_interior_sites(self):
# "FLPLIGAVLSGIL" contains no K or R → no trypsin sites
qc = check_sequence("w2", "FLPLIGAVLSGIL")
assert qc.n_acetylation_recommended is False

def test_d_amino_guidance_lists_all_trypsin_positions(self):
# "RRWQWRMKKLG" has 5 interior sites; entries must reference D- substitutions
qc = check_sequence("w3", "RRWQWRMKKLG")
assert len(qc.wave2_d_substitutions) > 0
assert all("→ D-" in entry for entry in qc.wave2_d_substitutions)

def test_d_amino_guidance_empty_for_no_trypsin_sites(self):
qc = check_sequence("w4", "FLPLIGAVLSGIL")
assert qc.wave2_d_substitutions == []

def test_d_amino_guidance_capped_at_3_positions(self):
# "RRWQWRMKKLG" has 5 interior trypsin sites → only top 3 returned
qc = check_sequence("w5", "RRWQWRMKKLG")
assert len(qc.wave2_d_substitutions) == 3

def test_n_acetylation_flag_appears_in_flags_list(self):
qc = check_sequence("w6", "RRWQWRMKKLG")
acetyl_flags = [f for f in qc.flags if "N_ACETYLATION_RECOMMENDED" in f]
assert len(acetyl_flags) == 1

def test_wave2_d_amino_flag_appears_in_flags_list(self):
qc = check_sequence("w7", "RRWQWRMKKLG")
d_flags = [f for f in qc.flags if "WAVE2_D_AMINO" in f]
assert len(d_flags) == 1

def test_to_dict_contains_new_fields(self):
qc = check_sequence("w8", "RRWQWRMKKLG")
d = qc.to_dict()
assert "n_acetylation_recommended" in d
assert "n_acetylation_reason" in d
assert "wave2_d_substitutions" in d

def test_d_lys_for_interior_lys(self):
# "KKKAAA" has K at positions 0, 1, 2 — all interior → all D-Lys guidance
qc = check_sequence("w9", "KKKAAA")
assert any("D-Lys" in entry for entry in qc.wave2_d_substitutions)

def test_d_arg_for_interior_arg(self):
# "RRRAAA" has R at positions 0, 1, 2 — all interior → all D-Arg guidance
qc = check_sequence("w10", "RRRAAA")
assert any("D-Arg" in entry for entry in qc.wave2_d_substitutions)

def test_n_acetylation_not_recommended_when_only_terminal_kr(self):
# C-terminal K is excluded by trypsin regex (?=.) lookahead + guard
qc = check_sequence("w_cterm_only", "AAAAK")
assert qc.n_acetylation_recommended is False
assert qc.wave2_d_substitutions == []
assert not any("N_ACETYLATION" in f for f in qc.flags)

def test_d_amino_guidance_positions_are_1_indexed(self):
# RRWQWRMKKLG: internal K/R at 0-based positions 0,1,5,7,8
# Top 3 (most N-terminal) = positions 0,1,5 → displayed as 1,2,6
qc = check_sequence("w_pos_1idx", "RRWQWRMKKLG")
assert len(qc.wave2_d_substitutions) == 3
assert "Position 1" in qc.wave2_d_substitutions[0]
assert "Position 2" in qc.wave2_d_substitutions[1]
assert "Position 6" in qc.wave2_d_substitutions[2]

def test_empty_sequence_no_crash_wave2(self):
qc = check_sequence("w_empty", "")
assert qc.n_acetylation_recommended is False
assert qc.wave2_d_substitutions == []

def test_n_acetylation_reason_populated_when_recommended(self):
qc = check_sequence("w_reason", "RRWQWRMKKLG")
assert qc.n_acetylation_recommended is True
assert len(qc.n_acetylation_reason) > 0
assert "acetylat" in qc.n_acetylation_reason.lower()
Loading