helmkit 0.7.5__tar.gz → 0.7.7__tar.gz
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-0.7.5 → helmkit-0.7.7}/PKG-INFO +2 -2
- {helmkit-0.7.5 → helmkit-0.7.7}/pyproject.toml +6 -1
- helmkit-0.7.7/src/helmkit/__init__.py +6 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/src/helmkit/molecule.py +36 -35
- {helmkit-0.7.5 → helmkit-0.7.7}/uv.lock +280 -203
- helmkit-0.7.5/src/helmkit/__init__.py +0 -6
- {helmkit-0.7.5 → helmkit-0.7.7}/.gitignore +0 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/.python-version +0 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/LICENSE +0 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/README.md +0 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/src/helmkit/data/monomers.sdf +0 -0
- {helmkit-0.7.5 → helmkit-0.7.7}/src/helmkit/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "helmkit"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.7"
|
|
4
4
|
description = "Parse HELM strings into RDKit molecules"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -47,6 +47,11 @@ ignore = ["I001", "N999"]
|
|
|
47
47
|
[tool.ruff.format]
|
|
48
48
|
skip-magic-trailing-comma = true
|
|
49
49
|
|
|
50
|
+
[tool.ty.rules]
|
|
51
|
+
all = "error"
|
|
52
|
+
unsound-return-statement = "ignore"
|
|
53
|
+
unsound-assignment = "ignore"
|
|
54
|
+
|
|
50
55
|
[tool.ty.src]
|
|
51
56
|
include = ["src"]
|
|
52
57
|
exclude = ["tests"]
|
|
@@ -4,9 +4,13 @@ import re
|
|
|
4
4
|
import warnings
|
|
5
5
|
from collections import defaultdict
|
|
6
6
|
from collections.abc import Callable
|
|
7
|
+
from collections.abc import Iterable
|
|
7
8
|
from collections.abc import Sequence
|
|
8
9
|
from functools import lru_cache
|
|
9
10
|
from importlib.resources import files
|
|
11
|
+
from typing import assert_never
|
|
12
|
+
from typing import cast
|
|
13
|
+
from typing import Literal
|
|
10
14
|
from typing import overload
|
|
11
15
|
from typing import TypedDict
|
|
12
16
|
from typing import TypeVar
|
|
@@ -15,16 +19,13 @@ from rdkit import Chem
|
|
|
15
19
|
from rdkit import rdBase
|
|
16
20
|
|
|
17
21
|
|
|
18
|
-
|
|
19
|
-
max_rgroups: int = 4
|
|
22
|
+
MAX_RGROUPS = 4
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
def get_molecule_property(
|
|
23
26
|
molecule: Chem.Mol, property_name: str, default: str | None = None
|
|
24
27
|
) -> str | None:
|
|
25
|
-
return (
|
|
26
|
-
molecule.GetProp(property_name) if molecule.HasProp(property_name) else default
|
|
27
|
-
)
|
|
28
|
+
return molecule.GetProp(property_name, default=default)
|
|
28
29
|
|
|
29
30
|
|
|
30
31
|
T = TypeVar("T")
|
|
@@ -69,14 +70,14 @@ def infer_attachment_points(
|
|
|
69
70
|
continue
|
|
70
71
|
|
|
71
72
|
atom = molecule.GetAtomWithIdx(r_idx)
|
|
73
|
+
bonds: tuple[Chem.Bond, ...] = atom.GetBonds()
|
|
72
74
|
|
|
73
|
-
for bond in
|
|
75
|
+
for bond in bonds:
|
|
74
76
|
other_idx = bond.GetOtherAtomIdx(r_idx)
|
|
75
77
|
attachment_points.append(other_idx)
|
|
76
78
|
break
|
|
77
79
|
else:
|
|
78
|
-
|
|
79
|
-
warnings.warn(
|
|
80
|
+
raise ValueError(
|
|
80
81
|
f"R-group atom {r_idx} has no bonds to determine attachment point"
|
|
81
82
|
)
|
|
82
83
|
|
|
@@ -103,12 +104,13 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
|
|
|
103
104
|
monomers_dict: MonomerLibrary = defaultdict(dict)
|
|
104
105
|
supplier = Chem.SDMolSupplier(library_path, removeHs=False)
|
|
105
106
|
|
|
106
|
-
for mol in supplier:
|
|
107
|
+
for mol in cast(Iterable[Chem.Mol | None], supplier):
|
|
107
108
|
if mol is None:
|
|
108
109
|
continue
|
|
109
110
|
|
|
110
111
|
symbol = get_molecule_property(mol, "symbol")
|
|
111
112
|
if not symbol:
|
|
113
|
+
warnings.warn("Monomer without a symbol property will be skipped")
|
|
112
114
|
continue
|
|
113
115
|
|
|
114
116
|
m_type = get_molecule_property(mol, "m_type", "")
|
|
@@ -122,9 +124,9 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
|
|
|
122
124
|
rgroup_idx = parse_comma_separated_property(mol, "m_RgroupIdx", int)
|
|
123
125
|
attachment_point_idx = infer_attachment_points(mol, rgroup_idx)
|
|
124
126
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
127
|
+
# m_abbr is only used for display, so fall back to the symbol when a
|
|
128
|
+
# library does not provide it instead of dropping the monomer.
|
|
129
|
+
abbr = get_molecule_property(mol, "m_abbr") or symbol
|
|
128
130
|
|
|
129
131
|
monomers_dict[m_type][symbol] = {
|
|
130
132
|
"m_romol": mol,
|
|
@@ -183,13 +185,13 @@ def _create_missing_monomer(monomer_name: str, m_type: str = "aa") -> MonomerDat
|
|
|
183
185
|
r_group_idx = [idx for _, idx in sorted_r]
|
|
184
186
|
mol = Chem.RenumberAtoms(mol, main_atoms + r_group_idx)
|
|
185
187
|
|
|
186
|
-
rgroup_idx_full: list[int | None] = [None] *
|
|
188
|
+
rgroup_idx_full: list[int | None] = [None] * MAX_RGROUPS
|
|
187
189
|
for i, (r_num, _) in enumerate(sorted_r):
|
|
188
|
-
if 1 <= r_num <=
|
|
190
|
+
if 1 <= r_num <= MAX_RGROUPS:
|
|
189
191
|
rgroup_idx_full[r_num - 1] = len(main_atoms) + i
|
|
190
192
|
|
|
191
193
|
attachment_points = infer_attachment_points(mol, rgroup_idx_full)
|
|
192
|
-
rgroup_vals: list[str | None] = [None] *
|
|
194
|
+
rgroup_vals: list[str | None] = [None] * MAX_RGROUPS
|
|
193
195
|
|
|
194
196
|
if m_type == "aa" and "_R1" not in monomer_name:
|
|
195
197
|
matches = {
|
|
@@ -295,10 +297,10 @@ class Molecule:
|
|
|
295
297
|
def _split_helm_sections(
|
|
296
298
|
self, helm: str
|
|
297
299
|
) -> tuple[list[str], list[str], list[str], str, str]:
|
|
298
|
-
parts = self._dollar_outside_brackets.split(helm, 4)
|
|
300
|
+
parts: list[str] = self._dollar_outside_brackets.split(helm, 4)
|
|
299
301
|
parts.extend([""] * (5 - len(parts)))
|
|
300
302
|
|
|
301
|
-
polymers = (
|
|
303
|
+
polymers: list[str] = (
|
|
302
304
|
self._pipe_outside_brackets.split(parts[0])
|
|
303
305
|
if "|" in parts[0]
|
|
304
306
|
else [parts[0]]
|
|
@@ -333,17 +335,18 @@ class Molecule:
|
|
|
333
335
|
|
|
334
336
|
return result
|
|
335
337
|
|
|
336
|
-
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _extract_polymer_type(chain_str: str) -> Literal["PEPTIDE", "RNA", "CHEM"]:
|
|
337
340
|
"""Extract chain ID and return (chain_id, polymer_type)."""
|
|
338
341
|
match = re.fullmatch(r"([A-Z]+)(\d+)", chain_str)
|
|
339
342
|
if not match:
|
|
340
343
|
raise ValueError(f"Invalid chain format: {chain_str}")
|
|
341
344
|
|
|
342
|
-
polymer_type = match.group(1)
|
|
345
|
+
polymer_type: str = match.group(1)
|
|
343
346
|
if polymer_type not in {"PEPTIDE", "RNA", "CHEM"}:
|
|
344
347
|
raise ValueError(f"Unsupported polymer type: {polymer_type}")
|
|
345
348
|
|
|
346
|
-
return
|
|
349
|
+
return polymer_type
|
|
347
350
|
|
|
348
351
|
def _process_monomer(
|
|
349
352
|
self, monomer_name: str, chain_id: str, residue_idx: int, polymer_type: str
|
|
@@ -354,7 +357,7 @@ class Molecule:
|
|
|
354
357
|
if monomer_name.startswith("[") and monomer_name.endswith("]")
|
|
355
358
|
else monomer_name
|
|
356
359
|
)
|
|
357
|
-
if monomer_name
|
|
360
|
+
if not monomer_name:
|
|
358
361
|
raise ValueError(f"Monomer {residue_idx + 1} has no name. Check HELM.")
|
|
359
362
|
|
|
360
363
|
# Check for (a,[b]) pattern
|
|
@@ -427,8 +430,8 @@ class Molecule:
|
|
|
427
430
|
warnings.warn(f"No sequence in polymer: {chain}")
|
|
428
431
|
continue
|
|
429
432
|
|
|
430
|
-
|
|
431
|
-
|
|
433
|
+
chain_id = chain[: match.start()]
|
|
434
|
+
polymer_type = self._extract_polymer_type(chain_id)
|
|
432
435
|
|
|
433
436
|
if chain_id in self.chain_offset:
|
|
434
437
|
raise ValueError(f"Duplicate chain ID: {chain_id}")
|
|
@@ -446,8 +449,6 @@ class Molecule:
|
|
|
446
449
|
monomer = self._process_monomer(
|
|
447
450
|
monomer_name, chain_id, residue_idx, polymer_type
|
|
448
451
|
)
|
|
449
|
-
if not monomer:
|
|
450
|
-
continue
|
|
451
452
|
|
|
452
453
|
self.monomers.append(monomer)
|
|
453
454
|
self.residue_reps[chain_id].append(monomer_idx)
|
|
@@ -551,9 +552,12 @@ class Molecule:
|
|
|
551
552
|
self.monomers.append(monomer)
|
|
552
553
|
self.residue_reps[chain_id].append(monomer_idx)
|
|
553
554
|
monomer_idx += 1
|
|
555
|
+
else:
|
|
556
|
+
assert_never(polymer_type)
|
|
554
557
|
|
|
558
|
+
@staticmethod
|
|
555
559
|
def _parse_connection(
|
|
556
|
-
|
|
560
|
+
connection_str: str,
|
|
557
561
|
) -> tuple[str, int, int, str, int, int] | None:
|
|
558
562
|
"""Parse a single connection string."""
|
|
559
563
|
parts = connection_str.split(",")
|
|
@@ -660,9 +664,9 @@ class Molecule:
|
|
|
660
664
|
|
|
661
665
|
rgroups = monomer["m_Rgroups"]
|
|
662
666
|
rgroup_idx = monomer["m_RgroupIdx"]
|
|
663
|
-
for i in range(min(len(rgroups),
|
|
667
|
+
for i in range(min(len(rgroups), MAX_RGROUPS)):
|
|
664
668
|
if rgroups[i] is not None:
|
|
665
|
-
self._replace_rgroup(
|
|
669
|
+
self._replace_rgroup(0, rgroup_idx[i], rgroups[i])
|
|
666
670
|
|
|
667
671
|
current_offset = self._mol.GetNumAtoms()
|
|
668
672
|
self.offset = [0, current_offset]
|
|
@@ -672,11 +676,9 @@ class Molecule:
|
|
|
672
676
|
|
|
673
677
|
rgroups = monomer["m_Rgroups"]
|
|
674
678
|
rgroup_idx = monomer["m_RgroupIdx"]
|
|
675
|
-
for i in range(min(len(rgroups),
|
|
679
|
+
for i in range(min(len(rgroups), MAX_RGROUPS)):
|
|
676
680
|
if rgroups[i] is not None:
|
|
677
|
-
self._replace_rgroup(
|
|
678
|
-
self._mol, current_offset, rgroup_idx[i], rgroups[i]
|
|
679
|
-
)
|
|
681
|
+
self._replace_rgroup(current_offset, rgroup_idx[i], rgroups[i])
|
|
680
682
|
|
|
681
683
|
atom_count = monomer["m_romol"].GetNumAtoms()
|
|
682
684
|
current_offset += atom_count
|
|
@@ -695,10 +697,9 @@ class Molecule:
|
|
|
695
697
|
absolute_atom1_idx, absolute_atom2_idx, Chem.BondType.SINGLE
|
|
696
698
|
)
|
|
697
699
|
|
|
698
|
-
def _replace_rgroup(
|
|
699
|
-
self, rdkit_mol: Chem.RWMol, atom_offset: int, atom_idx: int, atom_type: str
|
|
700
|
-
) -> None:
|
|
700
|
+
def _replace_rgroup(self, atom_offset: int, atom_idx: int, atom_type: str) -> None:
|
|
701
701
|
"""Replace an R-group with the appropriate atom type."""
|
|
702
|
+
rdkit_mol = self.mol
|
|
702
703
|
absolute_idx = atom_offset + atom_idx
|
|
703
704
|
|
|
704
705
|
if atom_type == "OH":
|