bfflearn 0.1.0.dev1__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.
- bff/__init__.py +30 -0
- bff/bayes/__init__.py +11 -0
- bff/bayes/gaussian_process.py +285 -0
- bff/bayes/kernels.py +71 -0
- bff/bayes/learning.py +484 -0
- bff/bayes/likelihoods.py +142 -0
- bff/bayes/posterior.py +62 -0
- bff/bayes/priors.py +179 -0
- bff/bayes/results.py +327 -0
- bff/bayes/utils.py +333 -0
- bff/cli.py +254 -0
- bff/data.py +93 -0
- bff/domain/__init__.py +12 -0
- bff/domain/bias.py +149 -0
- bff/domain/specs.py +347 -0
- bff/domain/trainset.py +201 -0
- bff/io/__init__.py +12 -0
- bff/io/colvars.py +186 -0
- bff/io/commands.py +20 -0
- bff/io/cp2k.py +330 -0
- bff/io/logs.py +213 -0
- bff/io/mdp.py +98 -0
- bff/io/plumed.py +184 -0
- bff/io/progress.py +74 -0
- bff/io/schedulers.py +118 -0
- bff/io/utils.py +208 -0
- bff/mcmc/__init__.py +3 -0
- bff/mcmc/convergence.py +168 -0
- bff/mcmc/proposal.py +254 -0
- bff/mcmc/sampler.py +481 -0
- bff/plotting.py +362 -0
- bff/qoi/__init__.py +9 -0
- bff/qoi/analysis.py +209 -0
- bff/qoi/data.py +169 -0
- bff/qoi/hbonds.py +255 -0
- bff/qoi/rdf.py +108 -0
- bff/qoi/routines.py +272 -0
- bff/tools.py +168 -0
- bff/topology.py +581 -0
- bff/workflows/__init__.py +0 -0
- bff/workflows/configs.py +1147 -0
- bff/workflows/learn.py +77 -0
- bff/workflows/md.py +326 -0
- bff/workflows/prepare.py +788 -0
- bff/workflows/qoi.py +260 -0
- bff/workflows/runsims.py +620 -0
- bff/workflows/simulate.py +28 -0
- bff/workflows/train.py +69 -0
- bff/workflows/validate.py +29 -0
- bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
- bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
- bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
- bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
- bfflearn-0.1.0.dev1.dist-info/top_level.txt +1 -0
bff/qoi/data.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any, Mapping
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
from ..io.utils import load_pt, save_pt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(slots=True)
|
|
10
|
+
class QoI:
|
|
11
|
+
"""One named quantity of interest produced for a single trajectory."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
values: np.ndarray
|
|
15
|
+
labels: tuple[str, ...] | None = None
|
|
16
|
+
values_per_label: int = 1
|
|
17
|
+
settings: dict[str, Any] = field(default_factory=dict)
|
|
18
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
19
|
+
|
|
20
|
+
def __post_init__(self) -> None:
|
|
21
|
+
values = np.asarray(self.values, dtype=float).reshape(-1)
|
|
22
|
+
labels = None if self.labels is None else tuple(self.labels)
|
|
23
|
+
values_per_label = int(self.values_per_label)
|
|
24
|
+
|
|
25
|
+
if values_per_label <= 0:
|
|
26
|
+
raise ValueError("'values_per_label' must be a positive integer.")
|
|
27
|
+
if labels is not None and len(values) != len(labels) * values_per_label:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
"Number of values does not match labels * values_per_label in QoI."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
self.values = values
|
|
33
|
+
self.labels = labels
|
|
34
|
+
self.values_per_label = values_per_label
|
|
35
|
+
self.settings = dict(self.settings)
|
|
36
|
+
self.metadata = dict(self.metadata)
|
|
37
|
+
|
|
38
|
+
@property
|
|
39
|
+
def n_values(self) -> int:
|
|
40
|
+
"""Total number of numeric values stored in the QoI."""
|
|
41
|
+
return int(self.values.size)
|
|
42
|
+
|
|
43
|
+
def to_dict(self) -> dict[str, Any]:
|
|
44
|
+
"""Serialize the QoI to a JSON/PT-friendly mapping."""
|
|
45
|
+
return {
|
|
46
|
+
"name": self.name,
|
|
47
|
+
"values": self.values.tolist(),
|
|
48
|
+
"labels": None if self.labels is None else list(self.labels),
|
|
49
|
+
"values_per_label": self.values_per_label,
|
|
50
|
+
"settings": dict(self.settings),
|
|
51
|
+
"metadata": dict(self.metadata),
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@classmethod
|
|
55
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "QoI":
|
|
56
|
+
"""Rebuild a serialized QoI object."""
|
|
57
|
+
return cls(
|
|
58
|
+
name=str(data["name"]),
|
|
59
|
+
values=np.asarray(data["values"], dtype=float),
|
|
60
|
+
labels=None if data.get("labels") is None else tuple(data["labels"]),
|
|
61
|
+
values_per_label=int(data.get("values_per_label", 1)),
|
|
62
|
+
settings=dict(data.get("settings", {})),
|
|
63
|
+
metadata=dict(data.get("metadata", {})),
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass(slots=True)
|
|
68
|
+
class QoIDataset:
|
|
69
|
+
"""Training-ready dataset for one named quantity of interest."""
|
|
70
|
+
|
|
71
|
+
name: str
|
|
72
|
+
inputs: np.ndarray
|
|
73
|
+
outputs: np.ndarray
|
|
74
|
+
outputs_ref: np.ndarray
|
|
75
|
+
labels: tuple[str, ...] | None = None
|
|
76
|
+
values_per_label: int = 1
|
|
77
|
+
nuisance: float | None = None
|
|
78
|
+
settings: dict[str, Any] | None = None
|
|
79
|
+
metadata: dict[str, Any] | None = None
|
|
80
|
+
|
|
81
|
+
def __post_init__(self) -> None:
|
|
82
|
+
self.inputs = np.asarray(self.inputs, dtype=float)
|
|
83
|
+
self.outputs = np.asarray(self.outputs, dtype=float)
|
|
84
|
+
self.outputs_ref = np.asarray(self.outputs_ref, dtype=float).reshape(-1)
|
|
85
|
+
self.settings = self._coerce_mapping(self.settings)
|
|
86
|
+
self.metadata = self._coerce_mapping(self.metadata)
|
|
87
|
+
self.labels = None if self.labels is None else tuple(self.labels)
|
|
88
|
+
self.values_per_label = int(self.values_per_label)
|
|
89
|
+
|
|
90
|
+
if self.inputs.shape[0] != self.outputs.shape[0]:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Number of input samples ({self.inputs.shape[0]}) does not match "
|
|
93
|
+
f"number of output samples ({self.outputs.shape[0]})."
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if self.outputs.shape[1] != self.outputs_ref.shape[0]:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"Output dimension ({self.outputs.shape[1]}) does not match "
|
|
99
|
+
f"reference dimension ({self.outputs_ref.shape[0]})."
|
|
100
|
+
)
|
|
101
|
+
if self.values_per_label <= 0:
|
|
102
|
+
raise ValueError("'values_per_label' must be a positive integer.")
|
|
103
|
+
if (
|
|
104
|
+
self.labels is not None
|
|
105
|
+
and self.outputs_ref.size != len(self.labels) * self.values_per_label
|
|
106
|
+
):
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Reference output size does not match labels * values_per_label."
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def _coerce_mapping(value: Any) -> dict[str, Any]:
|
|
113
|
+
return dict(value or {})
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def n_samples(self) -> int:
|
|
117
|
+
return int(self.inputs.shape[0])
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def n_observations(self) -> int:
|
|
121
|
+
if self.outputs_ref.size % self.values_per_label != 0:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Reference output size must be divisible by "
|
|
124
|
+
"'values_per_label'."
|
|
125
|
+
)
|
|
126
|
+
return int(self.outputs_ref.size // self.values_per_label)
|
|
127
|
+
|
|
128
|
+
def to_dict(self) -> dict[str, Any]:
|
|
129
|
+
data = {
|
|
130
|
+
"name": self.name,
|
|
131
|
+
"inputs": self.inputs.tolist(),
|
|
132
|
+
"outputs": self.outputs.tolist(),
|
|
133
|
+
"outputs_ref": self.outputs_ref.tolist(),
|
|
134
|
+
"labels": None if self.labels is None else list(self.labels),
|
|
135
|
+
"values_per_label": self.values_per_label,
|
|
136
|
+
"settings": dict(self.settings),
|
|
137
|
+
"metadata": dict(self.metadata),
|
|
138
|
+
}
|
|
139
|
+
if self.nuisance is not None:
|
|
140
|
+
data["nuisance"] = self.nuisance
|
|
141
|
+
return data
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def from_dict(cls, data: Mapping[str, Any]) -> "QoIDataset":
|
|
145
|
+
return cls(
|
|
146
|
+
name=str(data["name"]),
|
|
147
|
+
inputs=np.asarray(data["inputs"], dtype=float),
|
|
148
|
+
outputs=np.asarray(data["outputs"], dtype=float),
|
|
149
|
+
outputs_ref=np.asarray(data["outputs_ref"], dtype=float),
|
|
150
|
+
labels=None if data.get("labels") is None else tuple(data["labels"]),
|
|
151
|
+
values_per_label=int(data.get("values_per_label", 1)),
|
|
152
|
+
nuisance=data.get("nuisance"),
|
|
153
|
+
settings=dict(data.get("settings", {})),
|
|
154
|
+
metadata=dict(data.get("metadata", {})),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def write(self, fn_out: str) -> None:
|
|
158
|
+
save_pt(self.to_dict(), fn_out)
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def load(cls, fn_in: str) -> "QoIDataset":
|
|
162
|
+
data = load_pt(fn_in)
|
|
163
|
+
return cls.from_dict(data)
|
|
164
|
+
|
|
165
|
+
def __repr__(self) -> str:
|
|
166
|
+
return (
|
|
167
|
+
f"QoIDataset(n_samples={self.n_samples}, "
|
|
168
|
+
f"n_observations={self.n_observations}, name={self.name!r})"
|
|
169
|
+
)
|
bff/qoi/hbonds.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
import MDAnalysis as mda
|
|
4
|
+
import numpy as np
|
|
5
|
+
from MDAnalysis.lib.distances import calc_angles, capped_distance
|
|
6
|
+
|
|
7
|
+
from ..tools import get_unitcell
|
|
8
|
+
from .data import QoI
|
|
9
|
+
|
|
10
|
+
__all__ = ["compute_all_hbonds"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def count_hbonds(
|
|
14
|
+
universe: mda.Universe,
|
|
15
|
+
donor_indices: np.ndarray,
|
|
16
|
+
acceptor_indices: np.ndarray,
|
|
17
|
+
n_frames: int
|
|
18
|
+
) -> Dict[str, float]:
|
|
19
|
+
"""
|
|
20
|
+
Counts the average number of hydrogen bonds per hydrogen bond type.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
universe : MDAnalysis.Universe
|
|
25
|
+
donor_indices : np.ndarray
|
|
26
|
+
acceptor_indices : np.ndarray
|
|
27
|
+
n_frames : int
|
|
28
|
+
Number of trajectory frames used for the analysis.
|
|
29
|
+
threshold : float, optional
|
|
30
|
+
Minimum frequency threshold for a hydrogen bond type. Default is 0.1.
|
|
31
|
+
|
|
32
|
+
Returns
|
|
33
|
+
-------
|
|
34
|
+
results : dict
|
|
35
|
+
Dictionary containing the hydrogen bond types and their frequencies.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
if n_frames <= 0 or donor_indices.size == 0 or acceptor_indices.size == 0:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
hbonds = np.column_stack(
|
|
42
|
+
(
|
|
43
|
+
universe.atoms[donor_indices].resnames,
|
|
44
|
+
universe.atoms[donor_indices].types,
|
|
45
|
+
universe.atoms[acceptor_indices].resnames,
|
|
46
|
+
universe.atoms[acceptor_indices].types,
|
|
47
|
+
)
|
|
48
|
+
).astype("str")
|
|
49
|
+
|
|
50
|
+
hbond_types, counts = np.unique(hbonds, return_counts=True, axis=0)
|
|
51
|
+
|
|
52
|
+
# Filter and format results
|
|
53
|
+
results = {}
|
|
54
|
+
for hb, count in zip(hbond_types, counts):
|
|
55
|
+
name = f"{hb[0]}({hb[1]}) to {hb[2]}({hb[3]})"
|
|
56
|
+
results[name] = count / n_frames
|
|
57
|
+
|
|
58
|
+
return results
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _paired_donors_and_hydrogens(
|
|
62
|
+
universe: mda.Universe,
|
|
63
|
+
selection: str,
|
|
64
|
+
*,
|
|
65
|
+
hb_elements: set[str],
|
|
66
|
+
) -> tuple[mda.AtomGroup, mda.AtomGroup]:
|
|
67
|
+
"""Return donor and hydrogen groups with one-to-one indexing."""
|
|
68
|
+
hydrogens_all = universe.select_atoms(f"{selection} and element H")
|
|
69
|
+
donor_indices: list[int] = []
|
|
70
|
+
hydrogen_indices: list[int] = []
|
|
71
|
+
|
|
72
|
+
for hydrogen in hydrogens_all:
|
|
73
|
+
if not hydrogen.bonded_atoms:
|
|
74
|
+
continue
|
|
75
|
+
donor = hydrogen.bonded_atoms[0]
|
|
76
|
+
if donor.element not in hb_elements:
|
|
77
|
+
continue
|
|
78
|
+
donor_indices.append(donor.index)
|
|
79
|
+
hydrogen_indices.append(hydrogen.index)
|
|
80
|
+
|
|
81
|
+
if not donor_indices:
|
|
82
|
+
empty = universe.atoms[np.asarray([], dtype=int)]
|
|
83
|
+
return empty, empty
|
|
84
|
+
|
|
85
|
+
donors = universe.atoms[np.asarray(donor_indices, dtype=int)]
|
|
86
|
+
hydrogens = universe.atoms[np.asarray(hydrogen_indices, dtype=int)]
|
|
87
|
+
return donors, hydrogens
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _all_possible_hbond_labels(
|
|
91
|
+
donors: mda.AtomGroup,
|
|
92
|
+
acceptors: mda.AtomGroup,
|
|
93
|
+
) -> set[str]:
|
|
94
|
+
"""Enumerate all donor/acceptor type combinations possible for this system."""
|
|
95
|
+
if len(donors) == 0 or len(acceptors) == 0:
|
|
96
|
+
return set()
|
|
97
|
+
|
|
98
|
+
donor_types = np.unique(
|
|
99
|
+
np.column_stack((donors.resnames, donors.types)).astype(str),
|
|
100
|
+
axis=0,
|
|
101
|
+
)
|
|
102
|
+
acceptor_types = np.unique(
|
|
103
|
+
np.column_stack((acceptors.resnames, acceptors.types)).astype(str),
|
|
104
|
+
axis=0,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
f"{d_res}({d_type}) to {a_res}({a_type})"
|
|
109
|
+
for d_res, d_type in donor_types
|
|
110
|
+
for a_res, a_type in acceptor_types
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def compute_hbonds(
|
|
115
|
+
universe: mda.Universe,
|
|
116
|
+
hydrogens: mda.AtomGroup,
|
|
117
|
+
donors: mda.AtomGroup,
|
|
118
|
+
acceptors: mda.AtomGroup,
|
|
119
|
+
distance_cutoff: float = 3.5,
|
|
120
|
+
angle_cutoff: float = 150,
|
|
121
|
+
start: int = 0,
|
|
122
|
+
step: int = 1,
|
|
123
|
+
stop: int = None,
|
|
124
|
+
) -> tuple[np.ndarray, np.ndarray, int]:
|
|
125
|
+
"""
|
|
126
|
+
Computes hydrogen bonds between donor and acceptor atoms.
|
|
127
|
+
|
|
128
|
+
Parameters
|
|
129
|
+
----------
|
|
130
|
+
universe : MDAnalysis.Universe
|
|
131
|
+
hydrogens : MDAnalysis.AtomGroup
|
|
132
|
+
donors : MDAnalysis.AtomGroup
|
|
133
|
+
acceptors : MDAnalysis.AtomGroup
|
|
134
|
+
distance_cutoff : float, optional
|
|
135
|
+
Maximum distance between donor and acceptor atoms. Default is 3.0A.
|
|
136
|
+
angle_cutoff : float, optional
|
|
137
|
+
Minimum angle between donor-hydrogen-acceptor atoms.
|
|
138
|
+
Default is 150 degrees.
|
|
139
|
+
start : int, optional
|
|
140
|
+
Start frame for the analysis. Default is 0.
|
|
141
|
+
step : int, optional
|
|
142
|
+
Frame stride for the analysis. Default is 1.
|
|
143
|
+
stop : int, optional
|
|
144
|
+
End frame for the analysis. Default is None.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
donor_indices : np.ndarray
|
|
149
|
+
acceptor_indices : np.ndarray
|
|
150
|
+
n_frames : int
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
sl = slice(start, stop or len(universe.trajectory), step)
|
|
154
|
+
angle_cutoff = np.deg2rad(angle_cutoff)
|
|
155
|
+
|
|
156
|
+
donor_indices, acceptor_indices = [], []
|
|
157
|
+
n_frames = 0
|
|
158
|
+
for ts in universe.trajectory[sl]:
|
|
159
|
+
n_frames += 1
|
|
160
|
+
box = get_unitcell(universe, ts)
|
|
161
|
+
da_indices = capped_distance(
|
|
162
|
+
donors,
|
|
163
|
+
acceptors,
|
|
164
|
+
max_cutoff=distance_cutoff,
|
|
165
|
+
min_cutoff=1.0,
|
|
166
|
+
box=box,
|
|
167
|
+
return_distances=False,
|
|
168
|
+
)
|
|
169
|
+
if da_indices.size == 0:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
d = donors[da_indices[:, 0]]
|
|
173
|
+
h = hydrogens[da_indices[:, 0]]
|
|
174
|
+
a = acceptors[da_indices[:, 1]]
|
|
175
|
+
|
|
176
|
+
dha_angles = calc_angles(d, h, a, box=box)
|
|
177
|
+
hbond_indices = np.where(dha_angles > angle_cutoff)[0]
|
|
178
|
+
if hbond_indices.size == 0:
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
donor_indices.extend(d[hbond_indices].indices)
|
|
182
|
+
acceptor_indices.extend(a[hbond_indices].indices)
|
|
183
|
+
|
|
184
|
+
return (
|
|
185
|
+
np.asarray(donor_indices, dtype=int),
|
|
186
|
+
np.asarray(acceptor_indices, dtype=int),
|
|
187
|
+
n_frames,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def compute_all_hbonds(
|
|
192
|
+
universe: mda.Universe,
|
|
193
|
+
*,
|
|
194
|
+
mol_resname: str,
|
|
195
|
+
water_resname: str = "SOL",
|
|
196
|
+
distance_cutoff: float = 3.5,
|
|
197
|
+
angle_cutoff: float = 150,
|
|
198
|
+
hb_elements: set[str] = {"O", "N", "S"},
|
|
199
|
+
start: int = 0,
|
|
200
|
+
stop: int | None = None,
|
|
201
|
+
step: int = 1,
|
|
202
|
+
) -> QoI:
|
|
203
|
+
"""Compute all solute-water hydrogen-bond QoIs for one trajectory."""
|
|
204
|
+
selection_1 = f"resname {mol_resname}"
|
|
205
|
+
selection_2 = f"resname {water_resname}"
|
|
206
|
+
|
|
207
|
+
hb_elements = set(hb_elements)
|
|
208
|
+
hb_elements_str = " ".join(sorted(hb_elements))
|
|
209
|
+
|
|
210
|
+
selection_pairs = ((selection_1, selection_2), (selection_2, selection_1))
|
|
211
|
+
possible_labels: set[str] = set()
|
|
212
|
+
hbonds: dict[str, float] = {}
|
|
213
|
+
for sel_donors, sel_acceptors in selection_pairs:
|
|
214
|
+
donors, hydrogens = _paired_donors_and_hydrogens(
|
|
215
|
+
universe,
|
|
216
|
+
sel_donors,
|
|
217
|
+
hb_elements=hb_elements,
|
|
218
|
+
)
|
|
219
|
+
acceptors = universe.select_atoms(
|
|
220
|
+
f"{sel_acceptors} and element {hb_elements_str}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
possible_labels.update(_all_possible_hbond_labels(donors, acceptors))
|
|
224
|
+
|
|
225
|
+
if len(donors) == 0 or len(acceptors) == 0:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
donor_indices, acceptor_indices, n_frames = compute_hbonds(
|
|
229
|
+
universe,
|
|
230
|
+
hydrogens,
|
|
231
|
+
donors,
|
|
232
|
+
acceptors,
|
|
233
|
+
distance_cutoff=distance_cutoff,
|
|
234
|
+
angle_cutoff=angle_cutoff,
|
|
235
|
+
start=start,
|
|
236
|
+
stop=stop,
|
|
237
|
+
step=step,
|
|
238
|
+
)
|
|
239
|
+
hbonds.update(count_hbonds(universe, donor_indices, acceptor_indices, n_frames))
|
|
240
|
+
|
|
241
|
+
labels = tuple(sorted(possible_labels))
|
|
242
|
+
values = np.asarray([hbonds.get(label, 0.0) for label in labels], dtype=float)
|
|
243
|
+
metadata = {
|
|
244
|
+
"mol_resname": mol_resname,
|
|
245
|
+
"water_resname": water_resname,
|
|
246
|
+
"distance_cutoff": float(distance_cutoff),
|
|
247
|
+
"angle_cutoff": float(angle_cutoff),
|
|
248
|
+
}
|
|
249
|
+
return QoI(
|
|
250
|
+
name="hb",
|
|
251
|
+
values=values,
|
|
252
|
+
labels=labels,
|
|
253
|
+
values_per_label=1,
|
|
254
|
+
settings=metadata,
|
|
255
|
+
)
|
bff/qoi/rdf.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
|
|
3
|
+
import MDAnalysis as mda
|
|
4
|
+
import numpy as np
|
|
5
|
+
from scipy.ndimage import gaussian_filter
|
|
6
|
+
|
|
7
|
+
from ..tools import compute_distances, get_unitcell
|
|
8
|
+
from .data import QoI
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def compute_rdf(
|
|
12
|
+
universe: mda.Universe,
|
|
13
|
+
atoms_ref: mda.AtomGroup,
|
|
14
|
+
atoms_sel: mda.AtomGroup,
|
|
15
|
+
r_range: Tuple[float, float] = (0, 10),
|
|
16
|
+
n_bins: int = 200,
|
|
17
|
+
pbc: bool = True,
|
|
18
|
+
start: int = None,
|
|
19
|
+
stop: int = None,
|
|
20
|
+
step: int = None,
|
|
21
|
+
smooth: bool = True,
|
|
22
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
23
|
+
"""Compute the radial distribution function between two atom groups."""
|
|
24
|
+
distances = compute_distances(
|
|
25
|
+
universe,
|
|
26
|
+
atoms_ref,
|
|
27
|
+
atoms_sel,
|
|
28
|
+
start=start,
|
|
29
|
+
stop=stop,
|
|
30
|
+
step=step,
|
|
31
|
+
pbc=pbc,
|
|
32
|
+
)
|
|
33
|
+
g, edges = np.histogram(distances.reshape(-1), range=r_range, bins=n_bins)
|
|
34
|
+
g = g.astype(np.float64)
|
|
35
|
+
r = 0.5 * (edges[1:] + edges[:-1])
|
|
36
|
+
|
|
37
|
+
shell_volumes = 4.0 / 3.0 * np.pi * (edges[1:] ** 3 - edges[:-1] ** 3)
|
|
38
|
+
if pbc:
|
|
39
|
+
volume = float(np.prod(np.asarray(get_unitcell(universe)[:3], dtype=float)))
|
|
40
|
+
norm = distances.size * (1.0 / volume) * shell_volumes
|
|
41
|
+
g /= norm
|
|
42
|
+
else:
|
|
43
|
+
g /= distances.size * shell_volumes
|
|
44
|
+
|
|
45
|
+
if smooth:
|
|
46
|
+
g = gaussian_filter(g, sigma=3)
|
|
47
|
+
|
|
48
|
+
return r, g
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def compute_all_rdfs(
|
|
52
|
+
universe: mda.Universe,
|
|
53
|
+
*,
|
|
54
|
+
mol_resname: str,
|
|
55
|
+
solvent_sel: str = "resname SOL HOH WAT and name O*",
|
|
56
|
+
r_range: Tuple[float, float] = (0, 10),
|
|
57
|
+
n_bins: int = 200,
|
|
58
|
+
pbc: bool = True,
|
|
59
|
+
start: int = 0,
|
|
60
|
+
stop: int | None = None,
|
|
61
|
+
step: int = 1,
|
|
62
|
+
smooth: bool = True,
|
|
63
|
+
) -> QoI:
|
|
64
|
+
"""Compute solvent RDF QoIs around all solute atom types."""
|
|
65
|
+
mol = universe.select_atoms(f"resname {mol_resname}")
|
|
66
|
+
mask = mol.masses > 0.5
|
|
67
|
+
mol_atomtypes = np.unique(mol[mask].types)
|
|
68
|
+
|
|
69
|
+
rdf_results: dict[str, np.ndarray] = {}
|
|
70
|
+
atoms_solvent = universe.select_atoms(solvent_sel)
|
|
71
|
+
for atomtype in mol_atomtypes:
|
|
72
|
+
atoms_reference = universe.select_atoms(f"type {atomtype}")
|
|
73
|
+
r, g = compute_rdf(
|
|
74
|
+
universe,
|
|
75
|
+
atoms_reference,
|
|
76
|
+
atoms_solvent,
|
|
77
|
+
r_range=r_range,
|
|
78
|
+
n_bins=n_bins,
|
|
79
|
+
pbc=pbc,
|
|
80
|
+
start=start,
|
|
81
|
+
stop=stop,
|
|
82
|
+
step=step,
|
|
83
|
+
smooth=smooth,
|
|
84
|
+
)
|
|
85
|
+
rdf_results[atomtype] = np.array([r, g])
|
|
86
|
+
|
|
87
|
+
atomtypes = tuple(sorted(rdf_results))
|
|
88
|
+
values = np.concatenate(
|
|
89
|
+
[
|
|
90
|
+
np.asarray(rdf_results[atomtype][1], dtype=float).reshape(-1)
|
|
91
|
+
for atomtype in atomtypes
|
|
92
|
+
]
|
|
93
|
+
)
|
|
94
|
+
metadata = {
|
|
95
|
+
"mol_resname": mol_resname,
|
|
96
|
+
"solvent_sel": solvent_sel,
|
|
97
|
+
"r_range": tuple(r_range),
|
|
98
|
+
"n_bins": int(n_bins),
|
|
99
|
+
"pbc": bool(pbc),
|
|
100
|
+
"smooth": bool(smooth),
|
|
101
|
+
}
|
|
102
|
+
return QoI(
|
|
103
|
+
name="rdf",
|
|
104
|
+
values=values,
|
|
105
|
+
labels=atomtypes,
|
|
106
|
+
values_per_label=int(n_bins),
|
|
107
|
+
settings=metadata,
|
|
108
|
+
)
|