MolClusters 0.7.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.
- molclusters/__init__.py +68 -0
- molclusters/__main__.py +19 -0
- molclusters/analysis/__init__.py +40 -0
- molclusters/analysis/base.py +370 -0
- molclusters/analysis/builtins.py +124 -0
- molclusters/analysis/coordinates.py +186 -0
- molclusters/analysis/lineage.py +306 -0
- molclusters/analysis/nucleus.py +168 -0
- molclusters/analysis/report.py +191 -0
- molclusters/analysis/size.py +83 -0
- molclusters/analysis/solute.py +144 -0
- molclusters/cluster.py +1092 -0
- molclusters/config.py +727 -0
- molclusters/conntable.py +725 -0
- molclusters/log.py +220 -0
- molclusters/main.py +587 -0
- molclusters/molclusters.py +328 -0
- molclusters/output.py +320 -0
- molclusters/report.py +504 -0
- molclusters/symdict.py +177 -0
- molclusters/tracker.py +435 -0
- molclusters/version.py +10 -0
- molclusters-0.7.0.dist-info/METADATA +127 -0
- molclusters-0.7.0.dist-info/RECORD +31 -0
- molclusters-0.7.0.dist-info/WHEEL +4 -0
- molclusters-0.7.0.dist-info/entry_points.txt +3 -0
- molclusters-0.7.0.dist-info/licenses/COPYING +674 -0
- molclusters-0.7.0.dist-info/licenses/LICENSE +165 -0
- molclusters-0.7.0.dist-info/licenses/LICENSES/CC0-1.0.txt +121 -0
- molclusters-0.7.0.dist-info/licenses/LICENSES/LGPL-3.0-or-later.txt +304 -0
- molclusters-0.7.0.dist-info/licenses/LICENSES/OFL-1.1.txt +43 -0
molclusters/__init__.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Emanuel Mancio <emanuelmancio@usp.br>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""MolClusters: A Python tool for analyzing cluster formation in molecular dynamics simulations.
|
|
6
|
+
|
|
7
|
+
This module provides functionality to study and analyze the formation of molecular clusters
|
|
8
|
+
from simulation data. It includes tools for processing and analyzing cluster
|
|
9
|
+
dynamics, enabling researchers to gain insights into molecular behavior of clusters.
|
|
10
|
+
|
|
11
|
+
Attributes
|
|
12
|
+
----------
|
|
13
|
+
__version__ (str): The version of the MolClusters package.
|
|
14
|
+
version (str): The version of the MolClusters package.
|
|
15
|
+
|
|
16
|
+
Modules
|
|
17
|
+
-------
|
|
18
|
+
cluster: The `MolGroup` and `Cluster` classes that analyses read.
|
|
19
|
+
molclusters: Core module defining the main `MolClusters` class.
|
|
20
|
+
tracker: The `ClusterTracker`, which keeps cluster ids stable over a trajectory.
|
|
21
|
+
analysis: The analyses run on the tracked clusters, and their `FrameAnalysis` base.
|
|
22
|
+
output: The files the analyses write.
|
|
23
|
+
|
|
24
|
+
Exports
|
|
25
|
+
-------
|
|
26
|
+
MolClusters: The primary class for performing cluster analysis.
|
|
27
|
+
ClusterTracker: Follows the clusters of a trajectory, without analysing them.
|
|
28
|
+
Transition: How the clusters of a frame came from those of the frame before.
|
|
29
|
+
FrameAnalysis: The base class of an analysis, to pass to `MolClusters`.
|
|
30
|
+
Frame, Run: What an analysis sees of the current frame and of the run.
|
|
31
|
+
OutputFile: The declaration of a file an analysis writes.
|
|
32
|
+
Cluster, MolGroup: A tracked cluster, and any group of molecules (e.g. a nucleus).
|
|
33
|
+
start_logging: Function to set up molclusters logger.
|
|
34
|
+
|
|
35
|
+
Example
|
|
36
|
+
-------
|
|
37
|
+
>>> from molclusters import MolClusters
|
|
38
|
+
>>> analyzer = MolClusters(universe, config)
|
|
39
|
+
>>> analyzer.run()
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from loguru import logger
|
|
43
|
+
|
|
44
|
+
logger.disable("molclusters")
|
|
45
|
+
|
|
46
|
+
from . import cluster as cluster
|
|
47
|
+
from .analysis import Frame, FrameAnalysis, Run
|
|
48
|
+
from .cluster import Cluster, MolGroup
|
|
49
|
+
from .log import start_logging
|
|
50
|
+
from .molclusters import MolClusters
|
|
51
|
+
from .output import OutputFile
|
|
52
|
+
from .tracker import ClusterTracker, Transition
|
|
53
|
+
from .version import __version__, version
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"version",
|
|
57
|
+
"__version__",
|
|
58
|
+
"start_logging",
|
|
59
|
+
"MolClusters",
|
|
60
|
+
"ClusterTracker",
|
|
61
|
+
"Transition",
|
|
62
|
+
"FrameAnalysis",
|
|
63
|
+
"Frame",
|
|
64
|
+
"Run",
|
|
65
|
+
"OutputFile",
|
|
66
|
+
"Cluster",
|
|
67
|
+
"MolGroup",
|
|
68
|
+
]
|
molclusters/__main__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Emanuel Mancio <emanuelmancio@usp.br>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""Entry point for the `molclusters` package.
|
|
6
|
+
|
|
7
|
+
The module imports and executes the `main` function from the `main` module,
|
|
8
|
+
serving as the starting point for the application when run as a script.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
To execute the `molclusters` application, run this module directly.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
molclusters -h
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from .main import main
|
|
18
|
+
|
|
19
|
+
main()
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Emanuel Mancio <emanuelmancio@usp.br>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""The analyses run on the tracked clusters, each a `FrameAnalysis`.
|
|
6
|
+
|
|
7
|
+
Modules
|
|
8
|
+
-------
|
|
9
|
+
base: `FrameAnalysis`, and the `Run` and `Frame` an analysis sees.
|
|
10
|
+
builtins: `BUILTINS`, the built-in analyses in the order they run, and what
|
|
11
|
+
each needs from the config.
|
|
12
|
+
coordinates: `ClusterCoordinates`, the clusters holding solutes (.gro files).
|
|
13
|
+
lineage: `Lineage`, where the clusters came from, what became of them and when
|
|
14
|
+
(cluster_events.csv, cluster_lifetimes.csv).
|
|
15
|
+
nucleus: `Nucleus`, the nuclei inside the clusters (nucleus_data.csv).
|
|
16
|
+
report: `JsonReport`, every cluster of every frame (molclusters.jsonl).
|
|
17
|
+
size: `SizeEvolution`, the number and sizes of the clusters over time (evo.txt).
|
|
18
|
+
solute: `SoluteSolvent`, the clusters holding solutes and solvents
|
|
19
|
+
(solute_solvent.csv).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .base import Frame, FrameAnalysis, Run
|
|
23
|
+
from .coordinates import ClusterCoordinates
|
|
24
|
+
from .lineage import Lineage
|
|
25
|
+
from .nucleus import Nucleus
|
|
26
|
+
from .report import JsonReport
|
|
27
|
+
from .size import SizeEvolution
|
|
28
|
+
from .solute import SoluteSolvent
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"ClusterCoordinates",
|
|
32
|
+
"Frame",
|
|
33
|
+
"FrameAnalysis",
|
|
34
|
+
"JsonReport",
|
|
35
|
+
"Lineage",
|
|
36
|
+
"Nucleus",
|
|
37
|
+
"Run",
|
|
38
|
+
"SizeEvolution",
|
|
39
|
+
"SoluteSolvent",
|
|
40
|
+
]
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Emanuel Mancio <emanuelmancio@usp.br>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""Provides `FrameAnalysis`, the base of the analyses run on the tracked clusters.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
--------
|
|
9
|
+
- FrameAnalysis: The base class of an analysis, run frame by frame.
|
|
10
|
+
- Run: What an analysis sees of the run as a whole.
|
|
11
|
+
- Frame: What an analysis sees of the current frame.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from abc import ABC, abstractmethod
|
|
16
|
+
from collections.abc import Generator, Mapping, Sequence
|
|
17
|
+
from contextlib import contextmanager
|
|
18
|
+
from types import MappingProxyType
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import MDAnalysis as mda
|
|
22
|
+
from loguru import logger
|
|
23
|
+
|
|
24
|
+
from ..cluster import Cluster
|
|
25
|
+
from ..config import MolClsConfig
|
|
26
|
+
from ..output import OutputFile, RunOutput
|
|
27
|
+
from ..tracker import ClusterTracker, Transition
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FrameAnalysis(ABC):
|
|
31
|
+
"""An analysis of the tracked clusters, run frame by frame.
|
|
32
|
+
|
|
33
|
+
A run calls `prepare` once, then `analyse` for every frame (the first one
|
|
34
|
+
included) once the clusters are up to date with it, and finally `finish`.
|
|
35
|
+
The analyses of a run are called in order, so an analysis sees the results of
|
|
36
|
+
the ones before it for the same frame.
|
|
37
|
+
|
|
38
|
+
Only `analyse` must be implemented. An analysis keeps its results as its own
|
|
39
|
+
attributes; one that writes files does so through `Run.output` and declares
|
|
40
|
+
them in `outputs`. The same analysis may be run more than once, so it should
|
|
41
|
+
start its results over in `prepare`, not only in ``__init__``.
|
|
42
|
+
|
|
43
|
+
An analysis logs with loguru's ``logger``, as the rest of the package does;
|
|
44
|
+
the run tags whatever is logged during its hooks with its class name (the
|
|
45
|
+
``analysis`` key of the record's ``extra``), which the log shows. To keep the
|
|
46
|
+
log readable over long trajectories:
|
|
47
|
+
|
|
48
|
+
- `analyse` counts what's worth reporting (e.g. in a `collections.Counter`)
|
|
49
|
+
instead of logging it, and `finish` logs a summary: one INFO line, or one
|
|
50
|
+
warning saying how to fix the problem.
|
|
51
|
+
- Anything logged per frame is DEBUG or TRACE, with loguru's own arguments
|
|
52
|
+
(``logger.debug("cluster {}: ...", cls.id)``) rather than an f-string: a
|
|
53
|
+
message filtered out by the log level is then never built.
|
|
54
|
+
- A warning that could repeat is only logged once.
|
|
55
|
+
|
|
56
|
+
An analysis adds its own fields to the JSON report (`JsonReport`, which runs
|
|
57
|
+
after every other analysis) by overriding `report_frame`, `report_cluster`, or
|
|
58
|
+
both: what they return goes in each frame's, or each cluster's, record under
|
|
59
|
+
the analysis' class name, its floats rounded as the report's are.
|
|
60
|
+
|
|
61
|
+
Attributes
|
|
62
|
+
----------
|
|
63
|
+
outputs : tuple[OutputFile, ...]
|
|
64
|
+
The files the analysis writes, to tell which results of an earlier run
|
|
65
|
+
they overwrite (or leave behind), and to report them at the end.
|
|
66
|
+
Usually a class attribute; an analysis whose files depend on its options
|
|
67
|
+
sets it in ``__init__`` instead, since it's read before `prepare`.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
outputs: tuple[OutputFile, ...] = ()
|
|
71
|
+
|
|
72
|
+
def prepare(self, run: "Run") -> None: # noqa: B027 (optional hook)
|
|
73
|
+
"""Get ready for a run, starting over any results. Does nothing unless overridden.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
run : Run
|
|
78
|
+
The run about to start.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
@abstractmethod
|
|
82
|
+
def analyse(self, frame: "Frame") -> None:
|
|
83
|
+
"""Analyse the clusters of the current frame.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
frame : Frame
|
|
88
|
+
The current frame.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
def finish(self, run: "Run") -> None: # noqa: B027 (optional hook)
|
|
92
|
+
"""Wrap up once every frame was analysed. Does nothing unless overridden.
|
|
93
|
+
|
|
94
|
+
Parameters
|
|
95
|
+
----------
|
|
96
|
+
run : Run
|
|
97
|
+
The run that just ended.
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
def report_frame(self, frame: "Frame") -> Mapping[str, Any] | None:
|
|
101
|
+
"""Give what to add to the report's record of the current frame.
|
|
102
|
+
|
|
103
|
+
Called by `JsonReport` after every analysis has analysed the frame.
|
|
104
|
+
|
|
105
|
+
Parameters
|
|
106
|
+
----------
|
|
107
|
+
frame : Frame
|
|
108
|
+
The current frame.
|
|
109
|
+
|
|
110
|
+
Returns
|
|
111
|
+
-------
|
|
112
|
+
Mapping[str, Any] | None
|
|
113
|
+
The fields to add under the analysis' class name: JSON types, and numpy
|
|
114
|
+
scalars or arrays. None adds nothing (the default).
|
|
115
|
+
"""
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def report_cluster(
|
|
119
|
+
self, frame: "Frame", cluster: Cluster
|
|
120
|
+
) -> Mapping[str, Any] | None:
|
|
121
|
+
"""Give what to add to the report's record of a cluster of the current frame.
|
|
122
|
+
|
|
123
|
+
Called by `JsonReport`, for every cluster of the frame, after every analysis
|
|
124
|
+
has analysed the frame.
|
|
125
|
+
|
|
126
|
+
Parameters
|
|
127
|
+
----------
|
|
128
|
+
frame : Frame
|
|
129
|
+
The current frame.
|
|
130
|
+
cluster : Cluster
|
|
131
|
+
The cluster.
|
|
132
|
+
|
|
133
|
+
Returns
|
|
134
|
+
-------
|
|
135
|
+
Mapping[str, Any] | None
|
|
136
|
+
The fields to add under the analysis' class name: JSON types, and numpy
|
|
137
|
+
scalars or arrays. None adds nothing (the default).
|
|
138
|
+
"""
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Run:
|
|
143
|
+
"""What an analysis sees of the run as a whole.
|
|
144
|
+
|
|
145
|
+
Attributes
|
|
146
|
+
----------
|
|
147
|
+
universe : mda.Universe
|
|
148
|
+
The Universe being analysed.
|
|
149
|
+
config : MolClsConfig
|
|
150
|
+
The analysis configuration.
|
|
151
|
+
n_frames : int
|
|
152
|
+
The number of frames of the run.
|
|
153
|
+
output : RunOutput
|
|
154
|
+
Where the analyses write their files.
|
|
155
|
+
durations : list[float]
|
|
156
|
+
The time spent in each analysis' hooks so far, in seconds, in the order of
|
|
157
|
+
the analyses.
|
|
158
|
+
"""
|
|
159
|
+
|
|
160
|
+
__slots__ = [
|
|
161
|
+
"universe",
|
|
162
|
+
"config",
|
|
163
|
+
"n_frames",
|
|
164
|
+
"output",
|
|
165
|
+
"durations",
|
|
166
|
+
"_analyses",
|
|
167
|
+
"_visible",
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
def __init__(
|
|
171
|
+
self,
|
|
172
|
+
universe: mda.Universe,
|
|
173
|
+
config: MolClsConfig,
|
|
174
|
+
n_frames: int,
|
|
175
|
+
output: RunOutput,
|
|
176
|
+
analyses: Sequence[FrameAnalysis],
|
|
177
|
+
) -> None:
|
|
178
|
+
"""Initialize the context of a run.
|
|
179
|
+
|
|
180
|
+
Parameters
|
|
181
|
+
----------
|
|
182
|
+
universe : mda.Universe
|
|
183
|
+
The Universe being analysed.
|
|
184
|
+
config : MolClsConfig
|
|
185
|
+
The analysis configuration.
|
|
186
|
+
n_frames : int
|
|
187
|
+
The number of frames of the run.
|
|
188
|
+
output : RunOutput
|
|
189
|
+
Where the analyses write their files.
|
|
190
|
+
analyses : Sequence[FrameAnalysis]
|
|
191
|
+
The analyses of the run, in the order they're called.
|
|
192
|
+
"""
|
|
193
|
+
self.universe = universe
|
|
194
|
+
self.config = config
|
|
195
|
+
self.n_frames = n_frames
|
|
196
|
+
self.output = output
|
|
197
|
+
self.durations = [0.0] * len(analyses)
|
|
198
|
+
self._analyses = analyses
|
|
199
|
+
self._visible = len(analyses)
|
|
200
|
+
|
|
201
|
+
def prepare_analyses(self) -> None:
|
|
202
|
+
"""Prepare every analysis of the run, in order (called by the runner)."""
|
|
203
|
+
for i, analysis in enumerate(self._analyses):
|
|
204
|
+
self._visible = i
|
|
205
|
+
with self._hook(i, "prepare"):
|
|
206
|
+
analysis.prepare(self)
|
|
207
|
+
self._visible = len(self._analyses)
|
|
208
|
+
|
|
209
|
+
def analyse_frame(self, frame: "Frame") -> None:
|
|
210
|
+
"""Run every analysis of the run on `frame`, in order (called by the runner).
|
|
211
|
+
|
|
212
|
+
Parameters
|
|
213
|
+
----------
|
|
214
|
+
frame : Frame
|
|
215
|
+
The current frame.
|
|
216
|
+
"""
|
|
217
|
+
for i, analysis in enumerate(self._analyses):
|
|
218
|
+
with self._hook(i, "analyse", frame.index):
|
|
219
|
+
analysis.analyse(frame)
|
|
220
|
+
|
|
221
|
+
def finish_analyses(self) -> None:
|
|
222
|
+
"""Finish every analysis of the run, in order (called by the runner)."""
|
|
223
|
+
for i, analysis in enumerate(self._analyses):
|
|
224
|
+
with self._hook(i, "finish"):
|
|
225
|
+
analysis.finish(self)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def analyses(self) -> tuple[FrameAnalysis, ...]:
|
|
229
|
+
"""The analyses of the run, in order; while preparing, only those before.
|
|
230
|
+
|
|
231
|
+
Returns
|
|
232
|
+
-------
|
|
233
|
+
tuple[FrameAnalysis, ...]
|
|
234
|
+
The analyses whose results `analysis` finds (see there).
|
|
235
|
+
"""
|
|
236
|
+
return tuple(self._analyses[: self._visible])
|
|
237
|
+
|
|
238
|
+
def analysis[T: FrameAnalysis](self, kind: type[T]) -> T | None:
|
|
239
|
+
"""Find the first analysis of the run of type `kind`, for its results.
|
|
240
|
+
|
|
241
|
+
While the analyses are being prepared, only those before the one being
|
|
242
|
+
prepared are found, since only their results for a frame are ready when
|
|
243
|
+
it analyses the frame.
|
|
244
|
+
|
|
245
|
+
Parameters
|
|
246
|
+
----------
|
|
247
|
+
kind : type[T]
|
|
248
|
+
The analysis class to look for (subclasses match too).
|
|
249
|
+
|
|
250
|
+
Returns
|
|
251
|
+
-------
|
|
252
|
+
T | None
|
|
253
|
+
The analysis, or None if the run has none (before this one).
|
|
254
|
+
"""
|
|
255
|
+
for analysis in self._analyses[: self._visible]:
|
|
256
|
+
if isinstance(analysis, kind):
|
|
257
|
+
return analysis
|
|
258
|
+
return None
|
|
259
|
+
|
|
260
|
+
@contextmanager
|
|
261
|
+
def _hook(
|
|
262
|
+
self, i: int, hook: str, frame: int | None = None
|
|
263
|
+
) -> Generator[None, None, None]:
|
|
264
|
+
"""Call a hook of the `i`-th analysis: tag its log, time it, blame its errors.
|
|
265
|
+
|
|
266
|
+
What's logged during the call is tagged with the analysis' class name, and
|
|
267
|
+
an error it raises is noted with it, so either can be told from the run's
|
|
268
|
+
own; the time taken is added to `durations`.
|
|
269
|
+
|
|
270
|
+
Parameters
|
|
271
|
+
----------
|
|
272
|
+
i : int
|
|
273
|
+
The index of the analysis being called.
|
|
274
|
+
hook : str
|
|
275
|
+
The name of the method being called.
|
|
276
|
+
frame : int | None
|
|
277
|
+
The index of the frame being analysed, if any.
|
|
278
|
+
|
|
279
|
+
Yields
|
|
280
|
+
------
|
|
281
|
+
None
|
|
282
|
+
Control, for the call to the analysis.
|
|
283
|
+
"""
|
|
284
|
+
name = type(self._analyses[i]).__name__
|
|
285
|
+
start = time.perf_counter()
|
|
286
|
+
try:
|
|
287
|
+
with logger.contextualize(analysis=name):
|
|
288
|
+
yield
|
|
289
|
+
except Exception as err:
|
|
290
|
+
where = "" if frame is None else f" on frame {frame}"
|
|
291
|
+
err.add_note(f"Raised by {name}.{hook}(){where}")
|
|
292
|
+
raise
|
|
293
|
+
finally:
|
|
294
|
+
self.durations[i] += time.perf_counter() - start
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
class Frame:
|
|
298
|
+
"""What an analysis sees of the current frame.
|
|
299
|
+
|
|
300
|
+
Attributes
|
|
301
|
+
----------
|
|
302
|
+
index : int
|
|
303
|
+
The index of the frame in the run (0 for its first frame).
|
|
304
|
+
time : float
|
|
305
|
+
The time of the frame, as the trajectory reports it.
|
|
306
|
+
universe : mda.Universe
|
|
307
|
+
The Universe, positioned at this frame.
|
|
308
|
+
output : RunOutput
|
|
309
|
+
Where the run's analyses write their files (the same as `Run.output`),
|
|
310
|
+
e.g. to append this frame to a file.
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
__slots__ = ["index", "time", "universe", "output", "_tracker"]
|
|
314
|
+
|
|
315
|
+
def __init__(self, index: int, tracker: ClusterTracker, output: RunOutput) -> None:
|
|
316
|
+
"""Initialize the current frame of a run.
|
|
317
|
+
|
|
318
|
+
Parameters
|
|
319
|
+
----------
|
|
320
|
+
index : int
|
|
321
|
+
The index of the frame in the run.
|
|
322
|
+
tracker : ClusterTracker
|
|
323
|
+
The tracker, up to date with the frame.
|
|
324
|
+
output : RunOutput
|
|
325
|
+
Where the run's analyses write their files.
|
|
326
|
+
"""
|
|
327
|
+
self.index = index
|
|
328
|
+
self.time: float = tracker.uni.coord.time
|
|
329
|
+
self.universe = tracker.uni
|
|
330
|
+
self.output = output
|
|
331
|
+
self._tracker = tracker
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def clusters(self) -> Mapping[int, Cluster]:
|
|
335
|
+
"""The clusters of the frame, keyed by cluster id (read-only).
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
-------
|
|
339
|
+
Mapping[int, Cluster]
|
|
340
|
+
A read-only view of the clusters; the clusters must not be modified.
|
|
341
|
+
"""
|
|
342
|
+
return MappingProxyType(self._tracker.clusters)
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def transition(self) -> Transition:
|
|
346
|
+
"""How the frame's clusters came from the previous frame's.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
Transition
|
|
351
|
+
Where each cluster's molecules came from, which clusters are new, and
|
|
352
|
+
which ended, merged or dissolved (on the run's first frame, every
|
|
353
|
+
cluster is new).
|
|
354
|
+
"""
|
|
355
|
+
return self._tracker.transition
|
|
356
|
+
|
|
357
|
+
def find(self, mol: int) -> int | None:
|
|
358
|
+
"""Find the id of the cluster a molecule belongs to.
|
|
359
|
+
|
|
360
|
+
Parameters
|
|
361
|
+
----------
|
|
362
|
+
mol : int
|
|
363
|
+
The molecule's resid.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
int | None
|
|
368
|
+
The cluster id, or None if the molecule is in no cluster.
|
|
369
|
+
"""
|
|
370
|
+
return self._tracker.find(mol)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: © 2026 Emanuel Mancio <emanuelmancio@usp.br>
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: LGPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
"""Provides `BUILTINS`, the built-in analyses, in the order they run.
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
--------
|
|
9
|
+
- Builtin: A built-in analysis, how the config builds it and what it needs.
|
|
10
|
+
|
|
11
|
+
Functions:
|
|
12
|
+
----------
|
|
13
|
+
- build_builtins: Build the built-in analyses a config runs.
|
|
14
|
+
|
|
15
|
+
A built-in runs unless the config's `analyses` turns it off (by class name),
|
|
16
|
+
and only when the config options it needs are set. Adding one takes a line in
|
|
17
|
+
`BUILTINS`; the config checks `analyses` against it. They run in its order, the
|
|
18
|
+
report last (after the analyses given to `MolClusters` too).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from collections.abc import Callable, Iterable
|
|
22
|
+
from dataclasses import dataclass
|
|
23
|
+
|
|
24
|
+
from ..config import MolClsConfig
|
|
25
|
+
from .base import FrameAnalysis
|
|
26
|
+
from .coordinates import ClusterCoordinates
|
|
27
|
+
from .lineage import Lineage
|
|
28
|
+
from .nucleus import Nucleus
|
|
29
|
+
from .report import JsonReport
|
|
30
|
+
from .size import SizeEvolution
|
|
31
|
+
from .solute import SoluteSolvent
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True, slots=True)
|
|
35
|
+
class Builtin:
|
|
36
|
+
"""A built-in analysis, how the config builds it and what it needs.
|
|
37
|
+
|
|
38
|
+
Attributes
|
|
39
|
+
----------
|
|
40
|
+
kind : type[FrameAnalysis]
|
|
41
|
+
The analysis class; its name turns it on or off in the config's `analyses`.
|
|
42
|
+
build : Callable[[MolClsConfig], FrameAnalysis]
|
|
43
|
+
Builds the analysis from the config, once the options it needs are set.
|
|
44
|
+
needs : tuple[str, ...]
|
|
45
|
+
The config options it needs, without which it doesn't run.
|
|
46
|
+
last : bool
|
|
47
|
+
Whether it runs after every other analysis, the ones given to
|
|
48
|
+
`MolClusters` included: the report, which the others add to.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
kind: type[FrameAnalysis]
|
|
52
|
+
build: Callable[[MolClsConfig], FrameAnalysis]
|
|
53
|
+
needs: tuple[str, ...] = ()
|
|
54
|
+
last: bool = False
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def name(self) -> str:
|
|
58
|
+
"""The analysis' class name, as the config's `analyses` and the log name it.
|
|
59
|
+
|
|
60
|
+
Returns
|
|
61
|
+
-------
|
|
62
|
+
str
|
|
63
|
+
E.g. ``"JsonReport"``.
|
|
64
|
+
"""
|
|
65
|
+
return self.kind.__name__
|
|
66
|
+
|
|
67
|
+
def runs(self, config: MolClsConfig) -> bool:
|
|
68
|
+
"""Whether the config runs this analysis.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
config : MolClsConfig
|
|
73
|
+
The analysis configuration.
|
|
74
|
+
|
|
75
|
+
Returns
|
|
76
|
+
-------
|
|
77
|
+
bool
|
|
78
|
+
True unless `analyses` turns it off or an option it needs isn't set.
|
|
79
|
+
"""
|
|
80
|
+
return config.analyses.get(self.name, True) and all(
|
|
81
|
+
getattr(config, option) is not None for option in self.needs
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
BUILTINS: tuple[Builtin, ...] = (
|
|
86
|
+
Builtin(SizeEvolution, lambda c: SizeEvolution()),
|
|
87
|
+
Builtin(Lineage, lambda c: Lineage()),
|
|
88
|
+
Builtin(
|
|
89
|
+
SoluteSolvent, lambda c: SoluteSolvent(c.solute, c.solvent), needs=("solute",)
|
|
90
|
+
),
|
|
91
|
+
Builtin(
|
|
92
|
+
ClusterCoordinates,
|
|
93
|
+
lambda c: ClusterCoordinates(c.solute, follow=c._follow_solute),
|
|
94
|
+
needs=("solute",),
|
|
95
|
+
),
|
|
96
|
+
Builtin(Nucleus, lambda c: Nucleus(c.nucleus), needs=("nucleus",)),
|
|
97
|
+
Builtin(JsonReport, lambda c: JsonReport(c.report_compression), last=True),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_builtins(
|
|
102
|
+
config: MolClsConfig, extra: Iterable[FrameAnalysis] = ()
|
|
103
|
+
) -> list[FrameAnalysis]:
|
|
104
|
+
"""Build the built-in analyses a config runs, and order them with `extra`.
|
|
105
|
+
|
|
106
|
+
Parameters
|
|
107
|
+
----------
|
|
108
|
+
config : MolClsConfig
|
|
109
|
+
The analysis configuration, with the topology-dependent defaults filled in.
|
|
110
|
+
extra : Iterable[FrameAnalysis]
|
|
111
|
+
Other analyses to run, after the built-ins but those that run `last`.
|
|
112
|
+
|
|
113
|
+
Returns
|
|
114
|
+
-------
|
|
115
|
+
list[FrameAnalysis]
|
|
116
|
+
The analyses in the order they run: the built-ins the config runs (see
|
|
117
|
+
`Builtin.runs`), then `extra`, then the built-ins that run last.
|
|
118
|
+
"""
|
|
119
|
+
built = [(b.last, b.build(config)) for b in BUILTINS if b.runs(config)]
|
|
120
|
+
return [
|
|
121
|
+
*(analysis for last, analysis in built if not last),
|
|
122
|
+
*extra,
|
|
123
|
+
*(analysis for last, analysis in built if last),
|
|
124
|
+
]
|