hillclimber 0.1.0a1__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.
Potentially problematic release.
This version of hillclimber might be problematic. Click here for more details.
- hillclimber/__init__.py +18 -0
- hillclimber/actions.py +30 -0
- hillclimber/calc.py +15 -0
- hillclimber/cvs.py +646 -0
- hillclimber/interfaces.py +92 -0
- hillclimber/metadynamics.py +249 -0
- hillclimber/nodes.py +6 -0
- hillclimber/selectors.py +96 -0
- hillclimber-0.1.0a1.dist-info/METADATA +412 -0
- hillclimber-0.1.0a1.dist-info/RECORD +13 -0
- hillclimber-0.1.0a1.dist-info/WHEEL +4 -0
- hillclimber-0.1.0a1.dist-info/entry_points.txt +5 -0
- hillclimber-0.1.0a1.dist-info/licenses/LICENSE +251 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import pathlib
|
|
2
|
+
from typing import Protocol
|
|
3
|
+
|
|
4
|
+
import ase
|
|
5
|
+
from ase.calculators.calculator import Calculator
|
|
6
|
+
from PIL import Image
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class NodeWithCalculator(Protocol):
|
|
10
|
+
"""Any class with a `get_calculator` method returning an ASE Calculator."""
|
|
11
|
+
|
|
12
|
+
def get_calculator(
|
|
13
|
+
self, *, directory: str | pathlib.Path | None = None, **kwargs
|
|
14
|
+
) -> Calculator: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AtomSelector(Protocol):
|
|
18
|
+
"""Protocol for selecting atoms within a single ASE Atoms object.
|
|
19
|
+
|
|
20
|
+
This interface defines the contract for selecting atoms based on various
|
|
21
|
+
criteria within an individual frame/structure.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def select(self, atoms: ase.Atoms) -> list[list[int]]:
|
|
25
|
+
"""Select atoms based on the implemented criteria.
|
|
26
|
+
|
|
27
|
+
Parameters
|
|
28
|
+
----------
|
|
29
|
+
atoms : ase.Atoms
|
|
30
|
+
The atomic structure to select from.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
list[list[int]]
|
|
35
|
+
Groups of indices of selected atoms. All indices in the inner lists
|
|
36
|
+
are representative of the same group, e.g. one molecule.
|
|
37
|
+
"""
|
|
38
|
+
...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class PlumedGenerator(Protocol):
|
|
42
|
+
"""Protocol for generating PLUMED strings from collective variables."""
|
|
43
|
+
|
|
44
|
+
def to_plumed(self, atoms: ase.Atoms) -> list[str]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class CollectiveVariable(Protocol):
|
|
48
|
+
"""Protocol for collective variables (CVs) that can be used in PLUMED."""
|
|
49
|
+
|
|
50
|
+
prefix: str
|
|
51
|
+
|
|
52
|
+
def get_img(self, atoms: ase.Atoms) -> Image.Image: ...
|
|
53
|
+
|
|
54
|
+
def to_plumed(self, atoms: ase.Atoms) -> tuple[list[str], list[str]]:
|
|
55
|
+
"""
|
|
56
|
+
Convert the collective variable to a PLUMED string.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
atoms : ase.Atoms
|
|
61
|
+
The atomic structure to use for generating the PLUMED string.
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
tuple[list[str], str]
|
|
66
|
+
- List of distance labels.
|
|
67
|
+
- list of PLUMED strings representing the CV.
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class MetadynamicsBiasCollectiveVariable(Protocol):
|
|
73
|
+
"""Protocol for metadata associated with a bias in PLUMED."""
|
|
74
|
+
|
|
75
|
+
cv: CollectiveVariable
|
|
76
|
+
sigma: float | None = None
|
|
77
|
+
grid_min: float | None = None
|
|
78
|
+
grid_max: float | None = None
|
|
79
|
+
grid_bin: int | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"NodeWithCalculator",
|
|
84
|
+
"AtomSelector",
|
|
85
|
+
"PlumedGenerator",
|
|
86
|
+
"CollectiveVariable",
|
|
87
|
+
"MetadynamicsBiasCollectiveVariable",
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
def interfaces() -> dict[str, list[str]]:
|
|
91
|
+
"""Return a dictionary of available interfaces."""
|
|
92
|
+
return {"plumed-nodes": __all__}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import ase.units
|
|
5
|
+
import zntrack
|
|
6
|
+
|
|
7
|
+
from hillclimber.calc import NonOverwritingPlumed
|
|
8
|
+
from hillclimber.interfaces import (
|
|
9
|
+
CollectiveVariable,
|
|
10
|
+
MetadynamicsBiasCollectiveVariable,
|
|
11
|
+
NodeWithCalculator,
|
|
12
|
+
PlumedGenerator,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclasses.dataclass
|
|
17
|
+
class MetaDBiasCV(MetadynamicsBiasCollectiveVariable):
|
|
18
|
+
"""Metadynamics bias on a collective variable.
|
|
19
|
+
|
|
20
|
+
Parameters
|
|
21
|
+
----------
|
|
22
|
+
cv : CollectiveVariable
|
|
23
|
+
The collective variable to bias.
|
|
24
|
+
sigma : float, optional
|
|
25
|
+
The width of the Gaussian potential, by default None.
|
|
26
|
+
grid_min : float | str, optional
|
|
27
|
+
The minimum value of the grid, by default None.
|
|
28
|
+
grid_max : float | str, optional
|
|
29
|
+
The maximum value of the grid, by default None.
|
|
30
|
+
grid_bin : int, optional
|
|
31
|
+
The number of bins in the grid, by default None.
|
|
32
|
+
|
|
33
|
+
Resources
|
|
34
|
+
---------
|
|
35
|
+
- https://www.plumed.org/doc-master/user-doc/html/METAD/
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
cv: CollectiveVariable
|
|
39
|
+
sigma: float | None = None
|
|
40
|
+
grid_min: float | str | None = None
|
|
41
|
+
grid_max: float | str | None = None
|
|
42
|
+
grid_bin: int | None = None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclasses.dataclass
|
|
46
|
+
class MetaDynamicsConfig:
|
|
47
|
+
"""Base configuration for metadynamics.
|
|
48
|
+
|
|
49
|
+
This contains only the global parameters that apply to all CVs.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
height : float, optional
|
|
54
|
+
The height of the Gaussian potential in kJ/mol, by default 1.0.
|
|
55
|
+
pace : int, optional
|
|
56
|
+
The frequency of Gaussian deposition, by default 500.
|
|
57
|
+
biasfactor : float, optional
|
|
58
|
+
The bias factor for well-tempered metadynamics, by default None.
|
|
59
|
+
temp : float, optional
|
|
60
|
+
The temperature of the system in Kelvin, by default 300.0.
|
|
61
|
+
file : str, optional
|
|
62
|
+
The name of the hills file, by default "HILLS".
|
|
63
|
+
adaptive : str, optional
|
|
64
|
+
The adaptive scheme to use, by default "NONE".
|
|
65
|
+
flush : int | None
|
|
66
|
+
The frequency of flushing the output files.
|
|
67
|
+
If None, uses the plumed default.
|
|
68
|
+
|
|
69
|
+
Resources
|
|
70
|
+
---------
|
|
71
|
+
- https://www.plumed.org/doc-master/user-doc/html/METAD/
|
|
72
|
+
- https://www.plumed.org/doc-master/user-doc/html/FLUSH/
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
height: float = 1.0 # kJ/mol
|
|
76
|
+
pace: int = 500
|
|
77
|
+
biasfactor: float | None = None
|
|
78
|
+
temp: float = 300.0
|
|
79
|
+
file: str = "HILLS"
|
|
80
|
+
adaptive: str = "NONE" # NONE, DIFF, GEOM
|
|
81
|
+
flush: int | None = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# THERE SEEMS to be an issue with MetaDynamicsModel changing but ASEMD not updating?!?!
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class MetaDynamicsModel(zntrack.Node, NodeWithCalculator):
|
|
88
|
+
"""A metadynamics model.
|
|
89
|
+
|
|
90
|
+
Parameters
|
|
91
|
+
----------
|
|
92
|
+
config : MetaDynamicsConfig
|
|
93
|
+
The configuration for the metadynamics simulation.
|
|
94
|
+
data : list[ase.Atoms]
|
|
95
|
+
The input data for the simulation.
|
|
96
|
+
data_idx : int, optional
|
|
97
|
+
The index of the data to use, by default -1.
|
|
98
|
+
bias_cvs : list[MetaDBiasCV], optional
|
|
99
|
+
The collective variables to bias, by default [].
|
|
100
|
+
actions : list[PlumedGenerator], optional
|
|
101
|
+
A list of actions to perform during the simulation, by default [].
|
|
102
|
+
timestep : float, optional
|
|
103
|
+
The timestep of the simulation in fs, by default 1.0.
|
|
104
|
+
model : NodeWithCalculator
|
|
105
|
+
The model to use for the simulation.
|
|
106
|
+
|
|
107
|
+
Example
|
|
108
|
+
-------
|
|
109
|
+
>>> import hillclimber as pn
|
|
110
|
+
>>> import ipsuite as ips
|
|
111
|
+
>>>
|
|
112
|
+
>>> data = ips.AddData("seed.xyz")
|
|
113
|
+
>>> cv1 = pn.DistanceCV(
|
|
114
|
+
... x1=pn.SMARTSSelector(pattern="[H]O[H]"),
|
|
115
|
+
... x2=pn.SMARTSSelector(pattern="CO[C:1]"),
|
|
116
|
+
... prefix="d",
|
|
117
|
+
... )
|
|
118
|
+
>>> metad_cv1 = pn.MetaDBiasCV(
|
|
119
|
+
... cv=cv1, sigma=0.1, grid_min=0.0, grid_max=2.0, grid_bin=200
|
|
120
|
+
... )
|
|
121
|
+
>>> model = pn.MetaDynamicsModel(
|
|
122
|
+
... config=pn.MetaDynamicsConfig(height=0.25, temp=300, pace=2000, biasfactor=10),
|
|
123
|
+
... bias_cvs=[metad_cv1],
|
|
124
|
+
... data=data.frames,
|
|
125
|
+
... model=ips.MACEMP(),
|
|
126
|
+
... timestep=0.5
|
|
127
|
+
... )
|
|
128
|
+
>>> md = ips.ASEMD(
|
|
129
|
+
... model=model, data=data.frames, ...
|
|
130
|
+
... )
|
|
131
|
+
"""
|
|
132
|
+
|
|
133
|
+
config: MetaDynamicsConfig = zntrack.deps()
|
|
134
|
+
data: list[ase.Atoms] = zntrack.deps()
|
|
135
|
+
data_idx: int = zntrack.params(-1)
|
|
136
|
+
bias_cvs: list[MetaDBiasCV] = zntrack.deps(default_factory=list)
|
|
137
|
+
actions: list[PlumedGenerator] = zntrack.deps(default_factory=list)
|
|
138
|
+
timestep: float = zntrack.params(1.0) # in fs, default is 1 fs
|
|
139
|
+
model: NodeWithCalculator = zntrack.deps()
|
|
140
|
+
|
|
141
|
+
figures: Path = zntrack.outs_path(zntrack.nwd / "figures", independent=True)
|
|
142
|
+
|
|
143
|
+
def run(self):
|
|
144
|
+
self.figures.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
for cv in self.bias_cvs:
|
|
146
|
+
img = cv.cv.get_img(self.data[self.data_idx])
|
|
147
|
+
img.save(self.figures / f"{cv.cv.prefix}.png")
|
|
148
|
+
|
|
149
|
+
def get_calculator(
|
|
150
|
+
self, *, directory: str | Path | None = None, **kwargs
|
|
151
|
+
) -> NonOverwritingPlumed:
|
|
152
|
+
if directory is None:
|
|
153
|
+
raise ValueError("Directory must be specified for PLUMED input files.")
|
|
154
|
+
directory = Path(directory)
|
|
155
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
|
|
157
|
+
lines = self.to_plumed(self.data[self.data_idx])
|
|
158
|
+
# replace FILE= with f"FILE={directory}/" inside config
|
|
159
|
+
lines = [line.replace("FILE=", f"FILE={directory}/") for line in lines]
|
|
160
|
+
|
|
161
|
+
# Write plumed input file
|
|
162
|
+
with (directory / "plumed.dat").open("w") as file:
|
|
163
|
+
for line in lines:
|
|
164
|
+
file.write(line + "\n")
|
|
165
|
+
|
|
166
|
+
kT = ase.units.kB * self.config.temp
|
|
167
|
+
|
|
168
|
+
return NonOverwritingPlumed(
|
|
169
|
+
calc=self.model.get_calculator(directory=directory),
|
|
170
|
+
atoms=self.data[self.data_idx],
|
|
171
|
+
input=lines,
|
|
172
|
+
timestep=float(self.timestep * ase.units.fs),
|
|
173
|
+
kT=float(kT),
|
|
174
|
+
log=(directory / "plumed.log").as_posix(),
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
def to_plumed(self, atoms: ase.Atoms) -> list[str]:
|
|
178
|
+
"""Generate PLUMED input string for the metadynamics model."""
|
|
179
|
+
# check for duplicate CV prefixes
|
|
180
|
+
cv_labels = set()
|
|
181
|
+
for bias_cv in self.bias_cvs:
|
|
182
|
+
if bias_cv.cv.prefix in cv_labels:
|
|
183
|
+
raise ValueError(f"Duplicate CV prefix found: {bias_cv.cv.prefix}")
|
|
184
|
+
cv_labels.add(bias_cv.cv.prefix)
|
|
185
|
+
|
|
186
|
+
plumed_lines = []
|
|
187
|
+
all_labels = []
|
|
188
|
+
|
|
189
|
+
sigmas, grid_mins, grid_maxs, grid_bins = [], [], [], []
|
|
190
|
+
|
|
191
|
+
plumed_lines.append(
|
|
192
|
+
f"UNITS LENGTH=A TIME={1 / (1000 * ase.units.fs)} ENERGY={ase.units.mol / ase.units.kJ}"
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
for bias_cv in self.bias_cvs:
|
|
196
|
+
labels, cv_str = bias_cv.cv.to_plumed(atoms)
|
|
197
|
+
plumed_lines.extend(cv_str)
|
|
198
|
+
all_labels.extend(labels)
|
|
199
|
+
|
|
200
|
+
# Collect per-CV parameters for later
|
|
201
|
+
sigmas.append(str(bias_cv.sigma) if bias_cv.sigma is not None else None)
|
|
202
|
+
grid_mins.append(
|
|
203
|
+
str(bias_cv.grid_min) if bias_cv.grid_min is not None else None
|
|
204
|
+
)
|
|
205
|
+
grid_maxs.append(
|
|
206
|
+
str(bias_cv.grid_max) if bias_cv.grid_max is not None else None
|
|
207
|
+
)
|
|
208
|
+
grid_bins.append(
|
|
209
|
+
str(bias_cv.grid_bin) if bias_cv.grid_bin is not None else None
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
metad_parts = [
|
|
213
|
+
"METAD",
|
|
214
|
+
f"ARG={','.join(all_labels)}",
|
|
215
|
+
f"HEIGHT={self.config.height}",
|
|
216
|
+
f"PACE={self.config.pace}",
|
|
217
|
+
f"TEMP={self.config.temp}",
|
|
218
|
+
f"FILE={self.config.file}",
|
|
219
|
+
f"ADAPTIVE={self.config.adaptive}",
|
|
220
|
+
]
|
|
221
|
+
if self.config.biasfactor is not None:
|
|
222
|
+
metad_parts.append(f"BIASFACTOR={self.config.biasfactor}")
|
|
223
|
+
|
|
224
|
+
# Add SIGMA, GRID_MIN, GRID_MAX, GRID_BIN only if any value is set
|
|
225
|
+
if any(v is not None for v in sigmas):
|
|
226
|
+
metad_parts.append(
|
|
227
|
+
f"SIGMA={','.join(v if v is not None else '0.0' for v in sigmas)}"
|
|
228
|
+
)
|
|
229
|
+
if any(v is not None for v in grid_mins):
|
|
230
|
+
metad_parts.append(
|
|
231
|
+
f"GRID_MIN={','.join(v if v is not None else '0.0' for v in grid_mins)}"
|
|
232
|
+
)
|
|
233
|
+
if any(v is not None for v in grid_maxs):
|
|
234
|
+
metad_parts.append(
|
|
235
|
+
f"GRID_MAX={','.join(v if v is not None else '0.0' for v in grid_maxs)}"
|
|
236
|
+
)
|
|
237
|
+
if any(v is not None for v in grid_bins):
|
|
238
|
+
metad_parts.append(
|
|
239
|
+
f"GRID_BIN={','.join(v if v is not None else '0' for v in grid_bins)}"
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
plumed_lines.append(f"metad: {' '.join(metad_parts)}")
|
|
243
|
+
# Temporary until https://github.com/zincware/ZnTrack/issues/936
|
|
244
|
+
from hillclimber.actions import PrintCVAction
|
|
245
|
+
lines = PrintCVAction(cvs=[x.cv for x in self.bias_cvs], stride=100).to_plumed(atoms)
|
|
246
|
+
plumed_lines.extend(lines)
|
|
247
|
+
if self.config.flush is not None:
|
|
248
|
+
plumed_lines.append(f"FLUSH STRIDE={self.config.flush}")
|
|
249
|
+
return plumed_lines
|
hillclimber/nodes.py
ADDED
hillclimber/selectors.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import typing as tp
|
|
3
|
+
|
|
4
|
+
import ase
|
|
5
|
+
import rdkit2ase
|
|
6
|
+
|
|
7
|
+
from hillclimber.interfaces import AtomSelector
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclasses.dataclass
|
|
11
|
+
class IndexSelector(AtomSelector):
|
|
12
|
+
"""Select atoms based on grouped indices.
|
|
13
|
+
|
|
14
|
+
Parameters
|
|
15
|
+
----------
|
|
16
|
+
indices : list[list[int]]
|
|
17
|
+
A list of atom index groups to select. Each inner list represents
|
|
18
|
+
a group of atoms (e.g., a molecule). For example:
|
|
19
|
+
- [[0, 1], [2, 3]] selects two groups: atoms [0,1] and atoms [2,3]
|
|
20
|
+
- [[0], [1]] selects two single-atom groups
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
# mostly used for debugging
|
|
24
|
+
indices: list[list[int]]
|
|
25
|
+
|
|
26
|
+
def select(self, atoms: ase.Atoms) -> list[list[int]]:
|
|
27
|
+
return self.indices
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclasses.dataclass
|
|
31
|
+
class SMILESSelector(AtomSelector):
|
|
32
|
+
"""Select atoms based on a SMILES string.
|
|
33
|
+
|
|
34
|
+
Parameters
|
|
35
|
+
----------
|
|
36
|
+
smiles : str
|
|
37
|
+
The SMILES string to use for selection.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
smiles: str
|
|
41
|
+
|
|
42
|
+
def select(self, atoms: ase.Atoms) -> list[list[int]]:
|
|
43
|
+
matches = rdkit2ase.match_substructure(atoms, smiles=self.smiles)
|
|
44
|
+
return [list(match) for match in matches]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclasses.dataclass
|
|
48
|
+
class SMARTSSelector(AtomSelector):
|
|
49
|
+
"""Select atoms based on SMARTS or mapped SMILES patterns.
|
|
50
|
+
|
|
51
|
+
This selector uses RDKit's substructure matching to identify atoms
|
|
52
|
+
matching a given SMARTS pattern or mapped SMILES string. It supports
|
|
53
|
+
flexible hydrogen handling and can work with mapped atoms for
|
|
54
|
+
precise selection.
|
|
55
|
+
|
|
56
|
+
Note
|
|
57
|
+
----
|
|
58
|
+
The selector is applied only to the first trajectory frame.
|
|
59
|
+
Since indices can change during e.g. proton transfer, biasing specific groups (e.g. `[OH-]`) may fail.
|
|
60
|
+
In such cases, select all `[OH2]` and `[OH-]` groups and use CoordinationNumber CVs.
|
|
61
|
+
Account for this method with all changes in chemical structure.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
pattern : str
|
|
66
|
+
SMARTS pattern (e.g., "[F]", "[OH]", "C(=O)O") or SMILES with
|
|
67
|
+
atom maps (e.g., "C1[C:1]OC(=[O:1])O1"). If atom maps are present,
|
|
68
|
+
only the mapped atoms are selected.
|
|
69
|
+
hydrogens : {'exclude', 'include', 'isolated'}, default='exclude'
|
|
70
|
+
How to handle hydrogen atoms in the selection:
|
|
71
|
+
- 'exclude': Remove all hydrogens from the selection
|
|
72
|
+
- 'include': Include hydrogens bonded to selected heavy atoms
|
|
73
|
+
- 'isolated': Select only hydrogens bonded to selected heavy atoms
|
|
74
|
+
|
|
75
|
+
Examples
|
|
76
|
+
--------
|
|
77
|
+
>>> # Select all fluorine atoms
|
|
78
|
+
>>> selector = SMARTSSelection(pattern="[F]")
|
|
79
|
+
|
|
80
|
+
>>> # Select carboxylic acid groups including hydrogens
|
|
81
|
+
>>> selector = SMARTSSelection(pattern="C(=O)O", hydrogens="include")
|
|
82
|
+
|
|
83
|
+
>>> # Select only specific mapped atoms
|
|
84
|
+
>>> selector = SMARTSSelection(pattern="C1[C:1]OC(=[O:1])O1")
|
|
85
|
+
|
|
86
|
+
>>> # Select 4 elements in order to define an angle
|
|
87
|
+
>>> selector = SMARTSSelection(pattern="CC(=O)N[C:1]([C:2])[C:3](=O)[N:4]C")
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
pattern: str
|
|
91
|
+
hydrogens: tp.Literal["include", "exclude", "isolated"] = "exclude"
|
|
92
|
+
|
|
93
|
+
def select(self, atoms: ase.Atoms) -> list[list[int]]:
|
|
94
|
+
return rdkit2ase.select_atoms_grouped(
|
|
95
|
+
rdkit2ase.ase2rdkit(atoms), self.pattern, self.hydrogens
|
|
96
|
+
)
|