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,678 @@
1
+ """
2
+ xmolout trajectory-analysis workflow for ReaxKit.
3
+
4
+ This workflow provides a comprehensive set of tools for extracting, analyzing,
5
+ and visualizing atomic trajectory data from ReaxFF `xmolout` files, which store
6
+ time-resolved atomic coordinates and simulation cell information.
7
+
8
+ It supports:
9
+ - Extracting atom trajectories for selected atoms or atom types in long or wide
10
+ tabular formats, with flexible frame selection and axis conversion.
11
+ - Computing mean squared displacement (MSD) for one or multiple atoms, with
12
+ support for combined plots or per-atom subplots.
13
+ - Computing radial distribution functions (RDFs) using FREUD or OVITO backends,
14
+ either as averaged curves or as RDF-derived properties over frames.
15
+ - Extracting simulation box (cell) dimensions as functions of frame, iteration,
16
+ or time, with optional plotting.
17
+ - Writing trimmed `xmolout` files that retain only atom types and coordinates
18
+ for lightweight storage or downstream processing.
19
+
20
+ The workflow is designed to bridge raw ReaxFF trajectory output with common
21
+ structural and dynamical analyses in a reproducible, CLI-driven manner.
22
+ """
23
+
24
+
25
+ import argparse
26
+ from typing import Optional, Iterable
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from reaxkit.io.handlers.xmolout_handler import XmoloutHandler
31
+ from reaxkit.utils.path import resolve_output_path
32
+ from reaxkit.io.generators.xmolout_generator import write_xmolout_from_handler
33
+ from reaxkit.utils.media.plotter import single_plot, multi_subplots
34
+ from reaxkit.utils.frame_utils import (
35
+ _select_frames,
36
+ parse_frames as _parse_frames,
37
+ parse_atoms,
38
+ )
39
+
40
+ from reaxkit.analysis.per_file.xmolout_analyzer import (
41
+ get_mean_squared_displacement,
42
+ get_unit_cell_dimensions_across_frames,
43
+ get_atom_trajectories,
44
+ )
45
+ from reaxkit.analysis.composed.RDF_analyzer import (
46
+ rdf_using_freud,
47
+ rdf_using_ovito,
48
+ rdf_property_over_frames,
49
+ )
50
+ from reaxkit.utils.media.convert import convert_xaxis
51
+ from reaxkit.utils.units import unit_for
52
+
53
+ def _parse_types(s: Optional[str]):
54
+ if s is None or str(s).strip() == "":
55
+ return None
56
+ parts = [p for chunk in str(s).split(",") for p in chunk.split()]
57
+ return set(parts)
58
+
59
+ def _frames_from_args(xh: XmoloutHandler, args: argparse.Namespace) -> Iterable[int]:
60
+ if getattr(args, "frames", None):
61
+ sel = _parse_frames(args.frames)
62
+ if sel is None:
63
+ try:
64
+ return range(xh.n_frames())
65
+ except Exception:
66
+ return range(len(xh.dataframe()))
67
+ if isinstance(sel, slice):
68
+ try:
69
+ n = xh.n_frames()
70
+ except Exception:
71
+ n = len(xh.dataframe())
72
+ s, e, st = sel.indices(n)
73
+ return range(s, e, st if st != 0 else 1)
74
+ return [int(i) for i in sel]
75
+ return _select_frames(xh, getattr(args, "start", None), getattr(args, "stop", None), getattr(args, "every", 1))
76
+
77
+ # --------------------------
78
+ # TASK IMPLEMENTATIONS
79
+ # --------------------------
80
+ def _trajget_task(args: argparse.Namespace) -> int:
81
+ xh = XmoloutHandler(args.file)
82
+ frames = _frames_from_args(xh, args)
83
+
84
+ # atoms: optional, 1-based → 0-based
85
+ atoms = parse_atoms(args.atoms)
86
+ if atoms is not None:
87
+ atoms = [a - 1 for a in atoms]
88
+
89
+ # atom types
90
+ types = _parse_types(args.atom_types)
91
+
92
+ # dims
93
+ dims = args.dims or ["x", "y", "z"]
94
+ dims = [d for d in dims if d in ("x", "y", "z")]
95
+ if not dims:
96
+ raise ValueError("At least one of --dims x y z must be provided.")
97
+
98
+ # get trajectories
99
+ df = get_atom_trajectories(
100
+ xh,
101
+ frames=frames,
102
+ atoms=atoms,
103
+ atom_types=sorted(types) if types else None,
104
+ dims=dims,
105
+ format=args.format,
106
+ )
107
+
108
+ if df.empty:
109
+ print("No trajectory rows selected (check --atoms / --atom-types / frame range).")
110
+ return 1
111
+
112
+ # --- X-axis: iter/frame/time from frame_index ---
113
+ if "frame_index" not in df.columns:
114
+ raise KeyError("Expected 'frame_index' column in trajectory data.")
115
+ xvals, xlabel = convert_xaxis(df["frame_index"].to_numpy(), args.xaxis)
116
+
117
+ df = df.copy()
118
+ df.insert(0, "xaxis", xvals)
119
+
120
+ workflow_name = args.kind
121
+ # --- Export CSV ---
122
+ if args.export:
123
+ out = resolve_output_path(args.export, workflow_name)
124
+ df.to_csv(out, index=False)
125
+ print(f"[Done] Exported trajectories to {out}")
126
+
127
+ # --- Plot / Save (only long-format & single dim) ---
128
+ if args.save or args.plot:
129
+ if args.format == "long" and len(dims) == 1:
130
+ dim = dims[0]
131
+ dff = df
132
+
133
+ # if multiple atoms, use the first one (as before)
134
+ if "atom_id" in df.columns:
135
+ atom_ids = df["atom_id"].unique().tolist()
136
+ if len(atom_ids) > 1:
137
+ print(f"ℹ️ Multiple atoms detected: {atom_ids}. Plotting only atom_id={atom_ids[0]}.")
138
+ aid = atom_ids[0]
139
+ dff = df[df["atom_id"] == aid]
140
+
141
+ x = dff["xaxis"].values
142
+ y = dff[dim].values
143
+ title = f"{dim} vs {xlabel}"
144
+ if "aid" in locals():
145
+ title += f" (atom={aid})"
146
+
147
+ u = unit_for(args.yaxis) or unit_for(dim)
148
+ if args.save:
149
+ out = resolve_output_path(args.save, workflow_name)
150
+ single_plot(
151
+ x, y,
152
+ title=title,
153
+ xlabel=xlabel,
154
+ ylabel=f"{dim} ({u})" if u else dim,
155
+ save=out,
156
+ )
157
+
158
+ if args.plot:
159
+ single_plot(
160
+ x, y,
161
+ title=title,
162
+ xlabel=xlabel,
163
+ ylabel=f"{dim} ({u})" if u else dim,
164
+ save=None,
165
+ )
166
+ else:
167
+ print("⚠️ Plotting is only supported for long-format with a single dim (x|y|z).")
168
+
169
+ # --- No action fallback ---
170
+ if not args.export and not args.save and not args.plot:
171
+ print(df.head(10).to_string(index=False))
172
+
173
+ return 0
174
+
175
+ def _msd_task(args: argparse.Namespace) -> int:
176
+ xh = XmoloutHandler(args.file)
177
+
178
+ if not args.atoms:
179
+ print("❌ --atoms is required (comma/space-separated 1-based indices, e.g. '1,5,12').")
180
+ return 1
181
+
182
+ # Parse 1-based atom indices from string (do NOT use parse_atoms here;
183
+ # mean_squared_displacement expects 1-based atoms and handles conversion itself)
184
+ atoms: list[int] = []
185
+ for chunk in str(args.atoms).split(","):
186
+ for tok in chunk.split():
187
+ if tok:
188
+ atoms.append(int(tok))
189
+
190
+ if not atoms:
191
+ print("❌ Could not parse any atoms from --atoms.")
192
+ return 1
193
+
194
+ # Long-format MSD: frame_index, iter, atom_id, msd (per-atom, no averaging)
195
+ df_long = get_mean_squared_displacement(xh, atoms=atoms)
196
+ if df_long.empty:
197
+ print("No MSD data found for the selected atoms.")
198
+ return 1
199
+
200
+ workflow_name = args.kind
201
+
202
+ # ------------------------------------------------------------------
203
+ # X-axis: iter / frame / time (based on frame_index)
204
+ # ------------------------------------------------------------------
205
+ frames_unique = np.sort(df_long["frame_index"].unique())
206
+ xvals, xlabel = convert_xaxis(frames_unique, args.xaxis)
207
+ frame_to_x = dict(zip(frames_unique, xvals))
208
+
209
+ # ------------------------------------------------------------------
210
+ # Export CSV in WIDE format: [xlabel, frame_index, iter, msd[atom]...]
211
+ # ------------------------------------------------------------------
212
+ if args.export:
213
+ wide = (
214
+ df_long
215
+ .pivot(index=["frame_index", "iter"], columns="atom_id", values="msd")
216
+ .sort_index()
217
+ )
218
+
219
+ # Rename atom columns: atom_id → msd[atom_id]
220
+ wide.columns = [f"msd[{aid}]" for aid in wide.columns.to_list()]
221
+ wide = wide.reset_index()
222
+
223
+ # X column based on chosen xaxis
224
+ frames_wide = wide["frame_index"].to_numpy()
225
+ xvals_wide, xlabel_wide = convert_xaxis(frames_wide, args.xaxis)
226
+ wide.insert(0, xlabel_wide, xvals_wide)
227
+
228
+ out = resolve_output_path(args.export, workflow_name)
229
+ wide.to_csv(out, index=False)
230
+ print(f"[Done] MSD wide table saved to {out}")
231
+
232
+ # ------------------------------------------------------------------
233
+ # Build plotting data (one series per atom)
234
+ # ------------------------------------------------------------------
235
+ series_combined = [] # for single_plot (all atoms in one axis)
236
+ subplots_data = [] # for multi_subplots (one subplot per atom)
237
+
238
+ atom_ids = sorted(df_long["atom_id"].unique())
239
+ for aid in atom_ids:
240
+ dfi = df_long[df_long["atom_id"] == aid].sort_values("frame_index")
241
+ xs = [frame_to_x[i] for i in dfi["frame_index"].to_numpy()]
242
+ ys = dfi["msd"].to_numpy()
243
+ s = {"x": xs, "y": ys, "label": f"atom {aid}"}
244
+ series_combined.append(s)
245
+ subplots_data.append([s]) # each subplot has one series: this atom
246
+
247
+ title = f"MSD of atoms: {', '.join(map(str, atom_ids))}"
248
+
249
+ # ------------------------------------------------------------------
250
+ # Plot / Save: either single plot or subplots
251
+ # ------------------------------------------------------------------
252
+ if args.save or args.plot:
253
+ if args.subplot:
254
+ # one subplot per atom
255
+ if args.save:
256
+ out = resolve_output_path(args.save, workflow_name)
257
+ multi_subplots(
258
+ subplots=subplots_data,
259
+ title=title,
260
+ xlabel=xlabel,
261
+ ylabel="Ų",
262
+ legend=True,
263
+ save=out,
264
+ )
265
+ if args.plot:
266
+ multi_subplots(
267
+ subplots=subplots_data,
268
+ title=title,
269
+ xlabel=xlabel,
270
+ ylabel="Ų",
271
+ legend=True,
272
+ save=None,
273
+ )
274
+ else:
275
+ # all atoms in a single plot
276
+ if args.save:
277
+ out = resolve_output_path(args.save, workflow_name)
278
+ single_plot(
279
+ series=series_combined,
280
+ title=title,
281
+ xlabel=xlabel,
282
+ ylabel="Ų",
283
+ legend=True,
284
+ save=out,
285
+ )
286
+ if args.plot:
287
+ single_plot(
288
+ series=series_combined,
289
+ title=title,
290
+ xlabel=xlabel,
291
+ ylabel="Ų",
292
+ legend=True,
293
+ save=None,
294
+ )
295
+
296
+ # ------------------------------------------------------------------
297
+ # No action fallback
298
+ # ------------------------------------------------------------------
299
+ if not args.export and not args.save and not args.plot:
300
+ print("ℹ️ No action selected. Use --plot, --save, or --export.")
301
+ print(df_long.head(10).to_string(index=False))
302
+
303
+ return 0
304
+
305
+ def _rdf_task(args: argparse.Namespace) -> int:
306
+ xh = XmoloutHandler(args.file)
307
+ types_a = _parse_types(args.types_a)
308
+ types_b = _parse_types(args.types_b)
309
+ frames = _frames_from_args(xh, args)
310
+ backend = args.backend.lower()
311
+ workflow_name = args.kind
312
+
313
+ # Property mode
314
+ if args.prop is not None:
315
+ df = rdf_property_over_frames(
316
+ xh,
317
+ frames=frames,
318
+ backend=backend,
319
+ property=args.prop, # one of: first_peak, dominant_peak, area, excess_area
320
+ r_max=args.r_max,
321
+ bins=args.bins,
322
+ types_a=sorted(types_a) if types_a else None,
323
+ types_b=sorted(types_b) if types_b else None,
324
+ )
325
+ if args.export:
326
+ out = resolve_output_path(args.export, workflow_name)
327
+ df.to_csv(out, index=False)
328
+ print(f"[Done] Exported RDF property table to {out}")
329
+
330
+ if args.save or args.plot:
331
+ x = df["frame_index"].to_numpy()
332
+ # pick the right y-column
333
+ if args.prop == "area":
334
+ y = df["area"].to_numpy()
335
+ ylabel = "area"
336
+ elif args.prop == "excess_area":
337
+ y = df["excess_area"].to_numpy()
338
+ ylabel = "excess_area"
339
+ elif args.prop == "first_peak":
340
+ y = df["r_first_peak"].to_numpy()
341
+ ylabel = "r_first_peak (Å)"
342
+ else: # dominant_peak
343
+ y = df["r_peak"].to_numpy()
344
+ ylabel = "r_peak (Å)"
345
+
346
+ title = f"{args.prop} ({backend}) vs frame"
347
+
348
+ if args.save:
349
+ out = resolve_output_path(args.save, workflow_name)
350
+ single_plot(
351
+ x,
352
+ y,
353
+ title=title,
354
+ xlabel="frame",
355
+ ylabel=ylabel,
356
+ save=out,
357
+ )
358
+ if args.plot:
359
+ single_plot(
360
+ x,
361
+ y,
362
+ title=title,
363
+ xlabel="frame",
364
+ ylabel=ylabel,
365
+ save=None,
366
+ )
367
+
368
+ if not args.export and not args.save and not args.plot:
369
+ print(df.head(10).to_string(index=False))
370
+ return 0
371
+
372
+ # Curve mode (averaged g(r) vs r)
373
+ if backend == "freud":
374
+ r, g = rdf_using_freud(
375
+ xh,
376
+ frames=frames,
377
+ types_a=types_a,
378
+ types_b=types_b,
379
+ r_max=args.r_max if args.r_max is not None else None,
380
+ bins=args.bins,
381
+ average=True,
382
+ return_stack=False,
383
+ )
384
+ elif backend == "ovito":
385
+ r, g = rdf_using_ovito(
386
+ xh,
387
+ frames=frames,
388
+ r_max=(args.r_max if args.r_max is not None else 4.0),
389
+ bins=args.bins,
390
+ types_a=types_a,
391
+ types_b=types_b,
392
+ average=True,
393
+ return_stack=False,
394
+ )
395
+ else:
396
+ raise ValueError(f"Unknown backend: {backend}")
397
+
398
+ sel_txt = ""
399
+ if types_a or types_b:
400
+ sa = sorted(types_a) if types_a else ["*"]
401
+ sb = sorted(types_b) if types_b else ["*"]
402
+ sel_txt = f" A={sa}; B={sb}"
403
+ title = f"RDF [{backend}]{sel_txt}"
404
+
405
+ # Only plot if requested
406
+ if args.save or args.plot:
407
+ out = resolve_output_path(args.save, workflow_name)
408
+ if args.save:
409
+ single_plot(
410
+ r,
411
+ g,
412
+ plot_type="line",
413
+ title=title,
414
+ xlabel="r (Å)",
415
+ ylabel="g(r)",
416
+ save=out,
417
+ )
418
+ if args.plot:
419
+ single_plot(
420
+ r,
421
+ g,
422
+ plot_type="line",
423
+ title=title,
424
+ xlabel="r (Å)",
425
+ ylabel="g(r)",
426
+ save=None,
427
+ )
428
+
429
+ if args.export:
430
+ out = resolve_output_path(args.export, workflow_name)
431
+ df = pd.DataFrame({"r": r, "g": g})
432
+ df.to_csv(out, index=False)
433
+ print(f"[Done] Exported RDF to {out}")
434
+
435
+ if not args.export and not args.save and not args.plot:
436
+ # simple preview if user didn't ask for plot or export
437
+ out = pd.DataFrame({"r": r, "g": g})
438
+ print(out.head(10).to_string(index=False))
439
+
440
+ return 0
441
+
442
+ def _boxdims_task(args: argparse.Namespace) -> int:
443
+ xh = XmoloutHandler(args.file)
444
+
445
+ # frames: use all if not provided
446
+ frames = _parse_frames(args.frames) if getattr(args, "frames", None) else None
447
+
448
+ df = get_unit_cell_dimensions_across_frames(xh, frames=frames)
449
+ if df.empty:
450
+ print("No box-dimension data found for the requested frames.")
451
+ return 1
452
+
453
+ # --- X axis: iter / frame / time ---
454
+ xvals, xlabel = convert_xaxis(df["frame_index"].to_numpy(), args.xaxis)
455
+ df = df.copy()
456
+ df.insert(0, "x", xvals)
457
+
458
+ workflow_name = args.kind
459
+
460
+ # --- Export CSV ---
461
+ if args.export:
462
+ out = resolve_output_path(args.export, workflow_name)
463
+ df.to_csv(out, index=False)
464
+ print(f"[Done] Exported box-dimension table to {out}")
465
+
466
+ # --- Plot / Save using multi_subplots ---
467
+ if args.plot or args.save:
468
+ # plot a, b, c if present
469
+ ycols = [c for c in ("a", "b", "c") if c in df.columns]
470
+ if not ycols:
471
+ print("No a/b/c columns found in box-dimension table; nothing to plot.")
472
+ else:
473
+ subplots_data = []
474
+ for col in ycols:
475
+ subplots_data.append([
476
+ {
477
+ "x": df["x"].to_numpy(),
478
+ "y": df[col].to_numpy(),
479
+ "label": col,
480
+ }
481
+ ])
482
+
483
+ save_target = resolve_output_path(args.save, workflow_name) if args.save else None
484
+
485
+ multi_subplots(
486
+ subplots=subplots_data,
487
+ title=f"Box dimensions vs {xlabel}",
488
+ xlabel=xlabel,
489
+ ylabel="Length (Å)",
490
+ legend=True,
491
+ save=save_target if (args.save or args.plot) else None,
492
+ )
493
+
494
+ # --- No action fallback ---
495
+ if not args.export and not args.save and not args.plot:
496
+ print(df.head(10).to_string(index=False))
497
+
498
+ return 0
499
+
500
+ def _trim_task(args: argparse.Namespace) -> int:
501
+ """
502
+ Write a trimmed copy of an xmolout file that keeps only atom_type and x,y,z
503
+ columns for each atom line (drops any extra per-atom columns).
504
+ """
505
+ xh = XmoloutHandler(args.file)
506
+ out_path = args.output or "xmolout_trimmed"
507
+ # include_extras=False ⇒ only atom_type + x y z
508
+ write_xmolout_from_handler(xh, out_path, include_extras=False)
509
+ print(f"[Done] Wrote trimmed xmolout (type + x,y,z only) to {out_path}")
510
+ return 0
511
+
512
+ # --------------------------
513
+ # CLI REGISTRATION
514
+ # --------------------------
515
+ def _add_common_xmolout_io_args(
516
+ p: argparse.ArgumentParser,
517
+ *,
518
+ include_plot: bool = False,
519
+ ) -> None:
520
+ """Common I/O flags for xmolout-based tasks."""
521
+ p.add_argument("--file", default="xmolout", help="Path to xmolout file.")
522
+ if include_plot:
523
+ p.add_argument("--plot", action="store_true", help="Show plot interactively.")
524
+ p.add_argument("--save", default=None, help="Path to save plot image.")
525
+ p.add_argument("--export", default=None, help="Path to export CSV data.")
526
+
527
+
528
+ def register_tasks(subparsers: argparse._SubParsersAction) -> None:
529
+ # ------------------------------------------------------------------
530
+ # trajget (single or multi-atom trajectories)
531
+ # ------------------------------------------------------------------
532
+ pt = subparsers.add_parser(
533
+ "trajget",
534
+ help="Get atom trajectories per frame (single or multiple atoms).",
535
+ description=(
536
+ "Get atom trajectories from xmolout.\n\n"
537
+ "Examples:\n"
538
+ " reaxkit xmolout trajget --atoms 1 --dims z --xaxis time "
539
+ "--save atom1_z.png --export atom1_z.csv\n"
540
+ " reaxkit xmolout trajget --atom-types Al --dims x y z --format wide "
541
+ "--export Al_all_dims_traj.csv\n"
542
+ " reaxkit xmolout trajget --atom-types Al --frames 10:200:10 --dims z "
543
+ "--export Al_z_dim_traj.csv\n"
544
+ ),
545
+ formatter_class=argparse.RawTextHelpFormatter,
546
+ )
547
+ _add_common_xmolout_io_args(pt, include_plot=True)
548
+ pt.add_argument("--atoms", default=None,
549
+ help="Comma/space separated 1-based atom indices, e.g. '1,5,12'.")
550
+ pt.add_argument("--dims", nargs="+", default=None, choices=["x", "y", "z"],
551
+ help="Coordinate dimensions to include (default: x y z).")
552
+ pt.add_argument("--xaxis", default="frame", choices=["iter", "frame", "time"],
553
+ help="Quantity on x-axis (default: frame).")
554
+ pt.add_argument("--frames", default=None,
555
+ help="Frame selector: 'start:stop[:step]' or 'i,j,k' (default: all).")
556
+ pt.add_argument("--atom-types", "--types", dest="atom_types", default=None,
557
+ help="Comma/space separated atom types, e.g. 'Al,N'.")
558
+ pt.add_argument("--format", choices=["long", "wide"], default="long",
559
+ help="Output table layout: long or wide (default: long).")
560
+ pt.set_defaults(_run=_trajget_task)
561
+
562
+ # ------------------------------------------------------------------
563
+ # MSD
564
+ # ------------------------------------------------------------------
565
+ p2 = subparsers.add_parser(
566
+ "msd",
567
+ help="Compute mean squared displacement (MSD) for one or more atoms.",
568
+ description=(
569
+ "Compute MSD from xmolout.\n\n"
570
+ "Examples:\n"
571
+ " reaxkit xmolout msd --atoms 1 --xaxis frame "
572
+ "--save atom1_msd.png --export atom1_msd.csv\n"
573
+ " reaxkit xmolout msd --atoms 1,2,3 --xaxis time "
574
+ "--save msd.png --export msd.csv\n"
575
+ " reaxkit xmolout msd --atoms 1,2,3 --subplot "
576
+ "--save msd_subplot.png --export msd_subplot.csv\n"
577
+ ),
578
+ formatter_class=argparse.RawTextHelpFormatter,
579
+ )
580
+ _add_common_xmolout_io_args(p2, include_plot=True)
581
+ p2.add_argument("--atoms", required=True,
582
+ help="Comma/space separated 1-based atom indices, e.g. '1,5,12'.")
583
+ p2.add_argument("--xaxis", default="frame", choices=["iter", "frame", "time"],
584
+ help="Quantity on x-axis (default: frame).")
585
+ p2.add_argument("--subplot", action="store_true",
586
+ help="Plot each atom in its own subplot instead of a single combined plot.")
587
+ p2.set_defaults(_run=_msd_task)
588
+
589
+ # ------------------------------------------------------------------
590
+ # RDF
591
+ # ------------------------------------------------------------------
592
+ p3 = subparsers.add_parser(
593
+ "rdf",
594
+ help="Compute RDF curve or per-frame RDF-derived property.",
595
+ description=(
596
+ "Compute RDF using FREUD or OVITO backends.\n\n"
597
+ "Curve example:\n"
598
+ " reaxkit xmolout rdf --save rdf.png --export rdf.csv "
599
+ "--frames 0 --bins 200 --r-max 5\n\n"
600
+ "Property example:\n"
601
+ " reaxkit xmolout rdf --prop area --bins 200 --r-max 5 "
602
+ "--frames 0:10:1 --save rdf_area.png\n"
603
+ ),
604
+ formatter_class=argparse.RawTextHelpFormatter,
605
+ )
606
+ _add_common_xmolout_io_args(p3, include_plot=True)
607
+ p3.add_argument("--backend", choices=["freud", "ovito"], default="ovito",
608
+ help="RDF backend: freud or ovito (default: ovito).")
609
+ p3.add_argument(
610
+ "--prop",
611
+ choices=["first_peak", "dominant_peak", "area", "excess_area"],
612
+ default=None,
613
+ help="Compute this RDF-derived property per frame instead of a curve.",
614
+ )
615
+ p3.add_argument("--types-a", "--types_a", dest="types_a", default=None,
616
+ help="Comma/space separated atom types for set A, e.g. 'Al,N'.")
617
+ p3.add_argument("--types-b", "--types_b", dest="types_b", default=None,
618
+ help="Comma/space separated atom types for set B, e.g. 'N'.")
619
+ p3.add_argument("--bins", type=int, default=200, help="Number of RDF bins.")
620
+ p3.add_argument("--r-max", type=float, default=None,
621
+ help="Max radius in Å; default depends on backend.")
622
+ p3.add_argument("--frames", default=None,
623
+ help="Frame selector: 'start:stop[:step]' or 'i,j,k'.")
624
+ p3.add_argument("--every", type=int, default=1,
625
+ help="Use every Nth frame (default: 1).")
626
+ p3.add_argument("--start", type=int, default=None,
627
+ help="First frame index (0-based).")
628
+ p3.add_argument("--stop", type=int, default=None,
629
+ help="Last frame index (0-based).")
630
+ p3.add_argument("--norm", choices=["extent", "cell"], default="extent",
631
+ help="FREUD normalization: extent or cell (default: extent).")
632
+ p3.add_argument("--c-eff", type=float, default=None,
633
+ help="FREUD only: effective c-length for --norm cell.")
634
+ p3.set_defaults(_run=_rdf_task)
635
+
636
+ # ------------------------------------------------------------------
637
+ # BOX DIMS
638
+ # ------------------------------------------------------------------
639
+ pb = subparsers.add_parser(
640
+ "boxdims",
641
+ help="Get box/cell dimensions per frame and optionally plot them.",
642
+ description=(
643
+ "Get box/cell dimensions from xmolout.\n\n"
644
+ "Examples:\n"
645
+ " reaxkit xmolout boxdims --frames 0:500:5 --xaxis time "
646
+ "--export box_dims.csv\n"
647
+ " reaxkit xmolout boxdims --frames 0:100:10 --xaxis iter "
648
+ "--save box_dim_plots.png\n"
649
+ ),
650
+ formatter_class=argparse.RawTextHelpFormatter,
651
+ )
652
+ _add_common_xmolout_io_args(pb, include_plot=True)
653
+ pb.add_argument("--frames", default=None,
654
+ help="Frame selector: 'start:stop[:step]' or 'i,j,k'.")
655
+ pb.add_argument("--xaxis", choices=["frame", "iter", "time"], default="frame",
656
+ help="Quantity on x-axis (default: frame).")
657
+ pb.set_defaults(_run=_boxdims_task)
658
+
659
+ # ------------------------------------------------------------------
660
+ # TRIM
661
+ # ------------------------------------------------------------------
662
+ ptr = subparsers.add_parser(
663
+ "trim",
664
+ help="Write a trimmed copy of xmolout with only atom_type and x,y,z.",
665
+ description=(
666
+ "Trim xmolout to a lighter file with atom type and coordinates only.\n\n"
667
+ "Example:\n"
668
+ " reaxkit xmolout trim --file xmolout --output xmolout_trimmed\n"
669
+ ),
670
+ )
671
+ ptr.add_argument("--file", default="xmolout",
672
+ help="Input xmolout file.")
673
+ ptr.add_argument("--output", default="xmolout_trimmed",
674
+ help="Output trimmed xmolout file.")
675
+ ptr.set_defaults(_run=_trim_task)
676
+
677
+
678
+