helmkit 0.7.6__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.6 → helmkit-0.7.7}/PKG-INFO +2 -2
- {helmkit-0.7.6 → helmkit-0.7.7}/pyproject.toml +6 -1
- {helmkit-0.7.6 → helmkit-0.7.7}/src/helmkit/molecule.py +30 -28
- {helmkit-0.7.6 → helmkit-0.7.7}/uv.lock +280 -203
- {helmkit-0.7.6 → helmkit-0.7.7}/.gitignore +0 -0
- {helmkit-0.7.6 → helmkit-0.7.7}/.python-version +0 -0
- {helmkit-0.7.6 → helmkit-0.7.7}/LICENSE +0 -0
- {helmkit-0.7.6 → helmkit-0.7.7}/README.md +0 -0
- {helmkit-0.7.6 → helmkit-0.7.7}/src/helmkit/__init__.py +0 -0
- {helmkit-0.7.6 → helmkit-0.7.7}/src/helmkit/data/monomers.sdf +0 -0
- {helmkit-0.7.6 → 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
|
|
@@ -21,9 +25,7 @@ MAX_RGROUPS = 4
|
|
|
21
25
|
def get_molecule_property(
|
|
22
26
|
molecule: Chem.Mol, property_name: str, default: str | None = None
|
|
23
27
|
) -> str | None:
|
|
24
|
-
return (
|
|
25
|
-
molecule.GetProp(property_name) if molecule.HasProp(property_name) else default
|
|
26
|
-
)
|
|
28
|
+
return molecule.GetProp(property_name, default=default)
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
T = TypeVar("T")
|
|
@@ -68,14 +70,14 @@ def infer_attachment_points(
|
|
|
68
70
|
continue
|
|
69
71
|
|
|
70
72
|
atom = molecule.GetAtomWithIdx(r_idx)
|
|
73
|
+
bonds: tuple[Chem.Bond, ...] = atom.GetBonds()
|
|
71
74
|
|
|
72
|
-
for bond in
|
|
75
|
+
for bond in bonds:
|
|
73
76
|
other_idx = bond.GetOtherAtomIdx(r_idx)
|
|
74
77
|
attachment_points.append(other_idx)
|
|
75
78
|
break
|
|
76
79
|
else:
|
|
77
|
-
|
|
78
|
-
warnings.warn(
|
|
80
|
+
raise ValueError(
|
|
79
81
|
f"R-group atom {r_idx} has no bonds to determine attachment point"
|
|
80
82
|
)
|
|
81
83
|
|
|
@@ -102,12 +104,13 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
|
|
|
102
104
|
monomers_dict: MonomerLibrary = defaultdict(dict)
|
|
103
105
|
supplier = Chem.SDMolSupplier(library_path, removeHs=False)
|
|
104
106
|
|
|
105
|
-
for mol in supplier:
|
|
107
|
+
for mol in cast(Iterable[Chem.Mol | None], supplier):
|
|
106
108
|
if mol is None:
|
|
107
109
|
continue
|
|
108
110
|
|
|
109
111
|
symbol = get_molecule_property(mol, "symbol")
|
|
110
112
|
if not symbol:
|
|
113
|
+
warnings.warn("Monomer without a symbol property will be skipped")
|
|
111
114
|
continue
|
|
112
115
|
|
|
113
116
|
m_type = get_molecule_property(mol, "m_type", "")
|
|
@@ -121,9 +124,9 @@ def load_monomer_library(library_path: str | None = None) -> MonomerLibrary:
|
|
|
121
124
|
rgroup_idx = parse_comma_separated_property(mol, "m_RgroupIdx", int)
|
|
122
125
|
attachment_point_idx = infer_attachment_points(mol, rgroup_idx)
|
|
123
126
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
|
127
130
|
|
|
128
131
|
monomers_dict[m_type][symbol] = {
|
|
129
132
|
"m_romol": mol,
|
|
@@ -294,10 +297,10 @@ class Molecule:
|
|
|
294
297
|
def _split_helm_sections(
|
|
295
298
|
self, helm: str
|
|
296
299
|
) -> tuple[list[str], list[str], list[str], str, str]:
|
|
297
|
-
parts = self._dollar_outside_brackets.split(helm, 4)
|
|
300
|
+
parts: list[str] = self._dollar_outside_brackets.split(helm, 4)
|
|
298
301
|
parts.extend([""] * (5 - len(parts)))
|
|
299
302
|
|
|
300
|
-
polymers = (
|
|
303
|
+
polymers: list[str] = (
|
|
301
304
|
self._pipe_outside_brackets.split(parts[0])
|
|
302
305
|
if "|" in parts[0]
|
|
303
306
|
else [parts[0]]
|
|
@@ -332,17 +335,18 @@ class Molecule:
|
|
|
332
335
|
|
|
333
336
|
return result
|
|
334
337
|
|
|
335
|
-
|
|
338
|
+
@staticmethod
|
|
339
|
+
def _extract_polymer_type(chain_str: str) -> Literal["PEPTIDE", "RNA", "CHEM"]:
|
|
336
340
|
"""Extract chain ID and return (chain_id, polymer_type)."""
|
|
337
341
|
match = re.fullmatch(r"([A-Z]+)(\d+)", chain_str)
|
|
338
342
|
if not match:
|
|
339
343
|
raise ValueError(f"Invalid chain format: {chain_str}")
|
|
340
344
|
|
|
341
|
-
polymer_type = match.group(1)
|
|
345
|
+
polymer_type: str = match.group(1)
|
|
342
346
|
if polymer_type not in {"PEPTIDE", "RNA", "CHEM"}:
|
|
343
347
|
raise ValueError(f"Unsupported polymer type: {polymer_type}")
|
|
344
348
|
|
|
345
|
-
return
|
|
349
|
+
return polymer_type
|
|
346
350
|
|
|
347
351
|
def _process_monomer(
|
|
348
352
|
self, monomer_name: str, chain_id: str, residue_idx: int, polymer_type: str
|
|
@@ -353,7 +357,7 @@ class Molecule:
|
|
|
353
357
|
if monomer_name.startswith("[") and monomer_name.endswith("]")
|
|
354
358
|
else monomer_name
|
|
355
359
|
)
|
|
356
|
-
if monomer_name
|
|
360
|
+
if not monomer_name:
|
|
357
361
|
raise ValueError(f"Monomer {residue_idx + 1} has no name. Check HELM.")
|
|
358
362
|
|
|
359
363
|
# Check for (a,[b]) pattern
|
|
@@ -426,8 +430,8 @@ class Molecule:
|
|
|
426
430
|
warnings.warn(f"No sequence in polymer: {chain}")
|
|
427
431
|
continue
|
|
428
432
|
|
|
429
|
-
|
|
430
|
-
|
|
433
|
+
chain_id = chain[: match.start()]
|
|
434
|
+
polymer_type = self._extract_polymer_type(chain_id)
|
|
431
435
|
|
|
432
436
|
if chain_id in self.chain_offset:
|
|
433
437
|
raise ValueError(f"Duplicate chain ID: {chain_id}")
|
|
@@ -445,8 +449,6 @@ class Molecule:
|
|
|
445
449
|
monomer = self._process_monomer(
|
|
446
450
|
monomer_name, chain_id, residue_idx, polymer_type
|
|
447
451
|
)
|
|
448
|
-
if not monomer:
|
|
449
|
-
continue
|
|
450
452
|
|
|
451
453
|
self.monomers.append(monomer)
|
|
452
454
|
self.residue_reps[chain_id].append(monomer_idx)
|
|
@@ -550,9 +552,12 @@ class Molecule:
|
|
|
550
552
|
self.monomers.append(monomer)
|
|
551
553
|
self.residue_reps[chain_id].append(monomer_idx)
|
|
552
554
|
monomer_idx += 1
|
|
555
|
+
else:
|
|
556
|
+
assert_never(polymer_type)
|
|
553
557
|
|
|
558
|
+
@staticmethod
|
|
554
559
|
def _parse_connection(
|
|
555
|
-
|
|
560
|
+
connection_str: str,
|
|
556
561
|
) -> tuple[str, int, int, str, int, int] | None:
|
|
557
562
|
"""Parse a single connection string."""
|
|
558
563
|
parts = connection_str.split(",")
|
|
@@ -661,7 +666,7 @@ class Molecule:
|
|
|
661
666
|
rgroup_idx = monomer["m_RgroupIdx"]
|
|
662
667
|
for i in range(min(len(rgroups), MAX_RGROUPS)):
|
|
663
668
|
if rgroups[i] is not None:
|
|
664
|
-
self._replace_rgroup(
|
|
669
|
+
self._replace_rgroup(0, rgroup_idx[i], rgroups[i])
|
|
665
670
|
|
|
666
671
|
current_offset = self._mol.GetNumAtoms()
|
|
667
672
|
self.offset = [0, current_offset]
|
|
@@ -673,9 +678,7 @@ class Molecule:
|
|
|
673
678
|
rgroup_idx = monomer["m_RgroupIdx"]
|
|
674
679
|
for i in range(min(len(rgroups), MAX_RGROUPS)):
|
|
675
680
|
if rgroups[i] is not None:
|
|
676
|
-
self._replace_rgroup(
|
|
677
|
-
self._mol, current_offset, rgroup_idx[i], rgroups[i]
|
|
678
|
-
)
|
|
681
|
+
self._replace_rgroup(current_offset, rgroup_idx[i], rgroups[i])
|
|
679
682
|
|
|
680
683
|
atom_count = monomer["m_romol"].GetNumAtoms()
|
|
681
684
|
current_offset += atom_count
|
|
@@ -694,10 +697,9 @@ class Molecule:
|
|
|
694
697
|
absolute_atom1_idx, absolute_atom2_idx, Chem.BondType.SINGLE
|
|
695
698
|
)
|
|
696
699
|
|
|
697
|
-
def _replace_rgroup(
|
|
698
|
-
self, rdkit_mol: Chem.RWMol, atom_offset: int, atom_idx: int, atom_type: str
|
|
699
|
-
) -> None:
|
|
700
|
+
def _replace_rgroup(self, atom_offset: int, atom_idx: int, atom_type: str) -> None:
|
|
700
701
|
"""Replace an R-group with the appropriate atom type."""
|
|
702
|
+
rdkit_mol = self.mol
|
|
701
703
|
absolute_idx = atom_offset + atom_idx
|
|
702
704
|
|
|
703
705
|
if atom_type == "OH":
|