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
@@ -0,0 +1,84 @@
1
+ """
2
+ trainset (ReaxFF training-set) analysis utilities.
3
+
4
+ This module provides helper functions for extracting metadata and
5
+ human-readable annotations from ReaxFF ``trainset`` files via
6
+ ``TrainsetHandler``.
7
+
8
+ Typical use cases include:
9
+
10
+ - listing unique group comments defined across training sections
11
+ - auditing how training targets are grouped and documented
12
+ - preparing summaries of training-set structure for reporting
13
+ """
14
+
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any
19
+ import pandas as pd
20
+
21
+ from reaxkit.io.handlers.trainset_handler import TrainsetHandler
22
+
23
+
24
+ def get_trainset_group_comments(handler: TrainsetHandler, *, sort: bool = False) -> pd.DataFrame:
25
+ """
26
+ Collect unique group comments from a ReaxFF training-set file.
27
+
28
+ Each group comment is returned together with the training section
29
+ it belongs to.
30
+
31
+ Works on
32
+ --------
33
+ TrainsetHandler — ``trainset``
34
+
35
+ Parameters
36
+ ----------
37
+ handler : TrainsetHandler
38
+ Parsed trainset handler with metadata and section tables.
39
+ sort : bool, default=False
40
+ If True, sort the result by ``section`` and ``group_comment``.
41
+ If False, preserve the original appearance order.
42
+
43
+ Returns
44
+ -------
45
+ pandas.DataFrame
46
+ Table with columns:
47
+ ``section`` — training section name
48
+ ``group_comment`` — unique group annotation text
49
+
50
+ Examples
51
+ --------
52
+ >>> from reaxkit.io.handlers.trainset_handler import TrainsetHandler
53
+ >>> from reaxkit.analysis.per_file.trainset_analyzer import get_trainset_group_comments
54
+ >>> h = TrainsetHandler("trainset")
55
+ >>> df = get_trainset_group_comments(h, sort=True)
56
+ """
57
+ meta: dict[str, Any] = handler.metadata()
58
+ tables: dict[str, pd.DataFrame] = meta.get("tables", {})
59
+
60
+ rows: list[dict[str, str]] = []
61
+
62
+ for section_name, df in tables.items():
63
+ if "group_comment" not in df.columns:
64
+ continue
65
+
66
+ # keep only non-empty comments
67
+ series = df["group_comment"].astype(str).str.strip()
68
+ series = series[series != ""]
69
+
70
+ for gc in series.unique():
71
+ rows.append(
72
+ {
73
+ "section": section_name.lower(), # or keep as-is
74
+ "group_comment": gc,
75
+ }
76
+ )
77
+
78
+ result = pd.DataFrame(rows).drop_duplicates()
79
+
80
+ # Apply sort only if requested
81
+ if sort and not result.empty:
82
+ result = result.sort_values(["section", "group_comment"], ignore_index=True)
83
+
84
+ return result
@@ -0,0 +1,95 @@
1
+ """
2
+ vels / moldyn.vel / molsav analysis utilities.
3
+
4
+ This module provides a unified interface for accessing atomic coordinates,
5
+ velocities, and accelerations stored in ReaxFF velocity output files via
6
+ ``VelsHandler``.
7
+
8
+ Typical use cases include:
9
+
10
+ - retrieving atomic coordinates or velocities for selected atoms
11
+ - extracting acceleration histories for diagnostics or plotting
12
+ - accessing file metadata (timestep, atom count, sections present)
13
+ """
14
+
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Literal, Sequence
19
+
20
+ import pandas as pd
21
+
22
+ from reaxkit.io.handlers.vels_handler import VelsHandler
23
+
24
+
25
+ VelsKey = Literal[
26
+ "metadata",
27
+ "coordinates",
28
+ "velocities",
29
+ "accelerations",
30
+ "prev_accelerations",
31
+ ]
32
+
33
+
34
+ def get_vels_data(
35
+ handler: VelsHandler,
36
+ key: VelsKey,
37
+ *,
38
+ atoms: Sequence[int] | None = None,
39
+ ) -> pd.DataFrame | dict:
40
+ """
41
+ Retrieve metadata or a selected atomic table from a velocity output file.
42
+
43
+ Works on
44
+ --------
45
+ VelsHandler — ``vels`` / ``moldyn.vel`` / ``molsav``
46
+
47
+ Parameters
48
+ ----------
49
+ handler : VelsHandler
50
+ Parsed velocity file handler.
51
+ key : {"metadata", "coordinates", "velocities", "accelerations", "prev_accelerations"}
52
+ Section to retrieve:
53
+ - ``metadata``: file-level information (returned as a dict)
54
+ - ``coordinates``: atomic positions
55
+ - ``velocities``: atomic velocities
56
+ - ``accelerations``: atomic accelerations
57
+ - ``prev_accelerations``: accelerations from the previous step
58
+ atoms : sequence of int, optional
59
+ 1-based atom indices to include. If None, all atoms are returned.
60
+ Ignored when ``key="metadata"``.
61
+
62
+ Returns
63
+ -------
64
+ pandas.DataFrame or dict
65
+ If ``key="metadata"``, returns a metadata dictionary.
66
+ Otherwise, returns a DataFrame with one row per atom containing the
67
+ requested quantities and an ``atom_index`` column.
68
+
69
+ Examples
70
+ --------
71
+ >>> from reaxkit.io.handlers.vels_handler import VelsHandler
72
+ >>> from reaxkit.analysis.per_file.vels_analyzer import get_vels_data
73
+ >>> h = VelsHandler("moldyn.vel")
74
+ >>> v = get_vels_data(h, "velocities", atoms=[1, 2, 3])
75
+ >>> meta = get_vels_data(h, "metadata")
76
+ """
77
+ if key == "metadata":
78
+ return handler.metadata()
79
+
80
+ if key == "coordinates":
81
+ df = handler.section_df(handler.SECTION_COORDS).copy()
82
+ elif key == "velocities":
83
+ df = handler.section_df(handler.SECTION_VELS).copy()
84
+ elif key == "accelerations":
85
+ df = handler.section_df(handler.SECTION_ACCELS).copy()
86
+ elif key == "prev_accelerations":
87
+ df = handler.section_df(handler.SECTION_PREV_ACCELS).copy()
88
+ else:
89
+ raise ValueError(f"Unknown vels key: {key}")
90
+
91
+ if atoms:
92
+ # 1-based indices expected (matches how handler stores atom_index)
93
+ df = df[df["atom_index"].isin(list(atoms))].copy()
94
+
95
+ return df.reset_index(drop=True)
@@ -0,0 +1,528 @@
1
+ """
2
+ xmolout trajectory analysis utilities.
3
+
4
+ This module provides atomistic and trajectory-level analysis tools for
5
+ ReaxFF ``xmolout`` files via ``XmoloutHandler``.
6
+
7
+ It supports extraction of atom properties, trajectories, simulation box
8
+ information, displacement metrics, atom-type mappings, and radial
9
+ distribution functions (RDFs) using multiple backends.
10
+
11
+ Typical use cases include:
12
+
13
+ - exporting per-atom coordinates or properties across frames
14
+ - building atom trajectories in long or wide format
15
+ - tracking box dimensions and thermodynamic scalars over time
16
+ - computing mean-squared displacement (MSD)
17
+ - computing total or partial RDFs and RDF-derived properties
18
+ """
19
+
20
+
21
+ from __future__ import annotations
22
+ from typing import Iterable, Optional, Sequence, Union, Dict, Any, List
23
+ import numpy as np
24
+ import pandas as pd
25
+
26
+ from reaxkit.io.handlers.xmolout_handler import XmoloutHandler
27
+ from reaxkit.utils.frame_utils import select_frames as _df_select
28
+
29
+ from reaxkit.analysis.composed.RDF_analyzer import (
30
+ rdf_using_freud as _rdf_freud_many,
31
+ rdf_using_ovito as _rdf_ovito_many,
32
+ rdf_property_over_frames as _rdf_props,
33
+ )
34
+
35
+ # ==========================================================
36
+ # === non-RDF helpers (atom tables/trajectories/box) ===
37
+ # ==========================================================
38
+
39
+ FrameSel = Optional[Union[Sequence[int], range, slice]]
40
+ AtomSel = Optional[Union[Sequence[int], slice]]
41
+
42
+ BASE_ATOM_COLS = ("atom_type", "x", "y", "z")
43
+
44
+ def _frame_table(xh: XmoloutHandler, i: int) -> pd.DataFrame:
45
+ """Internal helper: return a DataFrame for a specific frame index from an XmoloutHandler.
46
+
47
+ If pre-parsed frames are cached in `xh._frames`, retrieves directly from cache.
48
+ Otherwise, loads the frame via `xh.frame(i)` and constructs a DataFrame with:
49
+ - atom_type
50
+ - x, y, z coordinates
51
+
52
+ Used internally by higher-level utilities (e.g., atom table extraction,
53
+ per-frame analysis, or visualization routines).
54
+ """
55
+ if hasattr(xh, "_frames") and i < len(xh._frames):
56
+ return xh._frames[i]
57
+ fr = xh.frame(i)
58
+ return pd.DataFrame({
59
+ "atom_type": fr["atom_types"],
60
+ "x": fr["coords"][:, 0], "y": fr["coords"][:, 1], "z": fr["coords"][:, 2],
61
+ })
62
+
63
+ def _get_xmolout_data_per_atom(
64
+ xh: XmoloutHandler,
65
+ *,
66
+ frames: FrameSel = None,
67
+ every: int = 1,
68
+ atoms: AtomSel = None,
69
+ atom_types: Optional[Sequence[str]] = None,
70
+ extra_cols: Optional[Sequence[str]] = None,
71
+ include_xyz: bool = True,
72
+ format: str = "long",
73
+ ) -> pd.DataFrame:
74
+ """Extract per-atom properties across selected frames.
75
+
76
+ Works on
77
+ --------
78
+ XmoloutHandler — ``xmolout``
79
+
80
+ Parameters
81
+ ----------
82
+ xh : XmoloutHandler
83
+ Parsed xmolout handler.
84
+ frames : int, slice, or sequence of int, optional
85
+ Frame indices to include.
86
+ every : int, default=1
87
+ Subsample frames by taking every Nth frame.
88
+ atoms : sequence of int or slice, optional
89
+ Atom indices to include (0-based internally, exported as 1-based).
90
+ atom_types : sequence of str, optional
91
+ Atom types to include (e.g. ``["Al", "N"]``).
92
+ extra_cols : sequence of str, optional
93
+ Additional per-atom columns to include if present.
94
+ include_xyz : bool, default=True
95
+ Whether to include ``x``, ``y``, ``z`` coordinates.
96
+ format : {"long", "wide"}, default="long"
97
+ Output format.
98
+
99
+ Returns
100
+ -------
101
+ pandas.DataFrame
102
+ Atom-level table in long or wide format, including frame and iteration
103
+ metadata.
104
+
105
+ Examples
106
+ --------
107
+ >>> df = _get_xmolout_data_per_atom(xh, frames=slice(0, 10), atom_types=["O"])
108
+ """
109
+ df_sim = xh.dataframe()
110
+ sub_df = _df_select(df_sim, frames)
111
+ fidx_all = list(sub_df.index)[::max(1, int(every))]
112
+
113
+ rows: list[dict[str, Any]] = []
114
+ for i in fidx_all:
115
+ ft = _frame_table(xh, i)
116
+ if atoms is not None:
117
+ if isinstance(atoms, slice):
118
+ atom_sel = list(range(*atoms.indices(len(ft))))
119
+ else:
120
+ atom_sel = [int(a) for a in atoms if 0 <= int(a) < len(ft)]
121
+ elif atom_types:
122
+ tset = {str(t) for t in atom_types}
123
+ atom_sel = [j for j, t in enumerate(ft["atom_type"].astype(str)) if t in tset]
124
+ else:
125
+ atom_sel = list(range(len(ft)))
126
+
127
+ extras_here = [c for c in ft.columns if c not in BASE_ATOM_COLS]
128
+ wanted = extras_here if extra_cols is None else [c for c in extra_cols if c in ft.columns]
129
+ vals_cols = (["x", "y", "z"] if include_xyz else []) + wanted
130
+
131
+ for j in atom_sel:
132
+ rec = {
133
+ "frame_index": int(i),
134
+ "iter": int(df_sim.iloc[i]["iter"]) if "iter" in df_sim.columns else int(i),
135
+ "atom_id": int(j) + 1, #1-based atom format
136
+ "atom_type": str(ft.at[j, "atom_type"]),
137
+ }
138
+ for c in vals_cols:
139
+ rec[c] = ft.at[j, c] if c in ft.columns else np.nan
140
+ rows.append(rec)
141
+
142
+ out = pd.DataFrame(rows).sort_values(["frame_index", "atom_id"]).reset_index(drop=True)
143
+
144
+ if format == "long":
145
+ return out
146
+
147
+ if format == "wide":
148
+ id_cols = ["frame_index", "iter"]
149
+ value_cols = [c for c in out.columns if c not in (id_cols + ["atom_id", "atom_type"])]
150
+ wide = out[id_cols + ["atom_id"] + value_cols].pivot(index=id_cols, columns="atom_id", values=value_cols)
151
+ wide.columns = [f"{col}[{aid}]" for (col, aid) in wide.columns.to_flat_index()]
152
+ return wide.reset_index().sort_values("frame_index").reset_index(drop=True)
153
+
154
+ raise ValueError("format must be 'long' or 'wide'")
155
+
156
+ def get_unit_cell_dimensions_across_frames(
157
+ xh: XmoloutHandler,
158
+ *,
159
+ frames: FrameSel = None,
160
+ every: int = 1,
161
+ ) -> pd.DataFrame:
162
+ """
163
+ Extract simulation box dimensions and scalar quantities per frame.
164
+
165
+ Works on
166
+ --------
167
+ XmoloutHandler — ``xmolout``
168
+
169
+ Parameters
170
+ ----------
171
+ xh : XmoloutHandler
172
+ Parsed xmolout handler.
173
+ frames : int, slice, or sequence of int, optional
174
+ Frame indices to include.
175
+ every : int, default=1
176
+ Subsample frames by taking every Nth frame.
177
+
178
+ Returns
179
+ -------
180
+ pandas.DataFrame
181
+ Table with columns such as ``a``, ``b``, ``c``, ``alpha``, ``beta``,
182
+ ``gamma``, ``E_pot``, and ``num_of_atoms``.
183
+
184
+ Examples
185
+ --------
186
+ >>> df = get_unit_cell_dimensions_across_frames(xh)
187
+ """
188
+ df_sim = xh.dataframe()
189
+ sub_df = _df_select(df_sim, frames)
190
+ fidx = list(sub_df.index)[::max(1, int(every))]
191
+
192
+ if df_sim.empty:
193
+ return pd.DataFrame(columns=["frame_index", "iter", "a", "b", "c",
194
+ "alpha", "beta", "gamma", "E_pot", "num_of_atoms"])
195
+
196
+ out = df_sim.iloc[fidx].copy().reset_index(drop=True)
197
+ out.insert(0, "frame_index", fidx)
198
+ wanted = ["frame_index", "iter", "a", "b", "c", "alpha", "beta", "gamma", "E_pot", "num_of_atoms"]
199
+ return out[[c for c in wanted if c in out.columns]]
200
+
201
+
202
+ def get_atom_trajectories(
203
+ xh: XmoloutHandler,
204
+ *,
205
+ frames: FrameSel = None,
206
+ every: int = 1,
207
+ atoms: AtomSel = None, # indices or slice
208
+ atom_types: Optional[Sequence[str]] = None,
209
+ dims: Sequence[str] = ("x", "y", "z"),
210
+ format: str = "long", # 'long' or 'wide'
211
+ ) -> pd.DataFrame:
212
+ """Extract atomic trajectories across frames.
213
+
214
+ Works on
215
+ --------
216
+ XmoloutHandler — ``xmolout``
217
+
218
+ Parameters
219
+ ----------
220
+ xh : XmoloutHandler
221
+ Parsed xmolout handler.
222
+ frames : int, slice, or sequence of int, optional
223
+ Frame indices to include.
224
+ every : int, default=1
225
+ Subsample frames by taking every Nth frame.
226
+ atoms : sequence of int or slice, optional
227
+ Atom indices to include.
228
+ atom_types : sequence of str, optional
229
+ Atom types to include.
230
+ dims : sequence of {"x", "y", "z"}, default=("x","y","z")
231
+ Coordinate components to extract.
232
+ format : {"long", "wide"}, default="long"
233
+ Output format.
234
+
235
+ Returns
236
+ -------
237
+ pandas.DataFrame
238
+ Atom trajectories in long or wide format.
239
+
240
+ Examples
241
+ --------
242
+ >>> df = get_atom_trajectories(xh, atoms=[1, 2], dims=("z",))
243
+ """
244
+ dims = tuple(d for d in dims if d in ("x", "y", "z"))
245
+ if not dims:
246
+ raise ValueError("dims must include at least one of 'x','y','z'")
247
+
248
+ df_sim = xh.dataframe()
249
+ sub_df = _df_select(df_sim, frames)
250
+ fidx = list(sub_df.index)[::max(1, int(every))]
251
+
252
+ rows: List[Dict[str, Any]] = []
253
+ for i in fidx:
254
+ fr = xh.frame(i)
255
+ coords = fr["coords"]
256
+
257
+ # selection
258
+ if atoms is not None:
259
+ if isinstance(atoms, slice):
260
+ atom_sel = list(range(*atoms.indices(coords.shape[0])))
261
+ else:
262
+ atom_sel = [int(a) for a in atoms if 0 <= int(a) < coords.shape[0]]
263
+ elif atom_types:
264
+ tset = {str(t) for t in atom_types}
265
+ atom_sel = [j for j, t in enumerate(fr["atom_types"]) if str(t) in tset]
266
+ else:
267
+ atom_sel = list(range(coords.shape[0]))
268
+
269
+ for j in atom_sel:
270
+ rec = {
271
+ "frame_index": int(i),
272
+ "iter": int(fr.get("iter", i)),
273
+ "atom_id": int(j) + 1,
274
+ "atom_type": str(fr["atom_types"][j]),
275
+ }
276
+ if "x" in dims: rec["x"] = float(coords[j, 0])
277
+ if "y" in dims: rec["y"] = float(coords[j, 1])
278
+ if "z" in dims: rec["z"] = float(coords[j, 2])
279
+ rows.append(rec)
280
+
281
+ out = pd.DataFrame(rows).sort_values(["frame_index", "atom_id"]).reset_index(drop=True)
282
+ if format == "long":
283
+ return out
284
+
285
+ if format == "wide":
286
+ val_cols = [d for d in ("x", "y", "z") if d in dims]
287
+ to_pivot = out[["frame_index", "iter", "atom_id"] + val_cols]
288
+ wide = to_pivot.pivot(index=["frame_index", "iter"], columns="atom_id", values=val_cols)
289
+ wide.columns = [f"{d}[{aid}]" for (d, aid) in wide.columns.to_flat_index()]
290
+ return wide.reset_index().sort_values("frame_index").reset_index(drop=True)
291
+
292
+ raise ValueError("format must be 'long' or 'wide'")
293
+
294
+
295
+ def get_mean_squared_displacement(
296
+ xh: XmoloutHandler,
297
+ *,
298
+ frames: FrameSel = None,
299
+ every: int = 1,
300
+ atoms: AtomSel = None,
301
+ atom_types: Optional[Sequence[str]] = None,
302
+ dims: Sequence[str] = ("x", "y", "z"),
303
+ origin: str = "first", # 'first' or an int frame index inside selection
304
+ ) -> pd.DataFrame:
305
+ """Compute per-atom mean-squared displacement (MSD) without PBC unwrapping.
306
+
307
+ Works on
308
+ --------
309
+ XmoloutHandler — ``xmolout``
310
+
311
+ Parameters
312
+ ----------
313
+ xh : XmoloutHandler
314
+ Parsed xmolout handler.
315
+ frames : int, slice, or sequence of int, optional
316
+ Frames over which MSD is computed.
317
+ every : int, default=1
318
+ Subsample frames by taking every Nth frame.
319
+ atoms : sequence of int, optional
320
+ Atom indices to include.
321
+ atom_types : sequence of str, optional
322
+ Atom types to include.
323
+ dims : sequence of {"x","y","z"}, default=("x","y","z")
324
+ Coordinate components used for displacement.
325
+ origin : {"first"} or int, default="first"
326
+ Reference frame for displacement.
327
+
328
+ Returns
329
+ -------
330
+ pandas.DataFrame
331
+ Long-format table with columns ``frame_index``, ``iter``,
332
+ ``atom_id``, and ``msd``.
333
+
334
+ Examples
335
+ --------
336
+ >>> df = get_mean_squared_displacement(xh, atom_types=["O"])
337
+ """
338
+ dims = tuple(d for d in dims if d in ("x", "y", "z"))
339
+ if not dims:
340
+ raise ValueError("dims must include at least one of 'x','y','z'")
341
+
342
+ df_sim = xh.dataframe()
343
+ sub_df = _df_select(df_sim, frames)
344
+ fidx = list(sub_df.index)[::max(1, int(every))]
345
+ if not fidx:
346
+ return pd.DataFrame(columns=["frame_index", "iter", "atom_id", "msd"])
347
+
348
+ # Choose reference frame in the selection
349
+ ref_frame = fidx[0] if origin == "first" else int(origin)
350
+ if ref_frame not in fidx:
351
+ raise ValueError("origin must be 'first' or a frame index inside the selected frames")
352
+
353
+ # Build selection of atoms using the reference frame
354
+ fr0 = xh.frame(ref_frame)
355
+ coords0 = fr0["coords"]
356
+
357
+ if atoms is not None:
358
+ # interpret `atoms` as 1-based indices
359
+ if isinstance(atoms, slice):
360
+ # if user passes a slice, assume 0-based like Python
361
+ sel = list(range(*atoms.indices(coords0.shape[0])))
362
+ else:
363
+ sel = []
364
+ for a in atoms:
365
+ ai = int(a)
366
+ if 1 <= ai <= coords0.shape[0]:
367
+ sel.append(ai - 1) # convert 1-based -> 0-based
368
+ elif atom_types:
369
+ tset = {str(t) for t in atom_types}
370
+ sel = [j for j, t in enumerate(fr0["atom_types"]) if str(t) in tset]
371
+ else:
372
+ sel = list(range(coords0.shape[0]))
373
+
374
+ if not sel:
375
+ return pd.DataFrame(columns=["frame_index", "iter", "atom_id", "msd"])
376
+
377
+ axes = {"x": 0, "y": 1, "z": 2}
378
+ use_cols = [axes[d] for d in dims]
379
+
380
+ sel_idx = np.asarray(sel, dtype=int) # 0-based indices
381
+ atom_ids = (sel_idx + 1).tolist() # 1-based atom ids
382
+ r0 = coords0[sel_idx[:, None], use_cols].astype(float) # (n_sel, len(dims))
383
+
384
+ rows: List[Dict[str, Any]] = []
385
+ for i in fidx:
386
+ fr = xh.frame(i)
387
+ coords = fr["coords"][sel_idx[:, None], use_cols].astype(float)
388
+ dr = coords - r0 # (n_sel, len(dims))
389
+ sq = np.sum(dr * dr, axis=1) # per-atom MSD, shape (n_sel,)
390
+
391
+ iter_val = int(fr.get("iter", i))
392
+ for atom_id, msd_val in zip(atom_ids, sq):
393
+ rows.append(
394
+ {
395
+ "frame_index": int(i),
396
+ "iter": iter_val,
397
+ "atom_id": int(atom_id),
398
+ "msd": float(msd_val),
399
+ }
400
+ )
401
+
402
+ return (
403
+ pd.DataFrame(rows)
404
+ .sort_values(["frame_index", "atom_id"])
405
+ .reset_index(drop=True)
406
+ )
407
+
408
+
409
+
410
+ def get_atom_type_mapping(
411
+ xh: XmoloutHandler,
412
+ frame: int = 0,
413
+ ) -> Dict[str, Any]:
414
+ """Return atom-type mappings for a given frame.
415
+
416
+ Works on
417
+ --------
418
+ XmoloutHandler — ``xmolout``
419
+
420
+ Parameters
421
+ ----------
422
+ xh : XmoloutHandler
423
+ Parsed xmolout handler.
424
+ frame : int, default=0
425
+ Frame index to inspect.
426
+
427
+ Returns
428
+ -------
429
+ dict
430
+ Mapping containing:
431
+ - ``types``: sorted unique atom types
432
+ - ``type_to_indices``: atom indices per type (1-based)
433
+ - ``index_to_type``: per-atom type list
434
+
435
+ Examples
436
+ --------
437
+ >>> m = get_atom_type_mapping(xh)
438
+ >>> m["types"]
439
+ """
440
+ fr = xh.frame(int(frame))
441
+ types = [str(t) for t in fr["atom_types"]]
442
+ uniq = sorted(set(types))
443
+ type_to_indices: Dict[str, List[int]] = {t: [] for t in uniq}
444
+ for idx, t in enumerate(types, start=1): # start=1 for 1-based numbering
445
+ type_to_indices[t].append(idx)
446
+ return {
447
+ "types": uniq,
448
+ "type_to_indices": type_to_indices,
449
+ "index_to_type": types,
450
+ }
451
+
452
+
453
+ # ==========================================================
454
+ # ========== Single public entry for RDF & properties ======
455
+ # ==========================================================
456
+
457
+ def get_radial_dist_fnc(
458
+ xh: XmoloutHandler,
459
+ *,
460
+ backend: str = "freud", # 'freud' or 'ovito'
461
+ frames: Optional[Iterable[int]] = None, # frame indices; None => all
462
+ types_a: Optional[Iterable[str]] = None,
463
+ types_b: Optional[Iterable[str]] = None,
464
+ r_max: Optional[float] = None, # OVITO accepts float; FREUD accepts Optional[float]
465
+ bins: int = 200,
466
+ property: Optional[str] = None, # None => return RDF curve; else 'first_peak'|'dominant_peak'|'area'|'excess_area'
467
+ average: bool = True, # for curves: average across frames
468
+ return_stack: bool = False # for curves: if average=False, optionally return list of per-frame g(r)
469
+ ):
470
+ """Compute radial distribution functions (RDFs) or RDF-derived properties.
471
+
472
+ Works on
473
+ --------
474
+ XmoloutHandler — ``xmolout``
475
+
476
+ Parameters
477
+ ----------
478
+ xh : XmoloutHandler
479
+ Parsed xmolout handler.
480
+ backend : {"freud", "ovito"}, default="freud"
481
+ RDF backend to use.
482
+ frames : iterable of int, optional
483
+ Frame indices to include.
484
+ types_a, types_b : iterable of str, optional
485
+ Atom types defining a partial RDF.
486
+ r_max : float, optional
487
+ Maximum radius cutoff.
488
+ bins : int, default=200
489
+ Number of RDF bins.
490
+ property : str, optional
491
+ RDF-derived quantity (e.g. ``first_peak``, ``dominant_peak``,
492
+ ``area``, ``excess_area``).
493
+ average : bool, default=True
494
+ Average RDF curves across frames.
495
+ return_stack : bool, default=False
496
+ Return per-frame RDF curves when not averaging.
497
+
498
+ Returns
499
+ -------
500
+ numpy.ndarray, tuple, or pandas.DataFrame
501
+ RDF curves or per-frame RDF-derived properties.
502
+
503
+ Examples
504
+ --------
505
+ >>> r, g = get_radial_dist_fnc(xh, types_a=["Al"], types_b=["N"])
506
+ >>> df = get_radial_dist_fnc(xh, property="first_peak")
507
+ """
508
+
509
+
510
+ if property is None:
511
+ if backend.lower() == "freud":
512
+ return _rdf_freud_many(
513
+ xh, frames=frames, types_a=types_a, types_b=types_b,
514
+ r_max=r_max, bins=bins, average=average, return_stack=return_stack
515
+ )
516
+ elif backend.lower() == "ovito":
517
+ return _rdf_ovito_many(
518
+ xh, frames=frames, r_max=float(r_max or 4.0), bins=bins,
519
+ types_a=types_a, types_b=types_b, average=average, return_stack=return_stack
520
+ )
521
+ else:
522
+ raise ValueError("backend must be 'freud' or 'ovito'")
523
+
524
+ # property mode
525
+ return _rdf_props(
526
+ xh, backend=backend, frames=frames, property=property,
527
+ r_max=r_max, bins=bins, types_a=types_a, types_b=types_b
528
+ )