Samad14 commited on
Commit
bfe5a36
·
verified ·
1 Parent(s): 8f3e484

Upload folder using huggingface_hub

Browse files
app/.source-revision CHANGED
@@ -1 +1 @@
1
- d64d3564a3e1c93a2a0ca6920c55d5dcf3116056
 
1
+ 44a903c1d800aedc4401f98894addaa88d1212c6
app/services/rdkit_interactions.py CHANGED
@@ -31,15 +31,118 @@ def _feature_factory():
31
  return ChemicalFeatures.BuildFeatureFactory(path)
32
 
33
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  def _pdb_mol(block: str, label: str):
35
  if not block or not any(line.startswith(("ATOM ", "HETATM")) for line in block.splitlines()):
36
  raise ValueError(f"{label} structure contains no PDB atom records")
37
- mol = Chem.MolFromPDBBlock(block, sanitize=True, removeHs=False, proximityBonding=True)
 
 
 
 
 
 
38
  if mol is None or mol.GetNumHeavyAtoms() == 0:
39
  raise ValueError(f"RDKit could not construct a chemically usable {label} molecule")
40
  return mol
41
 
42
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  def _ligand_with_template(ligand_pdb: str, smiles: str):
44
  template = Chem.MolFromSmiles(smiles)
45
  if template is None:
@@ -172,7 +275,7 @@ def _ring_interactions(protein, ligand, protein_features, ligand_features):
172
  def calculate_interactions(protein_pdb: str, ligand_pdb: str, ligand_smiles: str) -> dict[str, Any]:
173
  """Return RDKit feature-based interaction evidence for one docked pose."""
174
  factory = _feature_factory()
175
- protein = _with_hydrogens(_pdb_mol(protein_pdb, "protein"))
176
  ligand = _with_hydrogens(_ligand_with_template(ligand_pdb, ligand_smiles))
177
  pf = _atom_features(protein, factory)
178
  lf = _atom_features(ligand, factory)
@@ -252,7 +355,7 @@ def calculate_interactions(protein_pdb: str, ligand_pdb: str, ligand_smiles: str
252
  "limitations": [
253
  "Interaction classifications depend on the supplied protonation/tautomer state and prepared coordinates.",
254
  "RDKit feature perception is a computational model, not experimental evidence of binding.",
255
- "Only the provided receptor atoms and docked pose are evaluated; omitted waters, ions, cofactors, and alternate conformations are not inferred.",
256
  "Distances and angles are geometric screens, not interaction energies or binding-affinity measurements.",
257
  ],
258
  }
 
31
  return ChemicalFeatures.BuildFeatureFactory(path)
32
 
33
 
34
+ def _normalize_pose_pdb(block: str) -> str:
35
+ """Rebuild fixed-column PDB records from PDBQT while preserving atom order/coordinates."""
36
+ rows = []
37
+ for line in block.splitlines():
38
+ if line[:6].strip() not in ("ATOM", "HETATM"):
39
+ continue
40
+ fields = line.split()
41
+ if len(fields) < 9:
42
+ continue
43
+ try:
44
+ serial = int(fields[1])
45
+ atom_name = fields[2]
46
+ x, y, z = (float(fields[i]) for i in (5, 6, 7))
47
+ element = fields[-1].strip().upper()
48
+ except (ValueError, IndexError):
49
+ continue
50
+ element = "".join(ch for ch in element if ch.isalpha())[:2].title()
51
+ if not element:
52
+ element = "".join(ch for ch in atom_name if ch.isalpha())[:1].title()
53
+ atom_field = f" {atom_name:<3s}"[:4] if len(atom_name) < 4 else atom_name[:4]
54
+ rows.append(
55
+ f"ATOM {serial:5d} {atom_field:4s} UNL A 1 "
56
+ f"{x:8.3f}{y:8.3f}{z:8.3f}{1.0:6.2f}{0.0:6.2f} {element:>2s} "
57
+ )
58
+ if not rows:
59
+ raise ValueError("Could not recover ligand atom coordinates from PDB/PDBQT records")
60
+ return "\\n".join(rows) + "\\nEND\\n"
61
+
62
+
63
  def _pdb_mol(block: str, label: str):
64
  if not block or not any(line.startswith(("ATOM ", "HETATM")) for line in block.splitlines()):
65
  raise ValueError(f"{label} structure contains no PDB atom records")
66
+ try:
67
+ mol = Chem.MolFromPDBBlock(block, sanitize=True, removeHs=False, proximityBonding=True)
68
+ except Exception:
69
+ mol = None
70
+ if mol is None and label == "ligand":
71
+ normalized = _normalize_pose_pdb(block)
72
+ mol = Chem.MolFromPDBBlock(normalized, sanitize=True, removeHs=False, proximityBonding=True)
73
  if mol is None or mol.GetNumHeavyAtoms() == 0:
74
  raise ValueError(f"RDKit could not construct a chemically usable {label} molecule")
75
  return mol
76
 
77
 
