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.
Files changed (130) hide show
  1. reaxkit/__init__.py +0 -0
  2. reaxkit/analysis/__init__.py +0 -0
  3. reaxkit/analysis/composed/RDF_analyzer.py +560 -0
  4. reaxkit/analysis/composed/__init__.py +0 -0
  5. reaxkit/analysis/composed/connectivity_analyzer.py +706 -0
  6. reaxkit/analysis/composed/coordination_analyzer.py +144 -0
  7. reaxkit/analysis/composed/electrostatics_analyzer.py +687 -0
  8. reaxkit/analysis/per_file/__init__.py +0 -0
  9. reaxkit/analysis/per_file/control_analyzer.py +165 -0
  10. reaxkit/analysis/per_file/eregime_analyzer.py +108 -0
  11. reaxkit/analysis/per_file/ffield_analyzer.py +305 -0
  12. reaxkit/analysis/per_file/fort13_analyzer.py +79 -0
  13. reaxkit/analysis/per_file/fort57_analyzer.py +106 -0
  14. reaxkit/analysis/per_file/fort73_analyzer.py +61 -0
  15. reaxkit/analysis/per_file/fort74_analyzer.py +65 -0
  16. reaxkit/analysis/per_file/fort76_analyzer.py +191 -0
  17. reaxkit/analysis/per_file/fort78_analyzer.py +154 -0
  18. reaxkit/analysis/per_file/fort79_analyzer.py +83 -0
  19. reaxkit/analysis/per_file/fort7_analyzer.py +393 -0
  20. reaxkit/analysis/per_file/fort99_analyzer.py +411 -0
  21. reaxkit/analysis/per_file/molfra_analyzer.py +359 -0
  22. reaxkit/analysis/per_file/params_analyzer.py +258 -0
  23. reaxkit/analysis/per_file/summary_analyzer.py +84 -0
  24. reaxkit/analysis/per_file/trainset_analyzer.py +84 -0
  25. reaxkit/analysis/per_file/vels_analyzer.py +95 -0
  26. reaxkit/analysis/per_file/xmolout_analyzer.py +528 -0
  27. reaxkit/cli.py +181 -0
  28. reaxkit/count_loc.py +276 -0
  29. reaxkit/data/alias.yaml +89 -0
  30. reaxkit/data/constants.yaml +27 -0
  31. reaxkit/data/reaxff_input_files_contents.yaml +186 -0
  32. reaxkit/data/reaxff_output_files_contents.yaml +301 -0
  33. reaxkit/data/units.yaml +38 -0
  34. reaxkit/help/__init__.py +0 -0
  35. reaxkit/help/help_index_loader.py +531 -0
  36. reaxkit/help/introspection_utils.py +131 -0
  37. reaxkit/io/__init__.py +0 -0
  38. reaxkit/io/base_handler.py +165 -0
  39. reaxkit/io/generators/__init__.py +0 -0
  40. reaxkit/io/generators/control_generator.py +123 -0
  41. reaxkit/io/generators/eregime_generator.py +341 -0
  42. reaxkit/io/generators/geo_generator.py +967 -0
  43. reaxkit/io/generators/trainset_generator.py +1758 -0
  44. reaxkit/io/generators/tregime_generator.py +113 -0
  45. reaxkit/io/generators/vregime_generator.py +164 -0
  46. reaxkit/io/generators/xmolout_generator.py +304 -0
  47. reaxkit/io/handlers/__init__.py +0 -0
  48. reaxkit/io/handlers/control_handler.py +209 -0
  49. reaxkit/io/handlers/eregime_handler.py +122 -0
  50. reaxkit/io/handlers/ffield_handler.py +812 -0
  51. reaxkit/io/handlers/fort13_handler.py +123 -0
  52. reaxkit/io/handlers/fort57_handler.py +143 -0
  53. reaxkit/io/handlers/fort73_handler.py +145 -0
  54. reaxkit/io/handlers/fort74_handler.py +155 -0
  55. reaxkit/io/handlers/fort76_handler.py +195 -0
  56. reaxkit/io/handlers/fort78_handler.py +142 -0
  57. reaxkit/io/handlers/fort79_handler.py +227 -0
  58. reaxkit/io/handlers/fort7_handler.py +264 -0
  59. reaxkit/io/handlers/fort99_handler.py +128 -0
  60. reaxkit/io/handlers/geo_handler.py +224 -0
  61. reaxkit/io/handlers/molfra_handler.py +184 -0
  62. reaxkit/io/handlers/params_handler.py +137 -0
  63. reaxkit/io/handlers/summary_handler.py +135 -0
  64. reaxkit/io/handlers/trainset_handler.py +658 -0
  65. reaxkit/io/handlers/vels_handler.py +293 -0
  66. reaxkit/io/handlers/xmolout_handler.py +174 -0
  67. reaxkit/utils/__init__.py +0 -0
  68. reaxkit/utils/alias.py +219 -0
  69. reaxkit/utils/cache.py +77 -0
  70. reaxkit/utils/constants.py +75 -0
  71. reaxkit/utils/equation_of_states.py +96 -0
  72. reaxkit/utils/exceptions.py +27 -0
  73. reaxkit/utils/frame_utils.py +175 -0
  74. reaxkit/utils/log.py +43 -0
  75. reaxkit/utils/media/__init__.py +0 -0
  76. reaxkit/utils/media/convert.py +90 -0
  77. reaxkit/utils/media/make_video.py +91 -0
  78. reaxkit/utils/media/plotter.py +812 -0
  79. reaxkit/utils/numerical/__init__.py +0 -0
  80. reaxkit/utils/numerical/extrema_finder.py +96 -0
  81. reaxkit/utils/numerical/moving_average.py +103 -0
  82. reaxkit/utils/numerical/numerical_calcs.py +75 -0
  83. reaxkit/utils/numerical/signal_ops.py +135 -0
  84. reaxkit/utils/path.py +55 -0
  85. reaxkit/utils/units.py +104 -0
  86. reaxkit/webui/__init__.py +0 -0
  87. reaxkit/webui/app.py +0 -0
  88. reaxkit/webui/components.py +0 -0
  89. reaxkit/webui/layouts.py +0 -0
  90. reaxkit/webui/utils.py +0 -0
  91. reaxkit/workflows/__init__.py +0 -0
  92. reaxkit/workflows/composed/__init__.py +0 -0
  93. reaxkit/workflows/composed/coordination_workflow.py +393 -0
  94. reaxkit/workflows/composed/electrostatics_workflow.py +587 -0
  95. reaxkit/workflows/composed/xmolout_fort7_workflow.py +343 -0
  96. reaxkit/workflows/meta/__init__.py +0 -0
  97. reaxkit/workflows/meta/help_workflow.py +136 -0
  98. reaxkit/workflows/meta/introspection_workflow.py +235 -0
  99. reaxkit/workflows/meta/make_video_workflow.py +61 -0
  100. reaxkit/workflows/meta/plotter_workflow.py +601 -0
  101. reaxkit/workflows/per_file/__init__.py +0 -0
  102. reaxkit/workflows/per_file/control_workflow.py +110 -0
  103. reaxkit/workflows/per_file/eregime_workflow.py +267 -0
  104. reaxkit/workflows/per_file/ffield_workflow.py +390 -0
  105. reaxkit/workflows/per_file/fort13_workflow.py +86 -0
  106. reaxkit/workflows/per_file/fort57_workflow.py +137 -0
  107. reaxkit/workflows/per_file/fort73_workflow.py +151 -0
  108. reaxkit/workflows/per_file/fort74_workflow.py +88 -0
  109. reaxkit/workflows/per_file/fort76_workflow.py +188 -0
  110. reaxkit/workflows/per_file/fort78_workflow.py +135 -0
  111. reaxkit/workflows/per_file/fort79_workflow.py +314 -0
  112. reaxkit/workflows/per_file/fort7_workflow.py +592 -0
  113. reaxkit/workflows/per_file/fort83_workflow.py +60 -0
  114. reaxkit/workflows/per_file/fort99_workflow.py +223 -0
  115. reaxkit/workflows/per_file/geo_workflow.py +554 -0
  116. reaxkit/workflows/per_file/molfra_workflow.py +577 -0
  117. reaxkit/workflows/per_file/params_workflow.py +135 -0
  118. reaxkit/workflows/per_file/summary_workflow.py +161 -0
  119. reaxkit/workflows/per_file/trainset_workflow.py +356 -0
  120. reaxkit/workflows/per_file/tregime_workflow.py +79 -0
  121. reaxkit/workflows/per_file/vels_workflow.py +309 -0
  122. reaxkit/workflows/per_file/vregime_workflow.py +75 -0
  123. reaxkit/workflows/per_file/xmolout_workflow.py +678 -0
  124. reaxkit-1.0.0.dist-info/METADATA +128 -0
  125. reaxkit-1.0.0.dist-info/RECORD +130 -0
  126. reaxkit-1.0.0.dist-info/WHEEL +5 -0
  127. reaxkit-1.0.0.dist-info/entry_points.txt +2 -0
  128. reaxkit-1.0.0.dist-info/licenses/AUTHORS.md +20 -0
  129. reaxkit-1.0.0.dist-info/licenses/LICENSE +21 -0
  130. 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