AlienChen/Storage / pCoMole /peptide /constraints.py
AlienChen's picture
download
raw
9.3 kB
from dataclasses import dataclass
from typing import List, Dict, Any, Tuple, Optional
from rdkit import Chem
class DisulfideBuildError(Exception):
pass
def _clean_peptide_sequence(seq: str) -> str:
seq = seq.strip().upper().replace(" ", "")
if not seq:
raise DisulfideBuildError("Empty sequence.")
return seq
def _validate_disulfide_pair(seq: str, disulfide_pair: Tuple[int, int]) -> Tuple[int, int]:
if not isinstance(disulfide_pair, tuple) or len(disulfide_pair) != 2:
raise DisulfideBuildError("disulfide_pair must be a tuple of two 0-based sequence indices.")
i, j = disulfide_pair
n = len(seq)
if not (0 <= i < n and 0 <= j < n):
raise DisulfideBuildError(
f"disulfide_pair {disulfide_pair} is out of range for sequence length {n}."
)
if i == j:
raise DisulfideBuildError("The two cysteine indices must be different.")
if seq[i] != "C" or seq[j] != "C":
raise DisulfideBuildError(
f"disulfide_pair {disulfide_pair} does not point to two cysteines."
)
return tuple(sorted((i, j)))
def _get_residue_number_to_sg_atom_idx(mol: Chem.Mol) -> dict:
residue_to_sg = {}
for atom in mol.GetAtoms():
info = atom.GetPDBResidueInfo()
if info is None:
continue
atom_name = info.GetName().strip()
residue_name = info.GetResidueName().strip()
residue_number = info.GetResidueNumber()
if residue_name == "CYS" and atom_name == "SG":
if residue_number in residue_to_sg:
raise DisulfideBuildError(
f"Found multiple SG atoms for residue number {residue_number}."
)
residue_to_sg[residue_number] = atom.GetIdx()
return residue_to_sg
def _find_ss_bond_idx(mol: Chem.Mol) -> Optional[int]:
for bond in mol.GetBonds():
a1 = bond.GetBeginAtom()
a2 = bond.GetEndAtom()
if a1.GetSymbol() == "S" and a2.GetSymbol() == "S":
return bond.GetIdx()
return None
def _bond_is_in_ring(mol: Chem.Mol, bond_idx: int) -> bool:
for ring_bond_indices in mol.GetRingInfo().BondRings():
if bond_idx in ring_bond_indices:
return True
return False
def build_peptide_with_disulfide(
seq: str,
disulfide_pair: Tuple[int, int],
require_ring_closure: bool = True,
) -> Chem.Mol:
"""
Build an oxidized peptide with an explicit disulfide bond between two cysteines.
Parameters
----------
seq : str
One-letter amino-acid sequence.
disulfide_pair : tuple[int, int]
0-based sequence indices of the two cysteines to connect.
require_ring_closure : bool
If True, raise an error unless the constructed S-S bond is part of a ring.
Returns
-------
mol : rdkit.Chem.Mol
Sanitized RDKit molecule with explicit S-S bond.
Notes
-----
- This uses RDKit's MolFromFASTA, so it is intended for standard amino acids.
- The function does not generate 3D coordinates; it builds the molecular graph.
"""
seq = _clean_peptide_sequence(seq)
i, j = _validate_disulfide_pair(seq, disulfide_pair)
mol = Chem.MolFromFASTA(seq)
if mol is None:
raise DisulfideBuildError("RDKit failed to build peptide from sequence.")
residue_to_sg = _get_residue_number_to_sg_atom_idx(mol)
# RDKit residue numbering from MolFromFASTA starts at 1
res_i = i + 1
res_j = j + 1
if res_i not in residue_to_sg:
raise DisulfideBuildError(f"Could not find SG atom for cysteine at sequence index {i}.")
if res_j not in residue_to_sg:
raise DisulfideBuildError(f"Could not find SG atom for cysteine at sequence index {j}.")
sg_i = residue_to_sg[res_i]
sg_j = residue_to_sg[res_j]
existing_bond = mol.GetBondBetweenAtoms(sg_i, sg_j)
if existing_bond is not None:
ss_bond_idx = existing_bond.GetIdx()
if require_ring_closure and not _bond_is_in_ring(mol, ss_bond_idx):
raise DisulfideBuildError("An S-S bond already exists, but it is not part of a ring.")
return mol
rw = Chem.RWMol(mol)
rw.AddBond(sg_i, sg_j, Chem.BondType.SINGLE)
out = rw.GetMol()
try:
Chem.SanitizeMol(out)
except Exception as e:
raise DisulfideBuildError(f"Sanitization failed after adding disulfide bond: {e}")
ss_bond_idx = _find_ss_bond_idx(out)
if ss_bond_idx is None:
raise DisulfideBuildError("Failed to find S-S bond after construction.")
if require_ring_closure and not _bond_is_in_ring(out, ss_bond_idx):
raise DisulfideBuildError("Constructed S-S bond exists but does not close a loop.")
return out
@dataclass
class ConstraintResult:
feasible: bool
details: Dict[str, Any]
class DisulfideLoopConstraint:
def __init__(self, min_separation: int = 1):
"""
builder_fn(seq, disulfide_pair) -> rdkit.Chem.Mol
The builder_fn must:
1. build the peptide from the sequence
2. explicitly add a disulfide bond between the two cysteines
3. return the oxidized molecule
"""
self.builder_fn = build_peptide_with_disulfide
self.min_separation = min_separation
def _clean_seq(self, seq: str) -> str:
return seq.strip().upper().replace(" ", "")
def _get_cys_positions(self, seq: str) -> List[int]:
return [i for i, aa in enumerate(seq) if aa == "C"]
def _find_ss_bond_idx(self, mol):
for bond in mol.GetBonds():
a1 = bond.GetBeginAtom()
a2 = bond.GetEndAtom()
if a1.GetSymbol() == "S" and a2.GetSymbol() == "S":
return bond.GetIdx()
return None
def _ss_bond_closes_loop(self, mol, ss_bond_idx: int) -> bool:
ring_info = mol.GetRingInfo()
bond_rings = ring_info.BondRings()
for ring in bond_rings:
if ss_bond_idx in ring:
return True
return False
def evaluate_one(self, seq: str) -> ConstraintResult:
seq = self._clean_seq(seq)
cys_positions = self._get_cys_positions(seq)
if seq[-1] != 'C':
return ConstraintResult(
feasible=False,
details={
"reason": "terminal residue is not cysteine",
"cys_positions_0based": cys_positions,
},
)
if len(cys_positions) != 2:
return ConstraintResult(
feasible=False,
details={
"reason": "sequence must contain exactly two cysteines",
"num_cys": len(cys_positions),
"cys_positions_0based": cys_positions,
},
)
if cys_positions[1] - cys_positions[0] < self.min_separation:
return ConstraintResult(
feasible=False,
details={
"reason": "two cysteines are too close in sequence",
"num_cys": 2,
"cys_positions_0based": cys_positions,
},
)
try:
mol = self.builder_fn(seq, disulfide_pair=(cys_positions[0], cys_positions[1]), require_ring_closure=True)
except Exception as e:
return ConstraintResult(
feasible=False,
details={
"reason": "oxidized molecule build failed",
"error": str(e),
"cys_positions_0based": cys_positions,
},
)
if mol is None:
return ConstraintResult(
feasible=False,
details={
"reason": "builder returned None",
"cys_positions_0based": cys_positions,
},
)
ss_bond_idx = self._find_ss_bond_idx(mol)
if ss_bond_idx is None:
return ConstraintResult(
feasible=False,
details={
"reason": "no S-S bond found in built molecule",
"cys_positions_0based": cys_positions,
},
)
closes_loop = self._ss_bond_closes_loop(mol, ss_bond_idx)
if not closes_loop:
return ConstraintResult(
feasible=False,
details={
"reason": "S-S bond exists but is not part of a ring",
"cys_positions_0based": cys_positions,
"ss_bond_idx": ss_bond_idx,
},
)
return ConstraintResult(
feasible=True,
details={
"reason": "valid disulfide-closed loop",
"cys_positions_0based": cys_positions,
"ss_bond_idx": ss_bond_idx,
},
)
def __call__(self, input_seqs: List[str]) -> List[int]:
return [1 if self.evaluate_one(seq).feasible else 0 for seq in input_seqs]
class Length:
def __init__(self, orig_seq):
self.L0 = len(orig_seq)
print("Initial Length: ", self.L0)
def __call__(self, seqs):
scores = [1 if (len(seq) < self.L0 - 3) else 0 for seq in seqs]
return scores

Xet Storage Details

Size:
9.3 kB
·
Xet hash:
ba831cab881f1be651fe726213da45da10aef193a34623ec32c9aab31aea9a4d

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.