78
+ def _protein_mol(block: str):
79
+ """Parse protein residues independently to prevent false inter-residue proximity bonds.
80
+
81
+ Reconnects sequential peptide C-N bonds after residue-level chemical perception.
82
+ Any residue that cannot be perceived is explicitly recorded as excluded.
83
+ """
84
+ from collections import OrderedDict
85
+ residues = OrderedDict()
86
+ for line in block.splitlines():
87
+ if line.startswith("ATOM "):
88
+ if len(line) > 16 and line[16] not in (" ", "A"):
89
+ continue
90
+ key = (line[21:22], line[22:26], line[26:27], line[17:20])
91
+ residues.setdefault(key, []).append(line)
92
+ parsed = []
93
+ skipped = []
94
+ for key, lines in residues.items():
95
+ residue_block = "\\n".join(lines) + "\\nEND\\n"
96
+ try:
97
+ residue_mol = Chem.MolFromPDBBlock(
98
+ residue_block, sanitize=True, removeHs=False, proximityBonding=True
99
+ )
100
+ except Exception:
101
+ residue_mol = None
102
+ if residue_mol is None or residue_mol.GetNumHeavyAtoms() == 0:
103
+ skipped.append({"chain": key[0].strip(), "residue_number": key[1].strip(),
104
+ "residue_name": key[3].strip(), "reason": "RDKit residue sanitization failed"})
105
+ continue
106
+ parsed.append((key, residue_mol))
107
+ if not parsed:
108
+ raise ValueError("RDKit could not chemically perceive any protein residues")
109
+ combined = parsed[0][1]
110
+ residue_atom_indices = []
111
+ for key, mol in parsed:
112
+ if mol is not parsed[0][1]:
113
+ offset = combined.GetNumAtoms()
114
+ combined = Chem.CombineMols(combined, mol)
115
+ else:
116
+ offset = 0
117
+ names = {}
118
+ for atom in mol.GetAtoms():
119
+ info = atom.GetPDBResidueInfo()
120
+ if info:
121
+ names[info.GetName().strip()] = atom.GetIdx() + offset
122
+ residue_atom_indices.append((key, names))
123
+ editable = Chem.RWMol(combined)
124
+ peptide_bonds_added = 0
125
+ for (left_key, left_atoms), (right_key, right_atoms) in zip(residue_atom_indices, residue_atom_indices[1:]):
126
+ same_chain = left_key[0] == right_key[0]
127
+ try:
128
+ sequential = int(right_key[1]) == int(left_key[1]) + 1
129
+ except ValueError:
130
+ sequential = False
131
+ if same_chain and sequential and "C" in left_atoms and "N" in right_atoms:
132
+ c_idx, n_idx = left_atoms["C"], right_atoms["N"]
133
+ p1 = combined.GetConformer().GetAtomPosition(c_idx)
134
+ p2 = combined.GetConformer().GetAtomPosition(n_idx)
135
+ distance = p1.Distance(p2)
136
+ if 1.1 <= distance <= 1.8:
137
+ editable.AddBond(c_idx, n_idx, Chem.BondType.SINGLE)
138
+ peptide_bonds_added += 1
139
+ protein = editable.GetMol()
140
+ Chem.SanitizeMol(protein)
141
+ protein.SetProp("_omvetra_skipped_residues", str(skipped))
142
+ protein.SetProp("_omvetra_peptide_bonds_added", str(peptide_bonds_added))
143
+ return protein
144
+
145
+
146
  def _ligand_with_template(ligand_pdb: str, smiles: str):
147
  template = Chem.MolFromSmiles(smiles)
148
  if template is None:
 
275
  def calculate_interactions(protein_pdb: str, ligand_pdb: str, ligand_smiles: str) -> dict[str, Any]:
276
  """Return RDKit feature-based interaction evidence for one docked pose."""
277
  factory = _feature_factory()
278
+ protein_raw = _protein_mol(protein_pdb)\n protein_parse = {\n "parser": "RDKit residue-wise perception with sequential peptide-bond reconstruction",\n "residues_skipped": eval(protein_raw.GetProp("_omvetra_skipped_residues")),\n "peptide_bonds_reconstructed": int(protein_raw.GetProp("_omvetra_peptide_bonds_added")),\n }\n protein = _with_hydrogens(protein_raw)
279
  ligand = _with_hydrogens(_ligand_with_template(ligand_pdb, ligand_smiles))
280
  pf = _atom_features(protein, factory)
281
  lf = _atom_features(ligand, factory)
 
355
  "limitations": [
356
  "Interaction classifications depend on the supplied protonation/tautomer state and prepared coordinates.",
357
  "RDKit feature perception is a computational model, not experimental evidence of binding.",
358
+ "Only the provided receptor atoms and docked pose are evaluated; omitted waters, ions, cofactors, and alternate conformations are not inferred.",\n "Residues listed in protein_parse.residues_skipped were excluded from chemical feature perception; a DEGRADED status must not be interpreted as complete receptor coverage.",
359
  "Distances and angles are geometric screens, not interaction energies or binding-affinity measurements.",
360
  ],
361
  }