helmkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- helmkit/__init__.py +5 -0
- helmkit/data/monomers.sdf +19811 -0
- helmkit/molecule.py +456 -0
- helmkit/py.typed +0 -0
- helmkit-0.1.0.dist-info/METADATA +112 -0
- helmkit-0.1.0.dist-info/RECORD +8 -0
- helmkit-0.1.0.dist-info/WHEEL +4 -0
- helmkit-0.1.0.dist-info/licenses/LICENSE +21 -0
helmkit/molecule.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
import re
|
|
3
|
+
import warnings
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from typing import Dict
|
|
6
|
+
from typing import List
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
|
|
10
|
+
from rdkit import Chem
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SequenceConstants:
|
|
14
|
+
def_path = "helmkit.data"
|
|
15
|
+
def_lib_filename = "monomers.sdf"
|
|
16
|
+
monomer_join = "-"
|
|
17
|
+
chain_separator = "."
|
|
18
|
+
csv_separator = ","
|
|
19
|
+
helm_polymer = "|"
|
|
20
|
+
max_rgroups = 4
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def get_molecule_property(molecule: Chem.Mol, property_name: str, default=None):
|
|
24
|
+
return (
|
|
25
|
+
molecule.GetProp(property_name) if molecule.HasProp(property_name) else default
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def parse_comma_separated_property(
|
|
30
|
+
molecule: Chem.Mol, property_name: str, convert_func=None
|
|
31
|
+
) -> List:
|
|
32
|
+
property_value = get_molecule_property(molecule, property_name)
|
|
33
|
+
if not property_value:
|
|
34
|
+
return []
|
|
35
|
+
|
|
36
|
+
values = property_value.split(SequenceConstants.csv_separator)
|
|
37
|
+
if convert_func:
|
|
38
|
+
values = [convert_func(v) if v != "None" else None for v in values]
|
|
39
|
+
else:
|
|
40
|
+
values = [None if v == "None" else v for v in values]
|
|
41
|
+
|
|
42
|
+
return values
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def infer_attachment_points(molecule: Chem.Mol, rgroup_indices: List[int]) -> List[int]:
|
|
46
|
+
"""Infer attachment points by finding atoms bonded to R-group atoms."""
|
|
47
|
+
attachment_points = []
|
|
48
|
+
|
|
49
|
+
for r_idx in rgroup_indices:
|
|
50
|
+
if r_idx is None:
|
|
51
|
+
attachment_points.append(None)
|
|
52
|
+
continue
|
|
53
|
+
|
|
54
|
+
atom = molecule.GetAtomWithIdx(r_idx)
|
|
55
|
+
|
|
56
|
+
for bond in atom.GetBonds():
|
|
57
|
+
other_idx = bond.GetOtherAtomIdx(r_idx)
|
|
58
|
+
attachment_points.append(other_idx)
|
|
59
|
+
break
|
|
60
|
+
else:
|
|
61
|
+
attachment_points.append(None)
|
|
62
|
+
warnings.warn(
|
|
63
|
+
f"R-group atom {r_idx} has no bonds to determine attachment point"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
return attachment_points
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def load_monomer_library(library_path: str) -> Dict:
|
|
70
|
+
"""Load and prepare monomer data from SDF file."""
|
|
71
|
+
monomers_dict = {}
|
|
72
|
+
supplier = Chem.SDMolSupplier(library_path)
|
|
73
|
+
|
|
74
|
+
for mol in supplier:
|
|
75
|
+
if mol is None:
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
symbol = get_molecule_property(mol, "symbol")
|
|
79
|
+
if not symbol:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
rgroups = parse_comma_separated_property(mol, "m_Rgroups")
|
|
83
|
+
rgroup_idx = parse_comma_separated_property(mol, "m_RgroupIdx", int)
|
|
84
|
+
attachment_point_idx = infer_attachment_points(mol, rgroup_idx)
|
|
85
|
+
|
|
86
|
+
monomers_dict[symbol] = {
|
|
87
|
+
"m_romol": mol,
|
|
88
|
+
"m_Rgroups": rgroups,
|
|
89
|
+
"m_RgroupIdx": rgroup_idx,
|
|
90
|
+
"m_attachmentPointIdx": attachment_point_idx,
|
|
91
|
+
"m_type": get_molecule_property(mol, "m_type", ""),
|
|
92
|
+
"m_subtype": get_molecule_property(mol, "m_subtype", ""),
|
|
93
|
+
"m_abbr": get_molecule_property(mol, "m_abbr", ""),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return monomers_dict
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class Molecule:
|
|
100
|
+
"""Single class for HELM to RDKit Mol conversion."""
|
|
101
|
+
|
|
102
|
+
def __init__(self, helm: str, monomer_df: Optional[Dict] = None):
|
|
103
|
+
"""Initialize a Molecule object from a HELM string."""
|
|
104
|
+
self.mol = None
|
|
105
|
+
self.offset = []
|
|
106
|
+
self.bondlist = []
|
|
107
|
+
self.monomers = []
|
|
108
|
+
self.chains = {}
|
|
109
|
+
self.chain_offset = {}
|
|
110
|
+
|
|
111
|
+
if monomer_df is None:
|
|
112
|
+
default_monomer_df_filepath = files(SequenceConstants.def_path).joinpath(
|
|
113
|
+
SequenceConstants.def_lib_filename
|
|
114
|
+
)
|
|
115
|
+
self.monomer_df = load_monomer_library(str(default_monomer_df_filepath))
|
|
116
|
+
else:
|
|
117
|
+
self.monomer_df = monomer_df
|
|
118
|
+
|
|
119
|
+
self._parse_helm_string(helm)
|
|
120
|
+
self._build_molecule()
|
|
121
|
+
|
|
122
|
+
if not isinstance(self.mol, Chem.rdchem.Mol):
|
|
123
|
+
raise RuntimeError("Failed to initialize RDKit Mol object")
|
|
124
|
+
|
|
125
|
+
def _parse_helm_string(self, helm: str) -> None:
|
|
126
|
+
"""Parse a HELM string into molecular components."""
|
|
127
|
+
helm_parts = self._split_helm_sections(helm)
|
|
128
|
+
|
|
129
|
+
if len(helm_parts) < 5:
|
|
130
|
+
warnings.warn(f"Problem with HELM string - not enough sections: {helm}")
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
polymer_sections, connection_sections = helm_parts[0], helm_parts[1]
|
|
134
|
+
|
|
135
|
+
if not polymer_sections:
|
|
136
|
+
warnings.warn(f"No simple polymers in HELM string {helm}")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
self._process_polymers(polymer_sections)
|
|
140
|
+
self._process_connections(connection_sections)
|
|
141
|
+
self._create_backbone_bonds()
|
|
142
|
+
self._fix_rgroups()
|
|
143
|
+
|
|
144
|
+
def _split_helm_sections(self, helm: str) -> List:
|
|
145
|
+
"""Split a HELM string into its components."""
|
|
146
|
+
parts = helm.split("$", 4)
|
|
147
|
+
parts.extend([""] * (5 - len(parts)))
|
|
148
|
+
|
|
149
|
+
parts[0] = (
|
|
150
|
+
parts[0].split(SequenceConstants.helm_polymer)
|
|
151
|
+
if SequenceConstants.helm_polymer in parts[0]
|
|
152
|
+
else [parts[0]]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
if parts[1]:
|
|
156
|
+
parts[1] = (
|
|
157
|
+
parts[1].split(SequenceConstants.helm_polymer)
|
|
158
|
+
if SequenceConstants.helm_polymer in parts[1]
|
|
159
|
+
else [parts[1]]
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
parts[1] = []
|
|
163
|
+
|
|
164
|
+
return parts
|
|
165
|
+
|
|
166
|
+
def _split_sequence_with_brackets(self, sequence: str) -> List[str]:
|
|
167
|
+
"""Split a sequence into individual monomers, respecting brackets."""
|
|
168
|
+
result = []
|
|
169
|
+
current = ""
|
|
170
|
+
bracket_depth = 0
|
|
171
|
+
|
|
172
|
+
for char in sequence:
|
|
173
|
+
if char == "[":
|
|
174
|
+
bracket_depth += 1
|
|
175
|
+
current += char
|
|
176
|
+
elif char == "]":
|
|
177
|
+
bracket_depth -= 1
|
|
178
|
+
current += char
|
|
179
|
+
elif char == "." and bracket_depth == 0:
|
|
180
|
+
result.append(current)
|
|
181
|
+
current = ""
|
|
182
|
+
else:
|
|
183
|
+
current += char
|
|
184
|
+
|
|
185
|
+
if current:
|
|
186
|
+
result.append(current)
|
|
187
|
+
|
|
188
|
+
return result
|
|
189
|
+
|
|
190
|
+
def _extract_chain_id(self, chain_str: str) -> Tuple[int, bool]:
|
|
191
|
+
"""Extract chain ID and validate chain type."""
|
|
192
|
+
if chain_str.startswith("CHEM"):
|
|
193
|
+
return None, False
|
|
194
|
+
|
|
195
|
+
if not chain_str.startswith("PEPTIDE"):
|
|
196
|
+
warnings.warn(f"Non-peptide chain: {chain_str}")
|
|
197
|
+
return None, False
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
return int(chain_str.replace("PEPTIDE", "")), True
|
|
201
|
+
except ValueError:
|
|
202
|
+
warnings.warn(f"Invalid chain ID: {chain_str}")
|
|
203
|
+
return None, False
|
|
204
|
+
|
|
205
|
+
def _process_monomer(
|
|
206
|
+
self, monomer_name: str, chain_id: int, residue_idx: int
|
|
207
|
+
) -> Optional[Dict]:
|
|
208
|
+
"""Process a single monomer."""
|
|
209
|
+
monomer_name = re.sub(r"\[(.*)\]", r"\1", monomer_name)
|
|
210
|
+
|
|
211
|
+
if monomer_name not in self.monomer_df:
|
|
212
|
+
raise ValueError(f"Monomer {monomer_name} not found in monomer library")
|
|
213
|
+
|
|
214
|
+
monomer_info = self.monomer_df[monomer_name]
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
"m_name": monomer_name,
|
|
218
|
+
"m_chainID": chain_id,
|
|
219
|
+
"m_resID": residue_idx,
|
|
220
|
+
"m_romol": monomer_info["m_romol"],
|
|
221
|
+
"m_Rgroups": copy.deepcopy(monomer_info["m_Rgroups"]),
|
|
222
|
+
"m_RgroupIdx": monomer_info["m_RgroupIdx"],
|
|
223
|
+
"m_attachmentPointIdx": monomer_info["m_attachmentPointIdx"],
|
|
224
|
+
"m_type": monomer_info["m_type"],
|
|
225
|
+
"m_subtype": monomer_info["m_subtype"],
|
|
226
|
+
"m_abbr": monomer_info["m_abbr"],
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
def _process_polymers(self, polymers: List[str]) -> None:
|
|
230
|
+
"""Process polymer chains from HELM."""
|
|
231
|
+
monomer_idx = 0
|
|
232
|
+
chain_types = []
|
|
233
|
+
chain_monomer_ids = []
|
|
234
|
+
pattern = re.compile(r"{(.*?)}")
|
|
235
|
+
|
|
236
|
+
for chain in polymers:
|
|
237
|
+
chain = chain.strip()
|
|
238
|
+
|
|
239
|
+
match = pattern.search(chain)
|
|
240
|
+
if not match:
|
|
241
|
+
warnings.warn(f"No sequence in polymer: {chain}")
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
id_chain = chain[: match.start()]
|
|
245
|
+
|
|
246
|
+
chain_id, valid = self._extract_chain_id(id_chain)
|
|
247
|
+
if not valid:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
sequence = match.group(1)
|
|
251
|
+
if not sequence:
|
|
252
|
+
warnings.warn(f"Empty polymer: {chain}")
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
residues = self._split_sequence_with_brackets(sequence)
|
|
256
|
+
|
|
257
|
+
self.chain_offset[chain_id] = monomer_idx
|
|
258
|
+
|
|
259
|
+
chain_monomer_ids_local = []
|
|
260
|
+
monomer_types = set()
|
|
261
|
+
|
|
262
|
+
for residue_idx, monomer_name in enumerate(residues):
|
|
263
|
+
monomer = self._process_monomer(monomer_name, chain_id, residue_idx)
|
|
264
|
+
if not monomer:
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
self.monomers.append(monomer)
|
|
268
|
+
chain_monomer_ids_local.append(monomer_idx)
|
|
269
|
+
monomer_types.add(monomer["m_type"])
|
|
270
|
+
monomer_idx += 1
|
|
271
|
+
|
|
272
|
+
if len(monomer_types) == 1:
|
|
273
|
+
chain_type = "peptide" if "aa" in monomer_types else "chem"
|
|
274
|
+
else:
|
|
275
|
+
chain_type = "mixed"
|
|
276
|
+
|
|
277
|
+
chain_types.append(chain_type)
|
|
278
|
+
chain_monomer_ids.append(chain_monomer_ids_local)
|
|
279
|
+
|
|
280
|
+
self.chains = {
|
|
281
|
+
"s_nChains": len(polymers),
|
|
282
|
+
"s_cType": chain_types,
|
|
283
|
+
"s_monomerIDs": chain_monomer_ids,
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
def _parse_connection(self, connection_str: str) -> Optional[Tuple]:
|
|
287
|
+
"""Parse a single connection string."""
|
|
288
|
+
parts = connection_str.split(",")
|
|
289
|
+
if len(parts) != 3:
|
|
290
|
+
warnings.warn(f"Invalid connection format: {connection_str}")
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
chain_id1, chain_id2, bond_spec = parts
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
chain_id1 = int(chain_id1.replace("PEPTIDE", ""))
|
|
297
|
+
chain_id2 = int(chain_id2.replace("PEPTIDE", ""))
|
|
298
|
+
|
|
299
|
+
bond_parts = re.split(r"[-:]", bond_spec)
|
|
300
|
+
if len(bond_parts) != 4:
|
|
301
|
+
warnings.warn(f"Invalid bond format: {bond_spec}")
|
|
302
|
+
return None
|
|
303
|
+
|
|
304
|
+
residue1, rgroup1, residue2, rgroup2 = bond_parts
|
|
305
|
+
|
|
306
|
+
residue1 = int(residue1) - 1
|
|
307
|
+
residue2 = int(residue2) - 1
|
|
308
|
+
rgroup1 = int(rgroup1.replace("R", ""))
|
|
309
|
+
rgroup2 = int(rgroup2.replace("R", ""))
|
|
310
|
+
|
|
311
|
+
return chain_id1, residue1, rgroup1, chain_id2, residue2, rgroup2
|
|
312
|
+
except (ValueError, IndexError) as e:
|
|
313
|
+
warnings.warn(f"Error parsing connection {connection_str}: {e}")
|
|
314
|
+
return None
|
|
315
|
+
|
|
316
|
+
def _process_connections(self, connections: List[str]) -> None:
|
|
317
|
+
"""Process connections between chains."""
|
|
318
|
+
if not connections:
|
|
319
|
+
return
|
|
320
|
+
|
|
321
|
+
for connection_str in connections:
|
|
322
|
+
parsed = self._parse_connection(connection_str)
|
|
323
|
+
if not parsed:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
chain_id1, residue1, rgroup1, chain_id2, residue2, rgroup2 = parsed
|
|
327
|
+
|
|
328
|
+
monomer_idx1 = self.chain_offset[chain_id1] + residue1
|
|
329
|
+
monomer_idx2 = self.chain_offset[chain_id2] + residue2
|
|
330
|
+
|
|
331
|
+
monomer1 = self.monomers[monomer_idx1]
|
|
332
|
+
monomer2 = self.monomers[monomer_idx2]
|
|
333
|
+
|
|
334
|
+
attachment_idx1 = monomer1["m_attachmentPointIdx"][rgroup1 - 1]
|
|
335
|
+
attachment_idx2 = monomer2["m_attachmentPointIdx"][rgroup2 - 1]
|
|
336
|
+
|
|
337
|
+
self.bondlist.append(
|
|
338
|
+
[monomer_idx1, attachment_idx1, monomer_idx2, attachment_idx2]
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
def _create_backbone_bonds(self) -> None:
|
|
342
|
+
"""Create peptide backbone bonds within each chain."""
|
|
343
|
+
if not self.chains:
|
|
344
|
+
return
|
|
345
|
+
|
|
346
|
+
for chain_ids in self.chains["s_monomerIDs"]:
|
|
347
|
+
for i in range(len(chain_ids) - 1):
|
|
348
|
+
monomer_idx1 = chain_ids[i]
|
|
349
|
+
monomer_idx2 = chain_ids[i + 1]
|
|
350
|
+
|
|
351
|
+
monomer1 = self.monomers[monomer_idx1]
|
|
352
|
+
monomer2 = self.monomers[monomer_idx2]
|
|
353
|
+
|
|
354
|
+
attachment_points1 = monomer1["m_attachmentPointIdx"]
|
|
355
|
+
attachment_points2 = monomer2["m_attachmentPointIdx"]
|
|
356
|
+
|
|
357
|
+
self.bondlist.append(
|
|
358
|
+
[
|
|
359
|
+
monomer_idx1,
|
|
360
|
+
attachment_points1[1],
|
|
361
|
+
monomer_idx2,
|
|
362
|
+
attachment_points2[0],
|
|
363
|
+
]
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
def _fix_rgroups(self) -> None:
|
|
367
|
+
"""Mark R-groups that are used in bonds to be deleted later."""
|
|
368
|
+
for bond in self.bondlist:
|
|
369
|
+
monomer_idx1, attachment_idx1, monomer_idx2, attachment_idx2 = bond
|
|
370
|
+
|
|
371
|
+
self._mark_used_rgroup(monomer_idx1, attachment_idx1)
|
|
372
|
+
self._mark_used_rgroup(monomer_idx2, attachment_idx2)
|
|
373
|
+
|
|
374
|
+
def _mark_used_rgroup(self, monomer_idx: int, attachment_idx: int) -> None:
|
|
375
|
+
"""Mark an R-group as used based on its attachment point index."""
|
|
376
|
+
monomer = self.monomers[monomer_idx]
|
|
377
|
+
for i, idx in enumerate(monomer["m_attachmentPointIdx"]):
|
|
378
|
+
if idx == attachment_idx:
|
|
379
|
+
monomer["m_Rgroups"][i] = None
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
def _build_molecule(self) -> None:
|
|
383
|
+
"""Build the RDKit molecule from parsed monomer and bond data."""
|
|
384
|
+
self._generate_atom_offsets()
|
|
385
|
+
self._combine_monomers()
|
|
386
|
+
self._add_bonds()
|
|
387
|
+
self._process_rgroups()
|
|
388
|
+
self._sanitize()
|
|
389
|
+
|
|
390
|
+
def _generate_atom_offsets(self) -> None:
|
|
391
|
+
"""Generate atom offsets for each monomer in the molecule."""
|
|
392
|
+
self.offset = [0]
|
|
393
|
+
current_offset = 0
|
|
394
|
+
|
|
395
|
+
for monomer in self.monomers:
|
|
396
|
+
atom_count = monomer["m_romol"].GetNumAtoms()
|
|
397
|
+
current_offset += atom_count
|
|
398
|
+
self.offset.append(current_offset)
|
|
399
|
+
|
|
400
|
+
def _combine_monomers(self) -> None:
|
|
401
|
+
"""Combine all monomers into a single molecule."""
|
|
402
|
+
if not self.monomers:
|
|
403
|
+
self.mol = Chem.RWMol()
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
combined_mol = self.monomers[0]["m_romol"]
|
|
407
|
+
|
|
408
|
+
for i in range(1, len(self.monomers)):
|
|
409
|
+
combined_mol = Chem.CombineMols(combined_mol, self.monomers[i]["m_romol"])
|
|
410
|
+
|
|
411
|
+
self.mol = Chem.RWMol(combined_mol)
|
|
412
|
+
|
|
413
|
+
def _add_bonds(self) -> None:
|
|
414
|
+
"""Add bonds between monomers based on bond list."""
|
|
415
|
+
for monomer1_idx, atom1_idx, monomer2_idx, atom2_idx in self.bondlist:
|
|
416
|
+
absolute_atom1_idx = self.offset[monomer1_idx] + atom1_idx
|
|
417
|
+
absolute_atom2_idx = self.offset[monomer2_idx] + atom2_idx
|
|
418
|
+
|
|
419
|
+
self.mol.AddBond(
|
|
420
|
+
absolute_atom1_idx, absolute_atom2_idx, Chem.BondType.SINGLE
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def _process_rgroups(self) -> None:
|
|
424
|
+
"""Process R-groups in the molecule, replacing or removing as needed."""
|
|
425
|
+
rwmol = Chem.RWMol(self.mol)
|
|
426
|
+
|
|
427
|
+
for idx, monomer in enumerate(self.monomers):
|
|
428
|
+
rgroups = monomer["m_Rgroups"]
|
|
429
|
+
rgroup_idx = monomer["m_RgroupIdx"]
|
|
430
|
+
atom_offset = self.offset[idx]
|
|
431
|
+
|
|
432
|
+
for i in range(min(len(rgroups), SequenceConstants.max_rgroups)):
|
|
433
|
+
if rgroups[i] is not None:
|
|
434
|
+
self._replace_rgroup(rwmol, atom_offset, rgroup_idx[i], rgroups[i])
|
|
435
|
+
|
|
436
|
+
self.mol = rwmol
|
|
437
|
+
|
|
438
|
+
def _replace_rgroup(
|
|
439
|
+
self, rdkit_mol: Chem.RWMol, atom_offset: int, atom_idx: int, atom_type: str
|
|
440
|
+
) -> None:
|
|
441
|
+
"""Replace an R-group with the appropriate atom type."""
|
|
442
|
+
absolute_idx = atom_offset + atom_idx
|
|
443
|
+
|
|
444
|
+
if atom_type == "OH":
|
|
445
|
+
try:
|
|
446
|
+
oxygen_atom = Chem.Atom(8) # Oxygen
|
|
447
|
+
rdkit_mol.ReplaceAtom(absolute_idx, oxygen_atom)
|
|
448
|
+
except Exception as e:
|
|
449
|
+
warnings.warn(f"Failed to replace R-group with OH: {e}")
|
|
450
|
+
elif atom_type != "H":
|
|
451
|
+
warnings.warn(f"Unrecognized R-group type: {atom_type}")
|
|
452
|
+
|
|
453
|
+
def _sanitize(self) -> None:
|
|
454
|
+
"""Clean up the molecule by removing dummy atoms and sanitizing."""
|
|
455
|
+
self.mol = Chem.DeleteSubstructs(self.mol, Chem.MolFromSmarts("[#0]"))
|
|
456
|
+
Chem.SanitizeMol(self.mol)
|
helmkit/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: helmkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parse HELM strings into RDKit molecules
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: rdkit>=2025.3.3
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
# helmkit
|
|
11
|
+
|
|
12
|
+
A Python library for converting HELM (Hierarchical Editing Language for Macromolecules) notation to RDKit molecules.
|
|
13
|
+
|
|
14
|
+
## Basic Usage
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from helmkit import Molecule
|
|
18
|
+
|
|
19
|
+
# Create a molecule from a HELM string
|
|
20
|
+
helm_string = "PEPTIDE1{A.R.G}$$$"
|
|
21
|
+
molecule = Molecule(helm_string)
|
|
22
|
+
|
|
23
|
+
# Access the RDKit molecule object
|
|
24
|
+
rdkit_mol = molecule.mol
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Example
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
from helmkit import Molecule
|
|
31
|
+
from rdkit.Chem import AllChem, Draw
|
|
32
|
+
|
|
33
|
+
# Create a simple tripeptide (Ala-Arg-Gly)
|
|
34
|
+
molecule = Molecule("PEPTIDE1{A.R.G}$$$")
|
|
35
|
+
|
|
36
|
+
# Generate 2D coordinates for visualization
|
|
37
|
+
AllChem.Compute2DCoords(molecule.mol)
|
|
38
|
+
|
|
39
|
+
# Save the image
|
|
40
|
+
img = Draw.MolToImage(molecule.mol)
|
|
41
|
+
img.save("tripeptide.png")
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Understanding HELM Notation
|
|
45
|
+
|
|
46
|
+
HELM (Hierarchical Editing Language for Macromolecules) is a notation for representing complex biomolecules. A basic HELM string has the following format:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
PEPTIDE1{A.R.G}$PEPTIDE2{S.G.T}$PEPTIDE1,PEPTIDE2,1:R1-4:R3$$
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Where:
|
|
53
|
+
- `PEPTIDE1{A.R.G}` defines the first chain (a peptide with amino acids A, R, G)
|
|
54
|
+
- `PEPTIDE2{S.G.T}` defines the second chain
|
|
55
|
+
- `PEPTIDE1,PEPTIDE2,1:R1-4:R3` defines a connection between the chains (R1 of residue 1 in PEPTIDE1 connects to R3 of residue 4 in PEPTIDE2)
|
|
56
|
+
- `$` characters separate different sections of the HELM string
|
|
57
|
+
|
|
58
|
+
## Using Custom Monomer Data
|
|
59
|
+
|
|
60
|
+
By default, helmkit uses the monomer data in `helmkit/data/monomers.sdf`. To use a custom SDF file:
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from helmkit import Molecule, load_monomer_library
|
|
64
|
+
|
|
65
|
+
# Load your custom monomer data
|
|
66
|
+
custom_sdf_path = "/path/to/your/custom_monomers.sdf"
|
|
67
|
+
custom_monomers = load_monomer_library(custom_sdf_path)
|
|
68
|
+
|
|
69
|
+
# Create molecule with custom monomer data
|
|
70
|
+
molecule = Molecule("PEPTIDE1{A.R.G}$$$", monomer_df=custom_monomers)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## SDF File Structure Requirements
|
|
74
|
+
|
|
75
|
+
The SDF file containing monomer data must have the following properties for each molecule:
|
|
76
|
+
|
|
77
|
+
### Required Properties:
|
|
78
|
+
- `symbol`: A unique identifier for the monomer (e.g., "A" for alanine)
|
|
79
|
+
- `m_RgroupIdx`: Comma-separated list of R-group atom indices (e.g., "1,2,None,None")
|
|
80
|
+
|
|
81
|
+
### Optional Properties:
|
|
82
|
+
- `m_Rgroups`: Comma-separated list of R-group types (e.g., "H,OH,None,None")
|
|
83
|
+
- `m_type`: Monomer type (e.g., "aa" for amino acid)
|
|
84
|
+
- `m_subtype`: Monomer subtype
|
|
85
|
+
- `m_abbr`: Monomer abbreviation
|
|
86
|
+
|
|
87
|
+
### Example SDF Entry:
|
|
88
|
+
|
|
89
|
+
```
|
|
90
|
+
Your molecule atom data here...
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
> <symbol>
|
|
94
|
+
A
|
|
95
|
+
|
|
96
|
+
> <m_Rgroups>
|
|
97
|
+
H,OH,None,None
|
|
98
|
+
|
|
99
|
+
> <m_RgroupIdx>
|
|
100
|
+
1,2,None,None
|
|
101
|
+
|
|
102
|
+
> <m_type>
|
|
103
|
+
aa
|
|
104
|
+
|
|
105
|
+
> <m_subtype>
|
|
106
|
+
natural
|
|
107
|
+
|
|
108
|
+
> <m_abbr>
|
|
109
|
+
Ala
|
|
110
|
+
|
|
111
|
+
$$$$
|
|
112
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
helmkit/__init__.py,sha256=Zyka5sRICmVd3z8L_u0mHcecPTpfSWXx1x1x7BaWLIE,183
|
|
2
|
+
helmkit/molecule.py,sha256=jlAEXk1GPL5onUcpnoAfTSXvwWkvXxp4_ffjGg6C7-g,15563
|
|
3
|
+
helmkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
helmkit/data/monomers.sdf,sha256=MgRDLSP6CINOPC8tTzkMOYjRMaK4fpe1WgSolchtUM8,454859
|
|
5
|
+
helmkit-0.1.0.dist-info/METADATA,sha256=khFwKs1ZoEMvMpusqiqVPWTJKGct0TMhP40Ez-fesLM,2714
|
|
6
|
+
helmkit-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
helmkit-0.1.0.dist-info/licenses/LICENSE,sha256=PVD0q3h7qOWaJmtnP2THjFqmIp04z9A-9G2D_th8EUU,1068
|
|
8
|
+
helmkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 adaliaramon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|