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,577 @@
1
+ """
2
+ Molecular-fragment (molfra) analysis workflow for ReaxKit.
3
+
4
+ This workflow provides tools for analyzing ReaxFF `molfra.out` and
5
+ `molfra_ig.out` files, which describe molecular fragments, species
6
+ identities, and their evolution during a simulation.
7
+
8
+ It supports:
9
+ - Tracking occurrences of selected molecular species across frames,
10
+ with optional automatic selection based on occurrence thresholds.
11
+ - Computing and visualizing global totals, including total number of
12
+ molecules, total atoms, and total molecular mass versus iteration,
13
+ frame, or physical time.
14
+ - Identifying and analyzing the largest molecule in the system, either
15
+ by individual molecular mass or by per-element atom composition.
16
+ - Plotting results, saving figures, and exporting processed data to CSV
17
+ using standardized ReaxKit output paths.
18
+
19
+ The workflow is designed to enable systematic analysis of chemical
20
+ speciation, fragmentation, and growth processes in ReaxFF simulations.
21
+ """
22
+
23
+
24
+ from __future__ import annotations
25
+
26
+ import argparse
27
+ from typing import Optional, Sequence, Union, List
28
+
29
+ from reaxkit.io.handlers.molfra_handler import MolFraHandler
30
+ from reaxkit.analysis.per_file.molfra_analyzer import (
31
+ get_molfra_data_wide_format,
32
+ _qualifying_types,
33
+ get_molfra_totals_vs_axis,
34
+ largest_molecule_by_individual_mass,
35
+ atoms_in_the_largest_molecule_wide_format,
36
+ )
37
+ from reaxkit.utils.frame_utils import parse_frames, select_frames
38
+ from reaxkit.utils.media.convert import convert_xaxis
39
+ from reaxkit.utils.media.plotter import single_plot, multi_subplots
40
+ from reaxkit.utils.alias import normalize_choice
41
+ from reaxkit.utils.path import resolve_output_path
42
+
43
+ FramesT = Optional[Union[slice, Sequence[int]]]
44
+
45
+
46
+ # ============================================================
47
+ # Occurrences task
48
+ # ============================================================
49
+ def _molfra_occur_task(args: argparse.Namespace) -> int:
50
+ # Normalize x-axis alias
51
+ args.xaxis = normalize_choice(args.xaxis, domain="xaxis") or "iter"
52
+
53
+ handler = MolFraHandler(args.file)
54
+ handler._parse()
55
+
56
+ # Plot behavior: export-only suppresses interactive plotting
57
+ do_plot = bool(args.plot) and not bool(args.export)
58
+
59
+ frames_sel = parse_frames(args.frames)
60
+
61
+ # -------------------------
62
+ # Determine which molecules
63
+ # -------------------------
64
+ molecules: Optional[List[str]] = args.molecules
65
+ if not molecules:
66
+ if args.threshold is None:
67
+ print("ℹ️ No molecules or threshold given. Use --molecules ... or --threshold N.")
68
+ return 0
69
+ excl = set(args.exclude or [])
70
+ molecules = _qualifying_types(
71
+ handler,
72
+ threshold=args.threshold,
73
+ exclude_types=(excl if excl else None),
74
+ )
75
+ if not molecules:
76
+ print(f"ℹ️ No species reached max occurrence ≥ {args.threshold}.")
77
+ return 0
78
+
79
+ wide = get_molfra_data_wide_format(
80
+ handler,
81
+ molecules=molecules,
82
+ iters=None,
83
+ by_index=False,
84
+ fill_value=0,
85
+ )
86
+ if wide.empty:
87
+ print("ℹ️ No data available after selection.")
88
+ return 0
89
+
90
+ wide = select_frames(wide, frames_sel)
91
+
92
+ if wide.empty:
93
+ print("ℹ️ No data left after frame selection.")
94
+ return 0
95
+
96
+ # -------------------------
97
+ # Build x-axis
98
+ # -------------------------
99
+ iters = wide["iter"].to_numpy()
100
+ if args.xaxis == "iter":
101
+ x_vals = list(iters)
102
+ xlabel = "iter"
103
+ else:
104
+ x_vals, xlabel = convert_xaxis(iters, args.xaxis, control_file=args.control)
105
+ x_vals = list(x_vals)
106
+
107
+ # Series per molecule
108
+ series = {m: wide[m].tolist() for m in molecules if m in wide.columns}
109
+
110
+ # -------------------------
111
+ # Export
112
+ # -------------------------
113
+ if args.export:
114
+ workflow_name = getattr(args, "kind", "molfra")
115
+ out = resolve_output_path(args.export, workflow_name)
116
+
117
+ xcol = (
118
+ xlabel.strip()
119
+ .lower()
120
+ .replace(" ", "_")
121
+ .replace("(", "")
122
+ .replace(")", "")
123
+ )
124
+ out_df = wide.copy()
125
+ if args.xaxis != "iter":
126
+ out_df.insert(0, xcol, x_vals)
127
+ out_df.to_csv(out, index=False)
128
+ print(f"[Done] Exported data to {out}")
129
+
130
+ # -------------------------
131
+ # Plot / save
132
+ # -------------------------
133
+ if args.save:
134
+ workflow_name = getattr(args, "kind", "molfra")
135
+ save_path = resolve_output_path(args.save, workflow_name)
136
+ else:
137
+ save_path = None
138
+
139
+ if not args.title:
140
+ if args.threshold is not None and not args.molecules:
141
+ title = f"Species with max occurrence ≥ {args.threshold}"
142
+ elif len(molecules) == 1:
143
+ title = f"{molecules[0]} occurrence"
144
+ else:
145
+ title = "Molecule occurrence"
146
+ else:
147
+ title = args.title
148
+
149
+ if (do_plot or save_path) and series:
150
+ series_for_plot = [
151
+ {"x": x_vals, "y": yvals, "label": name}
152
+ for name, yvals in series.items()
153
+ ]
154
+ single_plot(
155
+ series=series_for_plot,
156
+ title=title,
157
+ xlabel=xlabel,
158
+ ylabel="occurrence",
159
+ save=str(save_path) if save_path else None,
160
+ legend=True,
161
+ )
162
+ elif not args.save and not args.export and not do_plot:
163
+ print("ℹ️ No action selected. Use one or more of --plot, --save, --export.")
164
+
165
+ return 0
166
+
167
+
168
+ # ============================================================
169
+ # Totals task
170
+ # ============================================================
171
+ def _molfra_total_task(args: argparse.Namespace) -> int:
172
+ """
173
+ Handle `reaxkit molfra total`:
174
+
175
+ - If --plot and/or --save: create a multi-subplot figure of *all* totals
176
+ (total_molecules, total_atoms, total_molecular_mass) vs chosen x-axis.
177
+ - If --export: export all totals as CSV using get_molfra_totals_vs_axis().
178
+ """
179
+ # Normalize x-axis alias
180
+ args.xaxis = normalize_choice(args.xaxis, domain="xaxis") or "iter"
181
+
182
+ handler = MolFraHandler(args.file)
183
+ handler._parse()
184
+
185
+ # Ensure totals table exists so we can inspect which columns are present
186
+ if not hasattr(handler, "_df_totals"):
187
+ print("⚠️ Totals dataframe not found in MolFraHandler. Parse handler with an updated version first.")
188
+ return 0
189
+
190
+ df_raw = handler._df_totals
191
+ if df_raw.empty:
192
+ print("⚠️ Totals dataframe is empty.")
193
+ return 0
194
+
195
+ # Determine which totals columns exist
196
+ totals_all = ("total_molecules", "total_atoms", "total_molecular_mass")
197
+ quantities = [c for c in totals_all if c in df_raw.columns]
198
+ if not quantities:
199
+ print("⚠️ No totals columns (total_molecules / total_atoms / total_molecular_mass) found.")
200
+ return 0
201
+
202
+ # Get totals vs requested x-axis using analyzer helper
203
+ df_tot = get_molfra_totals_vs_axis(
204
+ handler,
205
+ xaxis=args.xaxis,
206
+ control_file=args.control,
207
+ quantities=quantities,
208
+ )
209
+ if df_tot.empty:
210
+ print("⚠️ Totals table is empty after axis conversion.")
211
+ return 0
212
+
213
+ # Apply frame selection (position-based after filtering)
214
+ frames_sel = parse_frames(args.frames)
215
+ df_tot = select_frames(df_tot, frames_sel)
216
+ if df_tot.empty:
217
+ print("⚠️ Totals table empty after frame selection.")
218
+ return 0
219
+
220
+ # Identify x-axis column (whatever is not a totals column)
221
+ non_q = [c for c in df_tot.columns if c not in quantities]
222
+ if not non_q:
223
+ print("⚠️ Could not determine x-axis column.")
224
+ return 0
225
+ if "iter" in non_q and len(non_q) > 1:
226
+ # Prefer non-iter if both iter & time-like column exist
227
+ xcol = [c for c in non_q if c != "iter"][0]
228
+ else:
229
+ xcol = non_q[0]
230
+
231
+ x_vals = df_tot[xcol].tolist()
232
+ xlabel = xcol.replace("_", " ")
233
+
234
+ # Flags: plotting vs exporting
235
+ do_plot_or_save = bool(args.plot or args.save)
236
+ workflow_name = getattr(args, "kind", "molfra")
237
+
238
+ # ---------- Export all totals ----------
239
+ if args.export:
240
+ out_csv = resolve_output_path(args.export, workflow_name)
241
+ df_tot.to_csv(out_csv, index=False)
242
+ print(f"[Done] Exported totals to {out_csv}")
243
+
244
+ # ---------- Multi-subplots for totals ----------
245
+ if do_plot_or_save:
246
+ # Build subplot series: one subplot per quantity
247
+ subplots = []
248
+ for q in quantities:
249
+ subplots.append(
250
+ [
251
+ {
252
+ "x": x_vals,
253
+ "y": df_tot[q].tolist(),
254
+ "label": q,
255
+ }
256
+ ]
257
+ )
258
+
259
+ # Resolve save path (None → interactive only)
260
+ save_path = resolve_output_path(args.save, workflow_name) if args.save else None
261
+
262
+ title = args.title or f"Totals vs {xlabel}"
263
+ multi_subplots(
264
+ subplots=subplots,
265
+ title=title,
266
+ xlabel=xlabel,
267
+ ylabel=["count", "count", "mass (a.u.)"],
268
+ sharex=True,
269
+ sharey=False,
270
+ legend=True,
271
+ figsize=(8.0, 2.5 * len(quantities)),
272
+ save=save_path,
273
+ )
274
+
275
+ if not (args.plot or args.save or args.export):
276
+ print("ℹ️ No action selected. Use one or more of --plot, --save, --export.")
277
+
278
+ return 0
279
+
280
+
281
+ # ============================================================
282
+ # Largest-molecule task
283
+ # ============================================================
284
+ def _molfra_largest_task(args: argparse.Namespace) -> int:
285
+ # Normalize x-axis alias
286
+ args.xaxis = normalize_choice(args.xaxis, domain="xaxis") or "iter"
287
+
288
+ handler = MolFraHandler(args.file)
289
+ handler._parse()
290
+
291
+ do_plot = bool(args.plot) and not bool(args.export)
292
+ frames_sel = parse_frames(args.frames)
293
+
294
+ # Decide mode: mass vs atoms
295
+ mode_atoms = bool(args.atoms)
296
+ mode_mass = bool(args.mass)
297
+
298
+ if not mode_atoms and not mode_mass:
299
+ # default to atoms if nothing specified
300
+ mode_atoms = True
301
+
302
+ workflow_name = getattr(args, "kind", "molfra")
303
+
304
+ if mode_mass:
305
+ # ===============================
306
+ # Largest molecule mass vs x-axis
307
+ # ===============================
308
+ df = largest_molecule_by_individual_mass(handler)
309
+ if df.empty:
310
+ print("⚠️ No data for largest molecule by mass.")
311
+ return 0
312
+
313
+ df = select_frames(df, frames_sel)
314
+ if df.empty:
315
+ print("⚠️ No data for largest molecule after frame selection.")
316
+ return 0
317
+
318
+ iters = df["iter"].to_numpy()
319
+ if args.xaxis == "iter":
320
+ x_vals = list(iters)
321
+ xlabel = "iter"
322
+ else:
323
+ x_vals, xlabel = convert_xaxis(iters, args.xaxis, control_file=args.control)
324
+ x_vals = list(x_vals)
325
+
326
+ y = df["molecular_mass"].to_list()
327
+
328
+ # Export
329
+ if args.export:
330
+ out = resolve_output_path(args.export, workflow_name)
331
+ xcol = (
332
+ xlabel.strip()
333
+ .lower()
334
+ .replace(" ", "_")
335
+ .replace("(", "")
336
+ .replace(")", "")
337
+ )
338
+ out_df = df.copy()
339
+ if args.xaxis != "iter":
340
+ out_df.insert(0, xcol, x_vals)
341
+ out_df.to_csv(out, index=False)
342
+ print(f"[Done] Exported data to {out}")
343
+
344
+ # Save path
345
+ if args.save:
346
+ save_path = resolve_output_path(args.save, workflow_name)
347
+ else:
348
+ save_path = None
349
+
350
+ if not args.title:
351
+ title = "Largest molecule mass vs x-axis"
352
+ else:
353
+ title = args.title
354
+
355
+ if do_plot or save_path:
356
+ series_for_plot = [
357
+ {
358
+ "x": x_vals,
359
+ "y": y,
360
+ "label": "largest_molecule_mass",
361
+ }
362
+ ]
363
+ single_plot(
364
+ series=series_for_plot,
365
+ title=title,
366
+ xlabel=xlabel,
367
+ ylabel="Molecular mass",
368
+ save=str(save_path) if save_path else None,
369
+ legend=True,
370
+ )
371
+ elif not args.save and not args.export and not do_plot:
372
+ print("ℹ️ No action selected. Use one or more of --plot, --save, --export.")
373
+
374
+ return 0
375
+
376
+ # ===============================
377
+ # Largest-molecule atoms (wide)
378
+ # ===============================
379
+ df_wide = atoms_in_the_largest_molecule_wide_format(handler)
380
+ if df_wide.empty:
381
+ print("⚠️ No atom data available for largest molecule.")
382
+ return 0
383
+
384
+ df_wide = select_frames(df_wide, frames_sel)
385
+ if df_wide.empty:
386
+ print("⚠️ No atom data after frame selection.")
387
+ return 0
388
+
389
+ iters = df_wide["iter"].to_numpy()
390
+ if args.xaxis == "iter":
391
+ x_vals = list(iters)
392
+ xlabel = "iter"
393
+ else:
394
+ x_vals, xlabel = convert_xaxis(iters, args.xaxis, control_file=args.control)
395
+ x_vals = list(x_vals)
396
+
397
+ element_cols = [c for c in df_wide.columns if c != "iter"]
398
+ if not element_cols:
399
+ print("⚠️ No element columns found to plot.")
400
+ return 0
401
+
402
+ series = {elem: df_wide[elem].tolist() for elem in element_cols}
403
+
404
+ # Export
405
+ if args.export:
406
+ out = resolve_output_path(args.export, workflow_name)
407
+ xcol = (
408
+ xlabel.strip()
409
+ .lower()
410
+ .replace(" ", "_")
411
+ .replace("(", "")
412
+ .replace(")", "")
413
+ )
414
+ out_df = df_wide.copy()
415
+ if args.xaxis != "iter":
416
+ out_df.insert(0, xcol, x_vals)
417
+ out_df.to_csv(out, index=False)
418
+ print(f"[Done] Exported data to {out}")
419
+
420
+ # Save path
421
+ if args.save:
422
+ save_path = resolve_output_path(args.save, workflow_name)
423
+ else:
424
+ save_path = None
425
+
426
+ if not args.title:
427
+ title = "Atom counts in largest molecule"
428
+ else:
429
+ title = args.title
430
+
431
+ if (do_plot or save_path) and series:
432
+ series_for_plot = [
433
+ {"x": x_vals, "y": df_wide[elem].tolist(), "label": elem}
434
+ for elem in element_cols
435
+ ]
436
+ single_plot(
437
+ series=series_for_plot,
438
+ title=title,
439
+ xlabel=xlabel,
440
+ ylabel="Atom count",
441
+ save=str(save_path) if save_path else None,
442
+ legend=True,
443
+ )
444
+ elif not args.save and not args.export and not do_plot:
445
+ print("ℹ️ No action selected. Use one or more of --plot, --save, --export.")
446
+
447
+ return 0
448
+
449
+
450
+ # ============================================================
451
+ # Registry
452
+ # ============================================================
453
+ def _add_common_molfra_axes_args(p: argparse.ArgumentParser) -> None:
454
+ """Common axis and frame-selection flags for molfra-based tasks."""
455
+ p.add_argument(
456
+ "--xaxis",
457
+ default="iter",
458
+ help="X-axis mode. Canonical: 'iter', 'time', 'frame'.",
459
+ )
460
+ p.add_argument(
461
+ "--control",
462
+ default="control",
463
+ help="Path to control file for time conversion (used when --xaxis time).",
464
+ )
465
+ p.add_argument(
466
+ "--frames",
467
+ default=None,
468
+ help="Frame selection (position-based after filtering): 'start:stop[:step]' or 'i,j,k'.",
469
+ )
470
+
471
+
472
+ def _add_common_molfra_output_args(
473
+ p: argparse.ArgumentParser,
474
+ *,
475
+ plot_help: str = "Show the plot interactively.",
476
+ save_help: str = "Save the plot (path or directory, resolved via resolve_output_path).",
477
+ export_help: str = "Export the data table to CSV (path or directory, resolved via resolve_output_path).",
478
+ ) -> None:
479
+ """Common output / I/O flags for molfra-based tasks."""
480
+ p.add_argument("--title", default=None, help="Custom plot title.")
481
+ p.add_argument("--plot", action="store_true", help=plot_help)
482
+ p.add_argument("--save", default=None, help=save_help)
483
+ p.add_argument("--export", default=None, help=export_help)
484
+
485
+
486
+ def register_tasks(subparsers: argparse._SubParsersAction) -> None:
487
+ # -----------------------
488
+ # Occurrences subcommand
489
+ # -----------------------
490
+ p_occur = subparsers.add_parser(
491
+ "occur",
492
+ help="Get molecule occurrences across frames and optionally plot, save, or export.",
493
+ description=(
494
+ "Examples:\n"
495
+ " reaxkit molfra occur --molecules H2O N128Al128 --save water_and_slab_occurrence.png\n"
496
+ " reaxkit molfra occur --threshold 3 --exclude Pt --export species_with_max_occur.csv\n"
497
+ ),
498
+ formatter_class=argparse.RawTextHelpFormatter,
499
+ )
500
+ p_occur.add_argument("--file", default="molfra.out", help="Path to molfra.out")
501
+
502
+ group = p_occur.add_mutually_exclusive_group(required=False)
503
+ group.add_argument("--molecules", nargs="+", default=None,
504
+ help="One or more molecule types to include (e.g., H2O OH N128Al128).",
505
+ )
506
+ group.add_argument("--threshold", type=int, default=None,
507
+ help="Auto-include all species whose max occurrence ≥ threshold.",
508
+ )
509
+ p_occur.add_argument("--exclude", nargs="*", default=None,
510
+ help="Species to exclude when using --threshold (e.g., Pt).",
511
+ )
512
+
513
+ _add_common_molfra_axes_args(p_occur)
514
+ _add_common_molfra_output_args(p_occur,
515
+ plot_help="Show the plot interactively.",
516
+ save_help="Save the plot (path or directory, resolved via resolve_output_path).",
517
+ export_help="Export the data table to CSV (path or directory, resolved via resolve_output_path).",
518
+ )
519
+
520
+ p_occur.set_defaults(_run=_molfra_occur_task, kind="molfra")
521
+
522
+ # -----------------------
523
+ # Totals subcommand
524
+ # -----------------------
525
+ p_total = subparsers.add_parser(
526
+ "total",
527
+ help="Plot/export totals (molecules, atoms, mass) vs x-axis.",
528
+ description=(
529
+ "Examples:\n"
530
+ " reaxkit molfra total --file molfra_ig.out "
531
+ "--export totals_data.csv --save totals_data.png\n"
532
+ ),
533
+ formatter_class=argparse.RawTextHelpFormatter,
534
+ )
535
+ p_total.add_argument("--file", default="molfra.out", help="Path to molfra.out")
536
+
537
+ _add_common_molfra_axes_args(p_total)
538
+ _add_common_molfra_output_args(p_total,
539
+ plot_help="Show the multi-subplot figure interactively.",
540
+ save_help="Save the multi-subplot figure (path or directory, resolved via resolve_output_path).",
541
+ export_help="Export all totals to CSV (path or directory, resolved via resolve_output_path).",
542
+ )
543
+
544
+ p_total.set_defaults(_run=_molfra_total_task, kind="molfra")
545
+
546
+ # -----------------------
547
+ # Largest subcommand
548
+ # -----------------------
549
+ p_largest = subparsers.add_parser(
550
+ "largest",
551
+ help="Analyze the largest molecule (by individual mass or atom composition).",
552
+ description=(
553
+ "Examples:\n"
554
+ " reaxkit molfra largest --atoms --frames '0:30:2' "
555
+ "--xaxis time --save largest.png --export largest.csv\n"
556
+ " reaxkit molfra largest --mass --xaxis time --export largest_mass.csv\n"
557
+ ),
558
+ formatter_class=argparse.RawTextHelpFormatter,
559
+ )
560
+ p_largest.add_argument("--file", default="molfra.out", help="Path to molfra.out")
561
+
562
+ mode_group = p_largest.add_mutually_exclusive_group(required=False)
563
+ mode_group.add_argument("--atoms", dest="atoms", action="store_true",
564
+ help="Use per-element atom counts for the largest molecule per iter (default).",
565
+ )
566
+ mode_group.add_argument("--mass", action="store_true",
567
+ help="Use largest molecule individual mass vs x-axis.",
568
+ )
569
+
570
+ _add_common_molfra_axes_args(p_largest)
571
+ _add_common_molfra_output_args(p_largest,
572
+ plot_help="Show the plot interactively.",
573
+ save_help="Save the plot (path or directory, resolved via resolve_output_path).",
574
+ export_help="Export the data table to CSV (path or directory, resolved via resolve_output_path).",
575
+ )
576
+
577
+ p_largest.set_defaults(_run=_molfra_largest_task, kind="molfra")