CodeEntropy 1.1.0__py3-none-any.whl → 2.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.
- CodeEntropy/__init__.py +1 -1
- CodeEntropy/config/argparse.py +20 -14
- CodeEntropy/config/runtime.py +3 -3
- CodeEntropy/core/logging.py +2 -3
- CodeEntropy/entropy/configurational.py +5 -185
- CodeEntropy/entropy/graph.py +13 -7
- CodeEntropy/entropy/nodes/aggregate.py +9 -4
- CodeEntropy/entropy/nodes/configurational.py +17 -25
- CodeEntropy/entropy/nodes/orientational.py +84 -0
- CodeEntropy/entropy/nodes/vibrational.py +31 -14
- CodeEntropy/entropy/orientational.py +38 -79
- CodeEntropy/entropy/vibrational.py +36 -4
- CodeEntropy/entropy/water.py +9 -8
- CodeEntropy/entropy/workflow.py +42 -18
- CodeEntropy/levels/axes.py +7 -7
- CodeEntropy/levels/dihedrals.py +138 -98
- CodeEntropy/levels/forces.py +18 -13
- CodeEntropy/levels/frame_dag.py +11 -11
- CodeEntropy/levels/hierarchy.py +7 -8
- CodeEntropy/levels/level_dag.py +22 -18
- CodeEntropy/levels/linalg.py +4 -5
- CodeEntropy/levels/mda.py +8 -9
- CodeEntropy/levels/neighbors.py +251 -0
- CodeEntropy/levels/nodes/accumulators.py +12 -11
- CodeEntropy/levels/nodes/beads.py +12 -11
- CodeEntropy/levels/nodes/conformations.py +28 -13
- CodeEntropy/levels/nodes/covariance.py +45 -45
- CodeEntropy/levels/nodes/detect_levels.py +5 -5
- CodeEntropy/levels/nodes/detect_molecules.py +3 -3
- CodeEntropy/levels/nodes/find_neighbors.py +93 -0
- CodeEntropy/levels/search.py +241 -0
- CodeEntropy/molecules/grouping.py +7 -7
- CodeEntropy/results/reporter.py +37 -38
- {codeentropy-1.1.0.dist-info → codeentropy-2.1.0.dist-info}/METADATA +6 -4
- codeentropy-2.1.0.dist-info/RECORD +48 -0
- codeentropy-1.1.0.dist-info/RECORD +0 -44
- {codeentropy-1.1.0.dist-info → codeentropy-2.1.0.dist-info}/WHEEL +0 -0
- {codeentropy-1.1.0.dist-info → codeentropy-2.1.0.dist-info}/entry_points.txt +0 -0
- {codeentropy-1.1.0.dist-info → codeentropy-2.1.0.dist-info}/licenses/LICENSE +0 -0
CodeEntropy/__init__.py
CHANGED
CodeEntropy/config/argparse.py
CHANGED
|
@@ -25,7 +25,7 @@ import glob
|
|
|
25
25
|
import logging
|
|
26
26
|
import os
|
|
27
27
|
from dataclasses import dataclass
|
|
28
|
-
from typing import Any
|
|
28
|
+
from typing import Any
|
|
29
29
|
|
|
30
30
|
import yaml
|
|
31
31
|
|
|
@@ -48,11 +48,11 @@ class ArgSpec:
|
|
|
48
48
|
help: str
|
|
49
49
|
default: Any = None
|
|
50
50
|
type: Any = None
|
|
51
|
-
action:
|
|
52
|
-
nargs:
|
|
51
|
+
action: str | None = None
|
|
52
|
+
nargs: str | None = None
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
ARG_SPECS:
|
|
55
|
+
ARG_SPECS: dict[str, ArgSpec] = {
|
|
56
56
|
"top_traj_file": ArgSpec(
|
|
57
57
|
type=str,
|
|
58
58
|
nargs="+",
|
|
@@ -145,6 +145,12 @@ ARG_SPECS: Dict[str, ArgSpec] = {
|
|
|
145
145
|
default=True,
|
|
146
146
|
help="Use bonded axes to rotate forces for UA level vibrational entropies",
|
|
147
147
|
),
|
|
148
|
+
"search_type": ArgSpec(
|
|
149
|
+
type=str,
|
|
150
|
+
default="RAD",
|
|
151
|
+
help="Type of neighbor search to use."
|
|
152
|
+
"Default RAD; grid search is also available",
|
|
153
|
+
),
|
|
148
154
|
}
|
|
149
155
|
|
|
150
156
|
|
|
@@ -159,7 +165,7 @@ class ConfigResolver:
|
|
|
159
165
|
- validating trajectory-related numeric parameters
|
|
160
166
|
"""
|
|
161
167
|
|
|
162
|
-
def __init__(self, arg_specs:
|
|
168
|
+
def __init__(self, arg_specs: dict[str, ArgSpec] | None = None) -> None:
|
|
163
169
|
"""Initialize the manager.
|
|
164
170
|
|
|
165
171
|
Args:
|
|
@@ -168,7 +174,7 @@ class ConfigResolver:
|
|
|
168
174
|
"""
|
|
169
175
|
self._arg_specs = dict(arg_specs or ARG_SPECS)
|
|
170
176
|
|
|
171
|
-
def load_config(self, directory_path: str) ->
|
|
177
|
+
def load_config(self, directory_path: str) -> dict[str, Any]:
|
|
172
178
|
"""Load the first YAML config file found in a directory.
|
|
173
179
|
|
|
174
180
|
The current behavior matches your existing workflow:
|
|
@@ -188,7 +194,7 @@ class ConfigResolver:
|
|
|
188
194
|
|
|
189
195
|
config_path = yaml_files[0]
|
|
190
196
|
try:
|
|
191
|
-
with open(config_path,
|
|
197
|
+
with open(config_path, encoding="utf-8") as file:
|
|
192
198
|
config = yaml.safe_load(file) or {"run1": {}}
|
|
193
199
|
logger.info("Loaded configuration from: %s", config_path)
|
|
194
200
|
return config
|
|
@@ -253,7 +259,7 @@ class ConfigResolver:
|
|
|
253
259
|
)
|
|
254
260
|
continue
|
|
255
261
|
|
|
256
|
-
kwargs:
|
|
262
|
+
kwargs: dict[str, Any] = {}
|
|
257
263
|
if spec.type is not None:
|
|
258
264
|
kwargs["type"] = spec.type
|
|
259
265
|
if spec.default is not None:
|
|
@@ -266,7 +272,7 @@ class ConfigResolver:
|
|
|
266
272
|
return parser
|
|
267
273
|
|
|
268
274
|
def resolve(
|
|
269
|
-
self, args: argparse.Namespace, run_config:
|
|
275
|
+
self, args: argparse.Namespace, run_config: dict[str, Any] | None
|
|
270
276
|
) -> argparse.Namespace:
|
|
271
277
|
"""Merge CLI arguments with YAML configuration and adjust logging level.
|
|
272
278
|
|
|
@@ -306,8 +312,8 @@ class ConfigResolver:
|
|
|
306
312
|
|
|
307
313
|
@staticmethod
|
|
308
314
|
def _detect_cli_overrides(
|
|
309
|
-
args_dict:
|
|
310
|
-
) ->
|
|
315
|
+
args_dict: dict[str, Any], default_dict: dict[str, Any]
|
|
316
|
+
) -> set[str]:
|
|
311
317
|
"""Detect which args were explicitly overridden in the CLI.
|
|
312
318
|
|
|
313
319
|
Args:
|
|
@@ -322,8 +328,8 @@ class ConfigResolver:
|
|
|
322
328
|
def _apply_yaml_defaults(
|
|
323
329
|
self,
|
|
324
330
|
args: argparse.Namespace,
|
|
325
|
-
run_config:
|
|
326
|
-
cli_provided:
|
|
331
|
+
run_config: dict[str, Any],
|
|
332
|
+
cli_provided: set[str],
|
|
327
333
|
) -> None:
|
|
328
334
|
"""Apply YAML values onto args for keys not provided by CLI.
|
|
329
335
|
|
|
@@ -336,7 +342,7 @@ class ConfigResolver:
|
|
|
336
342
|
if yaml_value is None or key in cli_provided:
|
|
337
343
|
continue
|
|
338
344
|
if key in self._arg_specs:
|
|
339
|
-
logger.debug("Using YAML value for
|
|
345
|
+
logger.debug(f"Using YAML value for {key}: {yaml_value}")
|
|
340
346
|
setattr(args, key, yaml_value)
|
|
341
347
|
|
|
342
348
|
def _ensure_defaults(self, args: argparse.Namespace) -> None:
|
CodeEntropy/config/runtime.py
CHANGED
|
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|
|
18
18
|
import logging
|
|
19
19
|
import os
|
|
20
20
|
import pickle
|
|
21
|
-
from typing import Any
|
|
21
|
+
from typing import Any
|
|
22
22
|
|
|
23
23
|
import MDAnalysis as mda
|
|
24
24
|
import requests
|
|
@@ -112,7 +112,7 @@ class CodeEntropyRunner:
|
|
|
112
112
|
|
|
113
113
|
return new_folder_path
|
|
114
114
|
|
|
115
|
-
def load_citation_data(self) ->
|
|
115
|
+
def load_citation_data(self) -> dict[str, Any] | None:
|
|
116
116
|
"""Load CITATION.cff from GitHub.
|
|
117
117
|
|
|
118
118
|
If the request fails (offline, blocked, etc.), returns None.
|
|
@@ -335,7 +335,7 @@ class CodeEntropyRunner:
|
|
|
335
335
|
kcal_units = args.kcal_force_units
|
|
336
336
|
|
|
337
337
|
if forcefile is None:
|
|
338
|
-
logger.debug("Loading Universe with
|
|
338
|
+
logger.debug(f"Loading Universe with {tprfile} and {trrfile}")
|
|
339
339
|
return mda.Universe(tprfile, trrfile, format=fileformat)
|
|
340
340
|
|
|
341
341
|
return universe_operations.merge_forces(
|
CodeEntropy/core/logging.py
CHANGED
|
@@ -16,7 +16,6 @@ from __future__ import annotations
|
|
|
16
16
|
|
|
17
17
|
import logging
|
|
18
18
|
import os
|
|
19
|
-
from typing import Dict, Optional
|
|
20
19
|
|
|
21
20
|
from rich.console import Console
|
|
22
21
|
from rich.logging import RichHandler
|
|
@@ -55,7 +54,7 @@ class LoggingConfig:
|
|
|
55
54
|
handlers: Mapping of handler name to handler instance.
|
|
56
55
|
"""
|
|
57
56
|
|
|
58
|
-
_console:
|
|
57
|
+
_console: Console | None = None
|
|
59
58
|
|
|
60
59
|
@classmethod
|
|
61
60
|
def get_console(cls) -> Console:
|
|
@@ -80,7 +79,7 @@ class LoggingConfig:
|
|
|
80
79
|
|
|
81
80
|
self.level = level
|
|
82
81
|
self.console = self.get_console()
|
|
83
|
-
self.handlers:
|
|
82
|
+
self.handlers: dict[str, logging.Handler] = {}
|
|
84
83
|
|
|
85
84
|
self._setup_handlers()
|
|
86
85
|
|
|
@@ -10,46 +10,15 @@ This module provides:
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import logging
|
|
13
|
-
from
|
|
14
|
-
from typing import Any, Optional
|
|
13
|
+
from typing import Any
|
|
15
14
|
|
|
16
15
|
import numpy as np
|
|
17
16
|
|
|
18
17
|
logger = logging.getLogger(__name__)
|
|
19
18
|
|
|
20
19
|
|
|
21
|
-
@dataclass(frozen=True)
|
|
22
|
-
class ConformationConfig:
|
|
23
|
-
"""Configuration for assigning conformational states from a dihedral.
|
|
24
|
-
|
|
25
|
-
Attributes:
|
|
26
|
-
bin_width: Histogram bin width in degrees for peak detection.
|
|
27
|
-
start: Inclusive start frame index for trajectory slicing.
|
|
28
|
-
end: Exclusive end frame index for trajectory slicing.
|
|
29
|
-
step: Stride for trajectory slicing (must be positive).
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
bin_width: int
|
|
33
|
-
start: int
|
|
34
|
-
end: int
|
|
35
|
-
step: int
|
|
36
|
-
|
|
37
|
-
|
|
38
20
|
class ConformationalEntropy:
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
This class contains two independent responsibilities:
|
|
42
|
-
1) `assign_conformation`: Map a single dihedral angle time series to discrete
|
|
43
|
-
state labels by detecting histogram peaks and assigning the nearest peak.
|
|
44
|
-
2) `conformational_entropy_calculation`: Compute Shannon entropy of the
|
|
45
|
-
state distribution (in J/mol/K).
|
|
46
|
-
|
|
47
|
-
Notes:
|
|
48
|
-
`number_frames` is accepted by `conformational_entropy_calculation` for
|
|
49
|
-
compatibility with calling sites that track frame counts, but the entropy
|
|
50
|
-
is computed from the observed state counts (i.e., `len(states)`), which is
|
|
51
|
-
the correct normalization for the sampled distribution.
|
|
52
|
-
"""
|
|
21
|
+
"""Compute conformational entropy from states information."""
|
|
53
22
|
|
|
54
23
|
_GAS_CONST: float = 8.3144598484848
|
|
55
24
|
|
|
@@ -61,69 +30,7 @@ class ConformationalEntropy:
|
|
|
61
30
|
"""
|
|
62
31
|
pass
|
|
63
32
|
|
|
64
|
-
def
|
|
65
|
-
self,
|
|
66
|
-
data_container: Any,
|
|
67
|
-
dihedral: Any,
|
|
68
|
-
number_frames: int,
|
|
69
|
-
bin_width: int,
|
|
70
|
-
start: int,
|
|
71
|
-
end: int,
|
|
72
|
-
step: int,
|
|
73
|
-
) -> np.ndarray:
|
|
74
|
-
"""Assign discrete conformational states for a single dihedral.
|
|
75
|
-
|
|
76
|
-
The dihedral angle time series is:
|
|
77
|
-
1) Collected across the trajectory slice [start:end:step].
|
|
78
|
-
2) Converted to [0, 360) degrees.
|
|
79
|
-
3) Histogrammed using `bin_width`.
|
|
80
|
-
4) Peaks are identified as bins with locally maximal population.
|
|
81
|
-
5) Each frame is assigned the index of the nearest peak.
|
|
82
|
-
|
|
83
|
-
Args:
|
|
84
|
-
data_container: MDAnalysis Universe/AtomGroup with a trajectory.
|
|
85
|
-
dihedral: Object providing `value()` for the current frame dihedral.
|
|
86
|
-
number_frames: Provided for call-site compatibility; not used for sizing.
|
|
87
|
-
bin_width: Histogram bin width in degrees.
|
|
88
|
-
start: Inclusive start frame index.
|
|
89
|
-
end: Exclusive end frame index.
|
|
90
|
-
step: Stride for trajectory slicing.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
Array of integer state labels of length equal to the trajectory slice.
|
|
94
|
-
Returns an empty array if the slice is empty.
|
|
95
|
-
|
|
96
|
-
Raises:
|
|
97
|
-
ValueError: If `bin_width` or `step` are invalid.
|
|
98
|
-
"""
|
|
99
|
-
_ = number_frames
|
|
100
|
-
|
|
101
|
-
config = ConformationConfig(
|
|
102
|
-
bin_width=int(bin_width),
|
|
103
|
-
start=int(start),
|
|
104
|
-
end=int(end),
|
|
105
|
-
step=int(step),
|
|
106
|
-
)
|
|
107
|
-
self._validate_assignment_config(config)
|
|
108
|
-
|
|
109
|
-
traj_slice = data_container.trajectory[config.start : config.end : config.step]
|
|
110
|
-
n_slice = len(traj_slice)
|
|
111
|
-
if n_slice <= 0:
|
|
112
|
-
return np.array([], dtype=int)
|
|
113
|
-
|
|
114
|
-
phi = self._collect_dihedral_angles(traj_slice, dihedral)
|
|
115
|
-
peak_values = self._find_histogram_peaks(phi, config.bin_width)
|
|
116
|
-
|
|
117
|
-
if peak_values.size == 0:
|
|
118
|
-
return np.zeros(n_slice, dtype=int)
|
|
119
|
-
|
|
120
|
-
states = self._assign_nearest_peaks(phi, peak_values)
|
|
121
|
-
logger.debug("Final conformations: %s", states)
|
|
122
|
-
return states
|
|
123
|
-
|
|
124
|
-
def conformational_entropy_calculation(
|
|
125
|
-
self, states: Any, number_frames: int
|
|
126
|
-
) -> float:
|
|
33
|
+
def conformational_entropy_calculation(self, states: Any) -> float:
|
|
127
34
|
"""Compute conformational entropy for a sequence of state labels.
|
|
128
35
|
|
|
129
36
|
Entropy is computed as:
|
|
@@ -139,8 +46,6 @@ class ConformationalEntropy:
|
|
|
139
46
|
Returns:
|
|
140
47
|
float: Conformational entropy in J/mol/K.
|
|
141
48
|
"""
|
|
142
|
-
_ = number_frames
|
|
143
|
-
|
|
144
49
|
arr = self._to_1d_array(states)
|
|
145
50
|
if arr is None or arr.size == 0:
|
|
146
51
|
return 0.0
|
|
@@ -154,96 +59,11 @@ class ConformationalEntropy:
|
|
|
154
59
|
probs = probs[probs > 0.0]
|
|
155
60
|
|
|
156
61
|
s_conf = -self._GAS_CONST * float(np.sum(probs * np.log(probs)))
|
|
157
|
-
logger.debug("Total conformational entropy:
|
|
62
|
+
logger.debug(f"Total conformational entropy: {s_conf}")
|
|
158
63
|
return s_conf
|
|
159
64
|
|
|
160
65
|
@staticmethod
|
|
161
|
-
def
|
|
162
|
-
"""Validate conformation assignment configuration.
|
|
163
|
-
|
|
164
|
-
Args:
|
|
165
|
-
config: Assignment configuration.
|
|
166
|
-
|
|
167
|
-
Raises:
|
|
168
|
-
ValueError: If configuration values are invalid.
|
|
169
|
-
"""
|
|
170
|
-
if config.step <= 0:
|
|
171
|
-
raise ValueError("step must be a positive integer")
|
|
172
|
-
if config.bin_width <= 0 or config.bin_width > 360:
|
|
173
|
-
raise ValueError("bin_width must be in the range (0, 360]")
|
|
174
|
-
if 360 % config.bin_width != 0:
|
|
175
|
-
logger.warning(
|
|
176
|
-
"bin_width=%s does not evenly divide 360; histogram bins will be "
|
|
177
|
-
"uneven.",
|
|
178
|
-
config.bin_width,
|
|
179
|
-
)
|
|
180
|
-
|
|
181
|
-
@staticmethod
|
|
182
|
-
def _collect_dihedral_angles(traj_slice: Any, dihedral: Any) -> np.ndarray:
|
|
183
|
-
"""Collect dihedral angles for each frame in the trajectory slice.
|
|
184
|
-
|
|
185
|
-
Args:
|
|
186
|
-
traj_slice: Slice of a trajectory iterable where iterating advances frames.
|
|
187
|
-
dihedral: Object with `value()` returning the dihedral in degrees.
|
|
188
|
-
|
|
189
|
-
Returns:
|
|
190
|
-
Array of dihedral values mapped into [0, 360).
|
|
191
|
-
"""
|
|
192
|
-
phi = np.zeros(len(traj_slice), dtype=float)
|
|
193
|
-
for i, _ts in enumerate(traj_slice):
|
|
194
|
-
value = float(dihedral.value())
|
|
195
|
-
if value < 0.0:
|
|
196
|
-
value += 360.0
|
|
197
|
-
phi[i] = value
|
|
198
|
-
return phi
|
|
199
|
-
|
|
200
|
-
@staticmethod
|
|
201
|
-
def _find_histogram_peaks(phi: np.ndarray, bin_width: int) -> np.ndarray:
|
|
202
|
-
"""Identify peak bin centers from a histogram of dihedral angles.
|
|
203
|
-
|
|
204
|
-
A peak is defined as a bin whose population is greater than or equal to
|
|
205
|
-
its immediate neighbors (with circular handling at the final bin).
|
|
206
|
-
|
|
207
|
-
Args:
|
|
208
|
-
phi: Dihedral angles in degrees, in [0, 360).
|
|
209
|
-
bin_width: Histogram bin width in degrees.
|
|
210
|
-
|
|
211
|
-
Returns:
|
|
212
|
-
1D array of peak bin center values (degrees). Empty if no peaks found.
|
|
213
|
-
"""
|
|
214
|
-
number_bins = int(360 / bin_width)
|
|
215
|
-
popul, bin_edges = np.histogram(phi, bins=number_bins, range=(0.0, 360.0))
|
|
216
|
-
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
|
|
217
|
-
|
|
218
|
-
peaks: list[float] = []
|
|
219
|
-
for idx in range(number_bins):
|
|
220
|
-
if popul[idx] == 0:
|
|
221
|
-
continue
|
|
222
|
-
|
|
223
|
-
left = popul[idx - 1] if idx > 0 else popul[number_bins - 1]
|
|
224
|
-
right = popul[idx + 1] if idx < number_bins - 1 else popul[0]
|
|
225
|
-
|
|
226
|
-
if popul[idx] >= left and popul[idx] >= right:
|
|
227
|
-
peaks.append(float(bin_centers[idx]))
|
|
228
|
-
|
|
229
|
-
return np.asarray(peaks, dtype=float)
|
|
230
|
-
|
|
231
|
-
@staticmethod
|
|
232
|
-
def _assign_nearest_peaks(phi: np.ndarray, peak_values: np.ndarray) -> np.ndarray:
|
|
233
|
-
"""Assign each phi value to the index of its nearest peak.
|
|
234
|
-
|
|
235
|
-
Args:
|
|
236
|
-
phi: Dihedral angles in degrees.
|
|
237
|
-
peak_values: Peak centers (degrees).
|
|
238
|
-
|
|
239
|
-
Returns:
|
|
240
|
-
Integer state labels aligned with `phi`.
|
|
241
|
-
"""
|
|
242
|
-
distances = np.abs(phi[:, None] - peak_values[None, :])
|
|
243
|
-
return np.argmin(distances, axis=1).astype(int)
|
|
244
|
-
|
|
245
|
-
@staticmethod
|
|
246
|
-
def _to_1d_array(states: Any) -> Optional[np.ndarray]:
|
|
66
|
+
def _to_1d_array(states: Any) -> np.ndarray | None:
|
|
247
67
|
"""Convert a state sequence into a 1D numpy array.
|
|
248
68
|
|
|
249
69
|
Args:
|
CodeEntropy/entropy/graph.py
CHANGED
|
@@ -15,18 +15,19 @@ from __future__ import annotations
|
|
|
15
15
|
|
|
16
16
|
import logging
|
|
17
17
|
from dataclasses import dataclass
|
|
18
|
-
from typing import Any
|
|
18
|
+
from typing import Any
|
|
19
19
|
|
|
20
20
|
import networkx as nx
|
|
21
21
|
|
|
22
22
|
from CodeEntropy.entropy.nodes.aggregate import AggregateEntropyNode
|
|
23
23
|
from CodeEntropy.entropy.nodes.configurational import ConfigurationalEntropyNode
|
|
24
|
+
from CodeEntropy.entropy.nodes.orientational import OrientationalEntropyNode
|
|
24
25
|
from CodeEntropy.entropy.nodes.vibrational import VibrationalEntropyNode
|
|
25
26
|
|
|
26
27
|
logger = logging.getLogger(__name__)
|
|
27
28
|
|
|
28
29
|
|
|
29
|
-
SharedData =
|
|
30
|
+
SharedData = dict[str, Any]
|
|
30
31
|
|
|
31
32
|
|
|
32
33
|
@dataclass(frozen=True)
|
|
@@ -57,9 +58,9 @@ class EntropyGraph:
|
|
|
57
58
|
def __init__(self) -> None:
|
|
58
59
|
"""Initialize an empty entropy graph."""
|
|
59
60
|
self._graph: nx.DiGraph = nx.DiGraph()
|
|
60
|
-
self._nodes:
|
|
61
|
+
self._nodes: dict[str, Any] = {}
|
|
61
62
|
|
|
62
|
-
def build(self) ->
|
|
63
|
+
def build(self) -> EntropyGraph:
|
|
63
64
|
"""Populate the graph with the standard entropy workflow.
|
|
64
65
|
|
|
65
66
|
Returns:
|
|
@@ -68,10 +69,15 @@ class EntropyGraph:
|
|
|
68
69
|
specs = (
|
|
69
70
|
NodeSpec("vibrational_entropy", VibrationalEntropyNode()),
|
|
70
71
|
NodeSpec("configurational_entropy", ConfigurationalEntropyNode()),
|
|
72
|
+
NodeSpec("orientational_entropy", OrientationalEntropyNode()),
|
|
71
73
|
NodeSpec(
|
|
72
74
|
"aggregate_entropy",
|
|
73
75
|
AggregateEntropyNode(),
|
|
74
|
-
deps=(
|
|
76
|
+
deps=(
|
|
77
|
+
"vibrational_entropy",
|
|
78
|
+
"configurational_entropy",
|
|
79
|
+
"orientational_entropy",
|
|
80
|
+
),
|
|
75
81
|
),
|
|
76
82
|
)
|
|
77
83
|
|
|
@@ -82,7 +88,7 @@ class EntropyGraph:
|
|
|
82
88
|
|
|
83
89
|
def execute(
|
|
84
90
|
self, shared_data: SharedData, *, progress: object | None = None
|
|
85
|
-
) ->
|
|
91
|
+
) -> dict[str, Any]:
|
|
86
92
|
"""Execute the entropy graph in topological order.
|
|
87
93
|
|
|
88
94
|
Nodes are executed in dependency order (topological sort). Each node reads
|
|
@@ -105,7 +111,7 @@ class EntropyGraph:
|
|
|
105
111
|
Raises:
|
|
106
112
|
KeyError: If a node name is missing from the internal node registry.
|
|
107
113
|
"""
|
|
108
|
-
results:
|
|
114
|
+
results: dict[str, Any] = {}
|
|
109
115
|
|
|
110
116
|
for node_name in nx.topological_sort(self._graph):
|
|
111
117
|
node = self._nodes[node_name]
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
from collections.abc import Mapping, MutableMapping
|
|
5
6
|
from dataclasses import dataclass
|
|
6
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
|
-
EntropyResults =
|
|
9
|
+
EntropyResults = dict[str, Any]
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@dataclass(frozen=True, slots=True)
|
|
@@ -25,11 +26,12 @@ class AggregateEntropyNode:
|
|
|
25
26
|
|
|
26
27
|
vibrational_key: str = "vibrational_entropy"
|
|
27
28
|
configurational_key: str = "configurational_entropy"
|
|
29
|
+
orientational_key: str = "orientational_entropy"
|
|
28
30
|
output_key: str = "entropy_results"
|
|
29
31
|
|
|
30
32
|
def run(
|
|
31
33
|
self, shared_data: MutableMapping[str, Any], **_: Any
|
|
32
|
-
) ->
|
|
34
|
+
) -> dict[str, EntropyResults]:
|
|
33
35
|
"""Run the aggregation step.
|
|
34
36
|
|
|
35
37
|
Args:
|
|
@@ -69,10 +71,13 @@ class AggregateEntropyNode:
|
|
|
69
71
|
"configurational_entropy": self._get_optional(
|
|
70
72
|
shared_data, self.configurational_key
|
|
71
73
|
),
|
|
74
|
+
"orientational_entropy": self._get_optional(
|
|
75
|
+
shared_data, self.orientational_key
|
|
76
|
+
),
|
|
72
77
|
}
|
|
73
78
|
|
|
74
79
|
@staticmethod
|
|
75
|
-
def _get_optional(shared_data: Mapping[str, Any], key: str) ->
|
|
80
|
+
def _get_optional(shared_data: Mapping[str, Any], key: str) -> Any | None:
|
|
76
81
|
"""Fetch an optional value from shared data.
|
|
77
82
|
|
|
78
83
|
Args:
|
|
@@ -3,16 +3,9 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
|
+
from collections.abc import Iterable, Mapping, MutableMapping, Sequence
|
|
6
7
|
from typing import (
|
|
7
8
|
Any,
|
|
8
|
-
Dict,
|
|
9
|
-
Iterable,
|
|
10
|
-
Mapping,
|
|
11
|
-
MutableMapping,
|
|
12
|
-
Optional,
|
|
13
|
-
Sequence,
|
|
14
|
-
Tuple,
|
|
15
|
-
Union,
|
|
16
9
|
)
|
|
17
10
|
|
|
18
11
|
import numpy as np
|
|
@@ -23,8 +16,8 @@ logger = logging.getLogger(__name__)
|
|
|
23
16
|
|
|
24
17
|
GroupId = int
|
|
25
18
|
ResidueId = int
|
|
26
|
-
StateKey =
|
|
27
|
-
StateSequence =
|
|
19
|
+
StateKey = tuple[GroupId, ResidueId]
|
|
20
|
+
StateSequence = Sequence[Any] | np.ndarray
|
|
28
21
|
|
|
29
22
|
|
|
30
23
|
class ConfigurationalEntropyNode:
|
|
@@ -36,7 +29,7 @@ class ConfigurationalEntropyNode:
|
|
|
36
29
|
Results are written back into ``shared_data["configurational_entropy"]``.
|
|
37
30
|
"""
|
|
38
31
|
|
|
39
|
-
def run(self, shared_data: MutableMapping[str, Any], **_: Any) ->
|
|
32
|
+
def run(self, shared_data: MutableMapping[str, Any], **_: Any) -> dict[str, Any]:
|
|
40
33
|
"""Execute configurational entropy calculation.
|
|
41
34
|
|
|
42
35
|
Args:
|
|
@@ -58,7 +51,7 @@ class ConfigurationalEntropyNode:
|
|
|
58
51
|
ce = self._build_entropy_engine()
|
|
59
52
|
|
|
60
53
|
fragments = universe.atoms.fragments
|
|
61
|
-
results:
|
|
54
|
+
results: dict[int, dict[str, float]] = {}
|
|
62
55
|
|
|
63
56
|
for group_id, mol_ids in groups.items():
|
|
64
57
|
results[group_id] = {"ua": 0.0, "res": 0.0, "poly": 0.0}
|
|
@@ -104,9 +97,9 @@ class ConfigurationalEntropyNode:
|
|
|
104
97
|
|
|
105
98
|
def _get_state_containers(
|
|
106
99
|
self, shared_data: Mapping[str, Any]
|
|
107
|
-
) ->
|
|
108
|
-
|
|
109
|
-
|
|
100
|
+
) -> tuple[
|
|
101
|
+
dict[StateKey, StateSequence],
|
|
102
|
+
dict[GroupId, StateSequence] | Sequence[StateSequence | None],
|
|
110
103
|
]:
|
|
111
104
|
"""Retrieve conformational state containers.
|
|
112
105
|
|
|
@@ -144,7 +137,7 @@ class ConfigurationalEntropyNode:
|
|
|
144
137
|
residues: Iterable[Any],
|
|
145
138
|
states_ua: Mapping[StateKey, StateSequence],
|
|
146
139
|
n_frames: int,
|
|
147
|
-
reporter:
|
|
140
|
+
reporter: Any | None,
|
|
148
141
|
) -> float:
|
|
149
142
|
"""Compute united atom entropy for a group.
|
|
150
143
|
|
|
@@ -163,7 +156,7 @@ class ConfigurationalEntropyNode:
|
|
|
163
156
|
|
|
164
157
|
for res_id, res in enumerate(residues):
|
|
165
158
|
states = states_ua.get((group_id, res_id))
|
|
166
|
-
val = self._entropy_or_zero(ce, states
|
|
159
|
+
val = self._entropy_or_zero(ce, states)
|
|
167
160
|
total += val
|
|
168
161
|
|
|
169
162
|
if reporter is not None:
|
|
@@ -186,29 +179,28 @@ class ConfigurationalEntropyNode:
|
|
|
186
179
|
*,
|
|
187
180
|
ce: ConformationalEntropy,
|
|
188
181
|
group_id: int,
|
|
189
|
-
states_res:
|
|
182
|
+
states_res: dict[int, StateSequence] | Sequence[StateSequence | None],
|
|
190
183
|
n_frames: int,
|
|
191
184
|
) -> float:
|
|
192
185
|
"""Compute residue-level entropy for a group."""
|
|
193
186
|
group_states = self._get_group_states(states_res, group_id)
|
|
194
|
-
return self._entropy_or_zero(ce, group_states
|
|
187
|
+
return self._entropy_or_zero(ce, group_states)
|
|
195
188
|
|
|
196
189
|
def _entropy_or_zero(
|
|
197
190
|
self,
|
|
198
191
|
ce: ConformationalEntropy,
|
|
199
|
-
states:
|
|
200
|
-
n_frames: int,
|
|
192
|
+
states: StateSequence | None,
|
|
201
193
|
) -> float:
|
|
202
194
|
"""Return entropy value or zero if no state data exists."""
|
|
203
195
|
if not self._has_state_data(states):
|
|
204
196
|
return 0.0
|
|
205
|
-
return float(ce.conformational_entropy_calculation(states
|
|
197
|
+
return float(ce.conformational_entropy_calculation(states))
|
|
206
198
|
|
|
207
199
|
@staticmethod
|
|
208
200
|
def _get_group_states(
|
|
209
|
-
states_res:
|
|
201
|
+
states_res: dict[int, StateSequence] | Sequence[StateSequence | None],
|
|
210
202
|
group_id: int,
|
|
211
|
-
) ->
|
|
203
|
+
) -> StateSequence | None:
|
|
212
204
|
"""Fetch group states from container."""
|
|
213
205
|
if isinstance(states_res, dict):
|
|
214
206
|
return states_res.get(group_id)
|
|
@@ -217,7 +209,7 @@ class ConfigurationalEntropyNode:
|
|
|
217
209
|
return None
|
|
218
210
|
|
|
219
211
|
@staticmethod
|
|
220
|
-
def _has_state_data(states:
|
|
212
|
+
def _has_state_data(states: StateSequence | None) -> bool:
|
|
221
213
|
"""Check if state container has usable data."""
|
|
222
214
|
if states is None:
|
|
223
215
|
return False
|