reaxkit 1.0.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.
- reaxkit/__init__.py +0 -0
- reaxkit/analysis/__init__.py +0 -0
- reaxkit/analysis/composed/RDF_analyzer.py +560 -0
- reaxkit/analysis/composed/__init__.py +0 -0
- reaxkit/analysis/composed/connectivity_analyzer.py +706 -0
- reaxkit/analysis/composed/coordination_analyzer.py +144 -0
- reaxkit/analysis/composed/electrostatics_analyzer.py +687 -0
- reaxkit/analysis/per_file/__init__.py +0 -0
- reaxkit/analysis/per_file/control_analyzer.py +165 -0
- reaxkit/analysis/per_file/eregime_analyzer.py +108 -0
- reaxkit/analysis/per_file/ffield_analyzer.py +305 -0
- reaxkit/analysis/per_file/fort13_analyzer.py +79 -0
- reaxkit/analysis/per_file/fort57_analyzer.py +106 -0
- reaxkit/analysis/per_file/fort73_analyzer.py +61 -0
- reaxkit/analysis/per_file/fort74_analyzer.py +65 -0
- reaxkit/analysis/per_file/fort76_analyzer.py +191 -0
- reaxkit/analysis/per_file/fort78_analyzer.py +154 -0
- reaxkit/analysis/per_file/fort79_analyzer.py +83 -0
- reaxkit/analysis/per_file/fort7_analyzer.py +393 -0
- reaxkit/analysis/per_file/fort99_analyzer.py +411 -0
- reaxkit/analysis/per_file/molfra_analyzer.py +359 -0
- reaxkit/analysis/per_file/params_analyzer.py +258 -0
- reaxkit/analysis/per_file/summary_analyzer.py +84 -0
- reaxkit/analysis/per_file/trainset_analyzer.py +84 -0
- reaxkit/analysis/per_file/vels_analyzer.py +95 -0
- reaxkit/analysis/per_file/xmolout_analyzer.py +528 -0
- reaxkit/cli.py +181 -0
- reaxkit/count_loc.py +276 -0
- reaxkit/data/alias.yaml +89 -0
- reaxkit/data/constants.yaml +27 -0
- reaxkit/data/reaxff_input_files_contents.yaml +186 -0
- reaxkit/data/reaxff_output_files_contents.yaml +301 -0
- reaxkit/data/units.yaml +38 -0
- reaxkit/help/__init__.py +0 -0
- reaxkit/help/help_index_loader.py +531 -0
- reaxkit/help/introspection_utils.py +131 -0
- reaxkit/io/__init__.py +0 -0
- reaxkit/io/base_handler.py +165 -0
- reaxkit/io/generators/__init__.py +0 -0
- reaxkit/io/generators/control_generator.py +123 -0
- reaxkit/io/generators/eregime_generator.py +341 -0
- reaxkit/io/generators/geo_generator.py +967 -0
- reaxkit/io/generators/trainset_generator.py +1758 -0
- reaxkit/io/generators/tregime_generator.py +113 -0
- reaxkit/io/generators/vregime_generator.py +164 -0
- reaxkit/io/generators/xmolout_generator.py +304 -0
- reaxkit/io/handlers/__init__.py +0 -0
- reaxkit/io/handlers/control_handler.py +209 -0
- reaxkit/io/handlers/eregime_handler.py +122 -0
- reaxkit/io/handlers/ffield_handler.py +812 -0
- reaxkit/io/handlers/fort13_handler.py +123 -0
- reaxkit/io/handlers/fort57_handler.py +143 -0
- reaxkit/io/handlers/fort73_handler.py +145 -0
- reaxkit/io/handlers/fort74_handler.py +155 -0
- reaxkit/io/handlers/fort76_handler.py +195 -0
- reaxkit/io/handlers/fort78_handler.py +142 -0
- reaxkit/io/handlers/fort79_handler.py +227 -0
- reaxkit/io/handlers/fort7_handler.py +264 -0
- reaxkit/io/handlers/fort99_handler.py +128 -0
- reaxkit/io/handlers/geo_handler.py +224 -0
- reaxkit/io/handlers/molfra_handler.py +184 -0
- reaxkit/io/handlers/params_handler.py +137 -0
- reaxkit/io/handlers/summary_handler.py +135 -0
- reaxkit/io/handlers/trainset_handler.py +658 -0
- reaxkit/io/handlers/vels_handler.py +293 -0
- reaxkit/io/handlers/xmolout_handler.py +174 -0
- reaxkit/utils/__init__.py +0 -0
- reaxkit/utils/alias.py +219 -0
- reaxkit/utils/cache.py +77 -0
- reaxkit/utils/constants.py +75 -0
- reaxkit/utils/equation_of_states.py +96 -0
- reaxkit/utils/exceptions.py +27 -0
- reaxkit/utils/frame_utils.py +175 -0
- reaxkit/utils/log.py +43 -0
- reaxkit/utils/media/__init__.py +0 -0
- reaxkit/utils/media/convert.py +90 -0
- reaxkit/utils/media/make_video.py +91 -0
- reaxkit/utils/media/plotter.py +812 -0
- reaxkit/utils/numerical/__init__.py +0 -0
- reaxkit/utils/numerical/extrema_finder.py +96 -0
- reaxkit/utils/numerical/moving_average.py +103 -0
- reaxkit/utils/numerical/numerical_calcs.py +75 -0
- reaxkit/utils/numerical/signal_ops.py +135 -0
- reaxkit/utils/path.py +55 -0
- reaxkit/utils/units.py +104 -0
- reaxkit/webui/__init__.py +0 -0
- reaxkit/webui/app.py +0 -0
- reaxkit/webui/components.py +0 -0
- reaxkit/webui/layouts.py +0 -0
- reaxkit/webui/utils.py +0 -0
- reaxkit/workflows/__init__.py +0 -0
- reaxkit/workflows/composed/__init__.py +0 -0
- reaxkit/workflows/composed/coordination_workflow.py +393 -0
- reaxkit/workflows/composed/electrostatics_workflow.py +587 -0
- reaxkit/workflows/composed/xmolout_fort7_workflow.py +343 -0
- reaxkit/workflows/meta/__init__.py +0 -0
- reaxkit/workflows/meta/help_workflow.py +136 -0
- reaxkit/workflows/meta/introspection_workflow.py +235 -0
- reaxkit/workflows/meta/make_video_workflow.py +61 -0
- reaxkit/workflows/meta/plotter_workflow.py +601 -0
- reaxkit/workflows/per_file/__init__.py +0 -0
- reaxkit/workflows/per_file/control_workflow.py +110 -0
- reaxkit/workflows/per_file/eregime_workflow.py +267 -0
- reaxkit/workflows/per_file/ffield_workflow.py +390 -0
- reaxkit/workflows/per_file/fort13_workflow.py +86 -0
- reaxkit/workflows/per_file/fort57_workflow.py +137 -0
- reaxkit/workflows/per_file/fort73_workflow.py +151 -0
- reaxkit/workflows/per_file/fort74_workflow.py +88 -0
- reaxkit/workflows/per_file/fort76_workflow.py +188 -0
- reaxkit/workflows/per_file/fort78_workflow.py +135 -0
- reaxkit/workflows/per_file/fort79_workflow.py +314 -0
- reaxkit/workflows/per_file/fort7_workflow.py +592 -0
- reaxkit/workflows/per_file/fort83_workflow.py +60 -0
- reaxkit/workflows/per_file/fort99_workflow.py +223 -0
- reaxkit/workflows/per_file/geo_workflow.py +554 -0
- reaxkit/workflows/per_file/molfra_workflow.py +577 -0
- reaxkit/workflows/per_file/params_workflow.py +135 -0
- reaxkit/workflows/per_file/summary_workflow.py +161 -0
- reaxkit/workflows/per_file/trainset_workflow.py +356 -0
- reaxkit/workflows/per_file/tregime_workflow.py +79 -0
- reaxkit/workflows/per_file/vels_workflow.py +309 -0
- reaxkit/workflows/per_file/vregime_workflow.py +75 -0
- reaxkit/workflows/per_file/xmolout_workflow.py +678 -0
- reaxkit-1.0.0.dist-info/METADATA +128 -0
- reaxkit-1.0.0.dist-info/RECORD +130 -0
- reaxkit-1.0.0.dist-info/WHEEL +5 -0
- reaxkit-1.0.0.dist-info/entry_points.txt +2 -0
- reaxkit-1.0.0.dist-info/licenses/AUTHORS.md +20 -0
- reaxkit-1.0.0.dist-info/licenses/LICENSE +21 -0
- reaxkit-1.0.0.dist-info/top_level.txt +1 -0
reaxkit/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Radial Distribution Function (RDF) analysis utilities.
|
|
3
|
+
|
|
4
|
+
This module provides single-frame and multi-frame RDF calculations for
|
|
5
|
+
ReaxFF trajectories using either the FREUD or OVITO backends, as well as
|
|
6
|
+
helper utilities for extracting RDF-derived structural descriptors.
|
|
7
|
+
|
|
8
|
+
Typical use cases include:
|
|
9
|
+
|
|
10
|
+
- computing total or partial RDFs for selected frames
|
|
11
|
+
- averaging RDFs across multiple frames
|
|
12
|
+
- extracting RDF-based properties such as peak positions or integrated areas
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
from typing import Iterable, Optional, Sequence, Tuple, List, Dict, Union
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
# FREUD backend (pip install freud-analysis)
|
|
22
|
+
import freud
|
|
23
|
+
|
|
24
|
+
from reaxkit.io.handlers.xmolout_handler import XmoloutHandler
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ==========================================================
|
|
28
|
+
# =============== Single-frame RDF backends ================
|
|
29
|
+
# ==========================================================
|
|
30
|
+
|
|
31
|
+
def _rdf_freud_frame(
|
|
32
|
+
handler: XmoloutHandler,
|
|
33
|
+
frame_index: int,
|
|
34
|
+
*,
|
|
35
|
+
types_a: Optional[Iterable[str]] = None,
|
|
36
|
+
types_b: Optional[Iterable[str]] = None,
|
|
37
|
+
r_max: Optional[float] = None,
|
|
38
|
+
bins: int = 200,
|
|
39
|
+
drop_first_bin: bool = True,
|
|
40
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
41
|
+
"""Compute the RDF for a single frame using the FREUD backend.
|
|
42
|
+
|
|
43
|
+
Works on
|
|
44
|
+
--------
|
|
45
|
+
XmoloutHandler — ``xmolout``
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
handler : XmoloutHandler
|
|
50
|
+
Parsed trajectory handler.
|
|
51
|
+
frame_index : int
|
|
52
|
+
Frame index to analyze.
|
|
53
|
+
types_a, types_b : iterable of str, optional
|
|
54
|
+
Atom types defining A–B RDF. If None, all atoms are used.
|
|
55
|
+
r_max : float, optional
|
|
56
|
+
Maximum radius. Defaults to half the smallest box length.
|
|
57
|
+
bins : int, default=200
|
|
58
|
+
Number of RDF bins.
|
|
59
|
+
drop_first_bin : bool, default=True
|
|
60
|
+
Drop the first bin to avoid self-counting artifacts.
|
|
61
|
+
|
|
62
|
+
Returns
|
|
63
|
+
-------
|
|
64
|
+
r : np.ndarray
|
|
65
|
+
Radial distances.
|
|
66
|
+
g : np.ndarray
|
|
67
|
+
RDF values g(r).
|
|
68
|
+
|
|
69
|
+
Examples
|
|
70
|
+
--------
|
|
71
|
+
>>> r, g = _rdf_freud_frame(xh, frame_index=0, types_a=["Al"], types_b=["N"])
|
|
72
|
+
"""
|
|
73
|
+
df = handler.dataframe()
|
|
74
|
+
fr = handler.frame(int(frame_index))
|
|
75
|
+
|
|
76
|
+
coords = fr["coords"]
|
|
77
|
+
atom_types = np.asarray(fr["atom_types"], dtype=str)
|
|
78
|
+
|
|
79
|
+
# Box from sim DF (supports non-orthogonal if available)
|
|
80
|
+
row = df.iloc[int(frame_index)]
|
|
81
|
+
a, b, c = float(row["a"]), float(row["b"]), float(row["c"])
|
|
82
|
+
alpha = float(row["alpha"]) if "alpha" in df.columns else 90.0
|
|
83
|
+
beta = float(row["beta"]) if "beta" in df.columns else 90.0
|
|
84
|
+
gamma = float(row["gamma"]) if "gamma" in df.columns else 90.0
|
|
85
|
+
|
|
86
|
+
box = freud.box.Box.from_box_lengths_and_angles(
|
|
87
|
+
a, b, c, np.radians(alpha), np.radians(beta), np.radians(gamma)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# A/B masks
|
|
91
|
+
A_mask = np.ones(len(atom_types), dtype=bool) if types_a is None else np.isin(atom_types, list(types_a))
|
|
92
|
+
B_mask = np.ones(len(atom_types), dtype=bool) if types_b is None else np.isin(atom_types, list(types_b))
|
|
93
|
+
A = coords[A_mask]
|
|
94
|
+
B = coords[B_mask]
|
|
95
|
+
|
|
96
|
+
if r_max is None:
|
|
97
|
+
r_max = 0.5 * float(min(a, b, c))
|
|
98
|
+
|
|
99
|
+
rdf = freud.density.RDF(bins=int(bins), r_max=float(r_max))
|
|
100
|
+
rdf.compute((box, B), query_points=A)
|
|
101
|
+
|
|
102
|
+
r = rdf.bin_centers.copy()
|
|
103
|
+
g = rdf.rdf.copy()
|
|
104
|
+
if drop_first_bin and len(r) > 1:
|
|
105
|
+
r, g = r[1:], g[1:]
|
|
106
|
+
return r, g
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _write_xyz_temp(coords: np.ndarray, types: Sequence[str]) -> str:
|
|
110
|
+
import tempfile, os
|
|
111
|
+
def _norm(sym: str) -> str:
|
|
112
|
+
s = str(sym).strip()
|
|
113
|
+
return s[:1].upper() + s[1:].lower()
|
|
114
|
+
n = coords.shape[0]
|
|
115
|
+
fd, path = tempfile.mkstemp(suffix=".xyz", prefix="reaxkit_rdf_")
|
|
116
|
+
os.close(fd)
|
|
117
|
+
with open(path, "w") as f:
|
|
118
|
+
f.write(f"{n}\n")
|
|
119
|
+
f.write("generated by reaxkit RDF (single-frame)\n")
|
|
120
|
+
for t, (x, y, z) in zip(types, coords):
|
|
121
|
+
f.write(f"{_norm(t)} {x:.9f} {y:.9f} {z:.9f}\n")
|
|
122
|
+
return path
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _rdf_ovito_total_frame(
|
|
126
|
+
handler: XmoloutHandler,
|
|
127
|
+
frame_index: int,
|
|
128
|
+
*,
|
|
129
|
+
r_max: float = 4.0,
|
|
130
|
+
bins: int = 200,
|
|
131
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
132
|
+
"""Compute the total RDF for a single frame using the OVITO backend.
|
|
133
|
+
|
|
134
|
+
Works on
|
|
135
|
+
--------
|
|
136
|
+
XmoloutHandler — ``xmolout``
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
handler : XmoloutHandler
|
|
141
|
+
Parsed trajectory handler.
|
|
142
|
+
frame_index : int
|
|
143
|
+
Frame index to analyze.
|
|
144
|
+
r_max : float, default=4.0
|
|
145
|
+
RDF cutoff radius.
|
|
146
|
+
bins : int, default=200
|
|
147
|
+
Number of RDF bins.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
r : np.ndarray
|
|
152
|
+
Radial distances.
|
|
153
|
+
g : np.ndarray
|
|
154
|
+
RDF values g(r).
|
|
155
|
+
|
|
156
|
+
Examples
|
|
157
|
+
--------
|
|
158
|
+
>>> r, g = _rdf_ovito_total_frame(xh, frame_index=10)
|
|
159
|
+
"""
|
|
160
|
+
fr = handler.frame(int(frame_index))
|
|
161
|
+
coords = fr["coords"]
|
|
162
|
+
types = fr["atom_types"]
|
|
163
|
+
|
|
164
|
+
import os
|
|
165
|
+
xyz = _write_xyz_temp(coords, types)
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
from ovito.io import import_file
|
|
169
|
+
from ovito.modifiers import CoordinationAnalysisModifier
|
|
170
|
+
except Exception as e:
|
|
171
|
+
raise ImportError(
|
|
172
|
+
"OVITO is required for RDF analysis. "
|
|
173
|
+
"Install reaxkit[viz] and run in an environment with EGL/Qt."
|
|
174
|
+
) from e
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
pipe = import_file(xyz)
|
|
178
|
+
pipe.modifiers.append(CoordinationAnalysisModifier(cutoff=float(r_max), number_of_bins=int(bins), partial=False))
|
|
179
|
+
data = pipe.compute()
|
|
180
|
+
table = data.tables["coordination-rdf"]
|
|
181
|
+
arr = table.xy()
|
|
182
|
+
r = np.asarray(arr[:, 0])
|
|
183
|
+
g = np.asarray(arr[:, 1])
|
|
184
|
+
return r, g
|
|
185
|
+
finally:
|
|
186
|
+
try: os.remove(xyz)
|
|
187
|
+
except OSError: pass
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _rdf_ovito_partial_frame(
|
|
191
|
+
handler: XmoloutHandler,
|
|
192
|
+
frame_index: int,
|
|
193
|
+
*,
|
|
194
|
+
r_max: float = 4.0,
|
|
195
|
+
bins: int = 200,
|
|
196
|
+
type_a: str,
|
|
197
|
+
type_b: str,
|
|
198
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
199
|
+
"""Compute a partial A–B RDF for a single frame using the OVITO backend.
|
|
200
|
+
|
|
201
|
+
Works on
|
|
202
|
+
--------
|
|
203
|
+
XmoloutHandler — ``xmolout``
|
|
204
|
+
|
|
205
|
+
Parameters
|
|
206
|
+
----------
|
|
207
|
+
handler : XmoloutHandler
|
|
208
|
+
Parsed trajectory handler.
|
|
209
|
+
frame_index : int
|
|
210
|
+
Frame index to analyze.
|
|
211
|
+
r_max : float, default=4.0
|
|
212
|
+
RDF cutoff radius.
|
|
213
|
+
bins : int, default=200
|
|
214
|
+
Number of RDF bins.
|
|
215
|
+
type_a, type_b : str
|
|
216
|
+
Atom types defining the partial RDF (e.g. ``Al``–``N``).
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
-------
|
|
220
|
+
r : np.ndarray
|
|
221
|
+
Radial distances.
|
|
222
|
+
g : np.ndarray
|
|
223
|
+
RDF values g(r).
|
|
224
|
+
|
|
225
|
+
Examples
|
|
226
|
+
--------
|
|
227
|
+
>>> r, g = _rdf_ovito_partial_frame(xh, 0, type_a="Al", type_b="N")
|
|
228
|
+
"""
|
|
229
|
+
fr = handler.frame(int(frame_index))
|
|
230
|
+
coords = fr["coords"]
|
|
231
|
+
types = fr["atom_types"]
|
|
232
|
+
|
|
233
|
+
import os
|
|
234
|
+
xyz = _write_xyz_temp(coords, types)
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
from ovito.io import import_file
|
|
238
|
+
from ovito.modifiers import CoordinationAnalysisModifier
|
|
239
|
+
except Exception as e:
|
|
240
|
+
raise ImportError(
|
|
241
|
+
"OVITO is required for RDF analysis. "
|
|
242
|
+
"Install reaxkit[viz] and run in an environment with EGL/Qt."
|
|
243
|
+
) from e
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
pipe = import_file(xyz)
|
|
247
|
+
pipe.modifiers.append(CoordinationAnalysisModifier(cutoff=float(r_max), number_of_bins=int(bins), partial=True))
|
|
248
|
+
data = pipe.compute()
|
|
249
|
+
base = "coordination-rdf"
|
|
250
|
+
if base not in data.tables:
|
|
251
|
+
raise KeyError(f"'{base}' table not found. Available: {list(data.tables.keys())}")
|
|
252
|
+
table = data.tables[base]
|
|
253
|
+
|
|
254
|
+
# locate the pair component
|
|
255
|
+
try:
|
|
256
|
+
names = list(table.y.component_names)
|
|
257
|
+
except Exception:
|
|
258
|
+
names = list(getattr(table.y, "components", []))
|
|
259
|
+
want = f"{type_a}-{type_b}"
|
|
260
|
+
if want not in names and f"{type_b}-{type_a}" in names:
|
|
261
|
+
want = f"{type_b}-{type_a}"
|
|
262
|
+
idx = names.index(want)
|
|
263
|
+
|
|
264
|
+
arr = table.xy()
|
|
265
|
+
r = np.asarray(arr[:, 0])
|
|
266
|
+
g = np.asarray(arr[:, 1 + idx])
|
|
267
|
+
return r, g
|
|
268
|
+
finally:
|
|
269
|
+
try: os.remove(xyz)
|
|
270
|
+
except OSError: pass
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# ==========================================================
|
|
274
|
+
# ================= RDF utilities (no smoothing) ===========
|
|
275
|
+
# ==========================================================
|
|
276
|
+
|
|
277
|
+
def _dominant_peak(r: np.ndarray, g: np.ndarray) -> tuple[float, float]:
|
|
278
|
+
"""Return the position and height of the dominant (global) RDF peak.
|
|
279
|
+
|
|
280
|
+
Parameters
|
|
281
|
+
----------
|
|
282
|
+
r : np.ndarray
|
|
283
|
+
Radial distances.
|
|
284
|
+
g : np.ndarray
|
|
285
|
+
RDF values.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
r_peak : float
|
|
290
|
+
Radius at the global maximum of g(r).
|
|
291
|
+
g_peak : float
|
|
292
|
+
Height of the global maximum.
|
|
293
|
+
|
|
294
|
+
Examples
|
|
295
|
+
--------
|
|
296
|
+
>>> r_peak, g_peak = _dominant_peak(r, g)
|
|
297
|
+
"""
|
|
298
|
+
if len(g) == 0:
|
|
299
|
+
return float("nan"), float("nan")
|
|
300
|
+
k = int(np.argmax(g))
|
|
301
|
+
return float(r[k]), float(g[k])
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _first_local_max(r: np.ndarray, g: np.ndarray) -> tuple[float, float]:
|
|
305
|
+
"""Return the first local maximum of an RDF curve.
|
|
306
|
+
|
|
307
|
+
Parameters
|
|
308
|
+
----------
|
|
309
|
+
r : np.ndarray
|
|
310
|
+
Radial distances.
|
|
311
|
+
g : np.ndarray
|
|
312
|
+
RDF values.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
r_peak : float
|
|
317
|
+
Radius of the first local maximum.
|
|
318
|
+
g_peak : float
|
|
319
|
+
Height of the first local maximum.
|
|
320
|
+
|
|
321
|
+
Notes
|
|
322
|
+
-----
|
|
323
|
+
Falls back to the dominant peak if no local maximum is found.
|
|
324
|
+
|
|
325
|
+
Examples
|
|
326
|
+
--------
|
|
327
|
+
>>> r_first, g_first = _first_local_max(r, g)
|
|
328
|
+
"""
|
|
329
|
+
n = len(g)
|
|
330
|
+
for i in range(1, n - 1):
|
|
331
|
+
if g[i] > g[i - 1] and g[i] > g[i + 1]:
|
|
332
|
+
return float(r[i]), float(g[i])
|
|
333
|
+
return _dominant_peak(r, g)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# ==========================================================
|
|
337
|
+
# ============== Multi-frame RDF + properties ==============
|
|
338
|
+
# ==========================================================
|
|
339
|
+
|
|
340
|
+
def rdf_using_freud(
|
|
341
|
+
handler: XmoloutHandler,
|
|
342
|
+
*,
|
|
343
|
+
frames: Optional[Iterable[int]] = None,
|
|
344
|
+
types_a: Optional[Iterable[str]] = None,
|
|
345
|
+
types_b: Optional[Iterable[str]] = None,
|
|
346
|
+
r_max: Optional[float] = None,
|
|
347
|
+
bins: int = 200,
|
|
348
|
+
average: bool = True,
|
|
349
|
+
return_stack: bool = False,
|
|
350
|
+
) -> Tuple[np.ndarray, Union[np.ndarray, List[np.ndarray]]]:
|
|
351
|
+
"""Compute RDFs across multiple frames using the FREUD backend.
|
|
352
|
+
|
|
353
|
+
Works on
|
|
354
|
+
--------
|
|
355
|
+
XmoloutHandler — ``xmolout``
|
|
356
|
+
|
|
357
|
+
Parameters
|
|
358
|
+
----------
|
|
359
|
+
frames : iterable of int, optional
|
|
360
|
+
Frame indices to include.
|
|
361
|
+
types_a, types_b : iterable of str, optional
|
|
362
|
+
Atom types defining A–B RDF.
|
|
363
|
+
average : bool, default=True
|
|
364
|
+
If True, return the average RDF across frames.
|
|
365
|
+
return_stack : bool, default=False
|
|
366
|
+
If True and ``average=False``, return all per-frame RDFs.
|
|
367
|
+
|
|
368
|
+
Returns
|
|
369
|
+
-------
|
|
370
|
+
r : np.ndarray
|
|
371
|
+
Radial distances.
|
|
372
|
+
g : np.ndarray or list[np.ndarray]
|
|
373
|
+
Averaged RDF, last RDF, or stack of RDFs.
|
|
374
|
+
|
|
375
|
+
Examples
|
|
376
|
+
--------
|
|
377
|
+
>>> r, g = rdf_using_freud(xh, frames=range(100), average=True)
|
|
378
|
+
"""
|
|
379
|
+
df = handler.dataframe()
|
|
380
|
+
if frames is None:
|
|
381
|
+
frames = range(len(df))
|
|
382
|
+
frames = list(frames)
|
|
383
|
+
|
|
384
|
+
r_ref: np.ndarray | None = None
|
|
385
|
+
stack: List[np.ndarray] = []
|
|
386
|
+
|
|
387
|
+
for i in frames:
|
|
388
|
+
r, g = _rdf_freud_frame(
|
|
389
|
+
handler, frame_index=int(i),
|
|
390
|
+
types_a=types_a, types_b=types_b, r_max=r_max, bins=bins, drop_first_bin=True
|
|
391
|
+
)
|
|
392
|
+
if r_ref is None:
|
|
393
|
+
r_ref = r
|
|
394
|
+
else:
|
|
395
|
+
if len(r) != len(r_ref) or np.max(np.abs(r - r_ref)) > 1e-10:
|
|
396
|
+
raise ValueError("R grids differ between frames; fix bins/r_max.")
|
|
397
|
+
stack.append(g)
|
|
398
|
+
|
|
399
|
+
if r_ref is None:
|
|
400
|
+
raise ValueError("No frames selected.")
|
|
401
|
+
|
|
402
|
+
if average:
|
|
403
|
+
return r_ref, np.mean(np.vstack(stack), axis=0)
|
|
404
|
+
if return_stack:
|
|
405
|
+
return r_ref, stack
|
|
406
|
+
return r_ref, stack[-1]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def rdf_using_ovito(
|
|
410
|
+
handler: XmoloutHandler,
|
|
411
|
+
*,
|
|
412
|
+
frames: Optional[Iterable[int]] = None,
|
|
413
|
+
r_max: float = 4.0,
|
|
414
|
+
bins: int = 200,
|
|
415
|
+
types_a: Optional[Iterable[str]] = None, # if both provided => partial
|
|
416
|
+
types_b: Optional[Iterable[str]] = None,
|
|
417
|
+
average: bool = True,
|
|
418
|
+
return_stack: bool = False,
|
|
419
|
+
) -> Tuple[np.ndarray, Union[np.ndarray, List[np.ndarray]]]:
|
|
420
|
+
"""Compute RDFs across multiple frames using the OVITO backend.
|
|
421
|
+
|
|
422
|
+
Works on
|
|
423
|
+
--------
|
|
424
|
+
XmoloutHandler — ``xmolout``
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
frames : iterable of int, optional
|
|
429
|
+
Frame indices to include.
|
|
430
|
+
types_a, types_b : iterable of str, optional
|
|
431
|
+
If provided, compute partial RDF.
|
|
432
|
+
average : bool, default=True
|
|
433
|
+
Return the average RDF across frames.
|
|
434
|
+
|
|
435
|
+
Returns
|
|
436
|
+
-------
|
|
437
|
+
r : np.ndarray
|
|
438
|
+
Radial distances.
|
|
439
|
+
g : np.ndarray or list[np.ndarray]
|
|
440
|
+
RDF data.
|
|
441
|
+
|
|
442
|
+
Examples
|
|
443
|
+
--------
|
|
444
|
+
>>> r, g = rdf_using_ovito(xh, types_a=["Al"], types_b=["N"])
|
|
445
|
+
"""
|
|
446
|
+
df = handler.dataframe()
|
|
447
|
+
if frames is None:
|
|
448
|
+
frames = range(len(df))
|
|
449
|
+
frames = list(frames)
|
|
450
|
+
|
|
451
|
+
do_partial = (types_a is not None) and (types_b is not None)
|
|
452
|
+
if do_partial:
|
|
453
|
+
ta = next(iter(types_a))
|
|
454
|
+
tb = next(iter(types_b))
|
|
455
|
+
|
|
456
|
+
r_ref: np.ndarray | None = None
|
|
457
|
+
stack: List[np.ndarray] = []
|
|
458
|
+
|
|
459
|
+
for i in frames:
|
|
460
|
+
if do_partial:
|
|
461
|
+
r, g = _rdf_ovito_partial_frame(handler, int(i), r_max=r_max, bins=bins, type_a=str(ta), type_b=str(tb))
|
|
462
|
+
else:
|
|
463
|
+
r, g = _rdf_ovito_total_frame(handler, int(i), r_max=r_max, bins=bins)
|
|
464
|
+
|
|
465
|
+
if r_ref is None:
|
|
466
|
+
r_ref = r
|
|
467
|
+
else:
|
|
468
|
+
if len(r) != len(r_ref) or np.max(np.abs(r - r_ref)) > 1e-10:
|
|
469
|
+
raise ValueError("R grids differ between frames; fix bins/r_max.")
|
|
470
|
+
stack.append(g)
|
|
471
|
+
|
|
472
|
+
if r_ref is None:
|
|
473
|
+
raise ValueError("No frames selected.")
|
|
474
|
+
|
|
475
|
+
if average:
|
|
476
|
+
return r_ref, np.mean(np.vstack(stack), axis=0)
|
|
477
|
+
if return_stack:
|
|
478
|
+
return r_ref, stack
|
|
479
|
+
return r_ref, stack[-1]
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def rdf_property_over_frames(
|
|
483
|
+
handler: XmoloutHandler,
|
|
484
|
+
*,
|
|
485
|
+
backend: str = "ovito",
|
|
486
|
+
frames: Optional[Iterable[int]] = None,
|
|
487
|
+
property: str = "first_peak", # 'first_peak' | 'dominant_peak' | 'area' | 'excess_area'
|
|
488
|
+
r_max: Optional[float] = None, # for OVITO total/partial also accepts float
|
|
489
|
+
bins: int = 200,
|
|
490
|
+
types_a: Optional[Iterable[str]] = None,
|
|
491
|
+
types_b: Optional[Iterable[str]] = None,
|
|
492
|
+
) -> pd.DataFrame:
|
|
493
|
+
"""Compute a single RDF-derived property for each simulation frame.
|
|
494
|
+
|
|
495
|
+
Works on
|
|
496
|
+
--------
|
|
497
|
+
XmoloutHandler — ``xmolout``
|
|
498
|
+
|
|
499
|
+
Parameters
|
|
500
|
+
----------
|
|
501
|
+
backend : {"ovito", "freud"}, default="ovito"
|
|
502
|
+
RDF backend to use.
|
|
503
|
+
property : {"first_peak", "dominant_peak", "area", "excess_area"}
|
|
504
|
+
RDF-based property to compute.
|
|
505
|
+
frames : iterable of int, optional
|
|
506
|
+
Frame indices to include.
|
|
507
|
+
types_a, types_b : iterable of str, optional
|
|
508
|
+
Atom types defining partial RDF.
|
|
509
|
+
|
|
510
|
+
Returns
|
|
511
|
+
-------
|
|
512
|
+
pandas.DataFrame
|
|
513
|
+
One row per frame with frame index, iteration, and property values.
|
|
514
|
+
|
|
515
|
+
Examples
|
|
516
|
+
--------
|
|
517
|
+
>>> df = rdf_property_over_frames(xh, property="first_peak")
|
|
518
|
+
"""
|
|
519
|
+
df_sim = handler.dataframe()
|
|
520
|
+
if frames is None:
|
|
521
|
+
frames = range(len(df_sim))
|
|
522
|
+
frames = list(frames)
|
|
523
|
+
|
|
524
|
+
prop = property.strip().lower()
|
|
525
|
+
allowed = {"first_peak", "dominant_peak", "area", "excess_area"}
|
|
526
|
+
if prop not in allowed:
|
|
527
|
+
raise ValueError(f"property must be one of {allowed}")
|
|
528
|
+
|
|
529
|
+
rows: List[Dict[str, float]] = []
|
|
530
|
+
|
|
531
|
+
for i in frames:
|
|
532
|
+
if backend.lower() == "freud":
|
|
533
|
+
r, g = _rdf_freud_frame(handler, int(i), types_a=types_a, types_b=types_b, r_max=r_max, bins=bins)
|
|
534
|
+
elif backend.lower() == "ovito":
|
|
535
|
+
if (types_a is not None) and (types_b is not None):
|
|
536
|
+
ta = next(iter(types_a)); tb = next(iter(types_b))
|
|
537
|
+
r, g = _rdf_ovito_partial_frame(handler, int(i), r_max=float(r_max or 4.0), bins=bins, type_a=str(ta), type_b=str(tb))
|
|
538
|
+
else:
|
|
539
|
+
r, g = _rdf_ovito_total_frame(handler, int(i), r_max=float(r_max or 4.0), bins=bins)
|
|
540
|
+
else:
|
|
541
|
+
raise ValueError("backend must be 'freud' or 'ovito'")
|
|
542
|
+
|
|
543
|
+
if prop == "first_peak":
|
|
544
|
+
rp, gp = _first_local_max(r, g)
|
|
545
|
+
out = {"r_first_peak": rp, "g_first_peak": gp}
|
|
546
|
+
elif prop == "dominant_peak":
|
|
547
|
+
rp, gp = _dominant_peak(r, g)
|
|
548
|
+
out = {"r_peak": rp, "g_peak": gp}
|
|
549
|
+
elif prop == "area":
|
|
550
|
+
out = {"area": float(np.trapezoid(g, r)) if len(r) else np.nan}
|
|
551
|
+
else: # "excess_area"
|
|
552
|
+
out = {"excess_area": float(np.trapezoid(g - 1.0, r)) if len(r) else np.nan}
|
|
553
|
+
|
|
554
|
+
rows.append({
|
|
555
|
+
"frame_index": int(i),
|
|
556
|
+
"iter": int(df_sim.iloc[i]["iter"]) if "iter" in df_sim.columns else int(i),
|
|
557
|
+
**out,
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
return pd.DataFrame(rows).sort_values("frame_index").reset_index(drop=True)
|
|
File without changes
|