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,601 @@
1
+ """
2
+ General-purpose plotting workflow for ReaxKit.
3
+
4
+ This workflow provides flexible plotting utilities for arbitrary tabular data
5
+ (text, CSV, TSV, or whitespace-delimited files), without assuming any specific
6
+ ReaxFF file format or column headers.
7
+
8
+ It supports multiple plot types, including:
9
+ - single and multi-series line or scatter plots,
10
+ - directed (arrowed) line plots,
11
+ - dual y-axis plots,
12
+ - tornado plots for sensitivity-style visualization,
13
+ - 3D scatter plots with scalar coloring,
14
+ - 2D aggregated heatmaps projected from 3D data.
15
+
16
+ Columns are selected using simple 1-based column tokens (e.g. c1, c2, c3),
17
+ making the workflow suitable for rapid visualization of simulation outputs,
18
+ summaries, and post-processed analysis tables.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ from pathlib import Path
25
+ from typing import List, Dict, Any, Sequence
26
+
27
+ import numpy as np
28
+ import pandas as pd
29
+
30
+ from reaxkit.utils.media.plotter import (
31
+ single_plot,
32
+ directed_plot,
33
+ dual_yaxis_plot,
34
+ tornado_plot,
35
+ scatter3d_points,
36
+ heatmap2d_from_3d,
37
+ )
38
+
39
+
40
+ # ---------- Helpers ----------
41
+
42
+ def _load_table(path: str | Path) -> pd.DataFrame:
43
+ path = Path(path)
44
+ suf = path.suffix.lower()
45
+
46
+ # 1) CSV / TSV
47
+ if suf == ".csv":
48
+ return pd.read_csv(path, header=None, comment="#", engine="python")
49
+ if suf in {".tsv", ".tab"}:
50
+ return pd.read_csv(path, sep="\t", header=None, comment="#", engine="python")
51
+
52
+ # 2) Generic text: try whitespace first (summary.txt style)
53
+ try:
54
+ return pd.read_csv(
55
+ path,
56
+ sep=r"\s+",
57
+ engine="python",
58
+ header=None,
59
+ comment="#",
60
+ on_bad_lines="skip",
61
+ )
62
+ except Exception:
63
+ # 3) Last resort: let pandas sniff delimiter
64
+ return pd.read_csv(
65
+ path,
66
+ sep=None,
67
+ engine="python",
68
+ header=None,
69
+ comment="#",
70
+ on_bad_lines="skip",
71
+ )
72
+
73
+
74
+
75
+ def _parse_col_token(token: str) -> int:
76
+ """
77
+ Parse a column selector like 'c1', 'c2', '3', etc. into a zero-based index.
78
+
79
+ - 'c1' -> 0
80
+ - 'c3' -> 2
81
+ - '1' -> 0
82
+ - '3' -> 2
83
+ """
84
+ t = token.strip().lower()
85
+ if t.startswith("c"):
86
+ t = t[1:]
87
+ if not t:
88
+ raise ValueError(f"Invalid column token: '{token}'")
89
+ idx = int(t) - 1
90
+ if idx < 0:
91
+ raise ValueError(f"Column indices are 1-based: got '{token}'")
92
+ return idx
93
+
94
+
95
+ def _parse_col_list(spec: str) -> List[int]:
96
+ """
97
+ Parse a comma-separated list like 'c1,c3' -> [0, 2].
98
+ """
99
+ tokens = [s for s in spec.split(",") if s.strip()]
100
+ if not tokens:
101
+ raise ValueError(f"Empty column specification: '{spec}'")
102
+ return [_parse_col_token(tok) for tok in tokens]
103
+
104
+
105
+ def _extract_numeric_subframe(df_raw: pd.DataFrame, col_indices: Sequence[int]) -> pd.DataFrame:
106
+ """
107
+ From a raw DataFrame, take the subset of columns with the given indices and
108
+ convert each to numeric, coercing invalid entries to NaN.
109
+
110
+ Returns a new DataFrame whose columns are named 'c1', 'c2', ... in the sense
111
+ of 1-based original indices, and which has rows where **any** of the selected
112
+ columns is NaN dropped (so all selected columns are aligned).
113
+ """
114
+ cols_unique = sorted(set(col_indices))
115
+ data: Dict[str, Any] = {}
116
+ for idx in cols_unique:
117
+ if idx >= df_raw.shape[1]:
118
+ raise IndexError(
119
+ f"Requested column c{idx + 1}, but input file has only "
120
+ f"{df_raw.shape[1]} columns."
121
+ )
122
+ name = f"c{idx + 1}"
123
+ data[name] = pd.to_numeric(df_raw.iloc[:, idx], errors="coerce")
124
+
125
+ sub = pd.DataFrame(data)
126
+ # Drop rows where any selected column is NaN (header lines, malformed rows, etc.)
127
+ sub = sub.dropna(how="any").reset_index(drop=True)
128
+ if sub.empty:
129
+ raise ValueError("After cleaning, no valid numeric rows remain to plot.")
130
+ return sub
131
+
132
+
133
+ # ---------- Tasks ----------
134
+
135
+ def _plotter_single_task(args: argparse.Namespace) -> int:
136
+ """
137
+ reaxkit plotter single --file summary.txt --xaxis c1,c3 --yaxis c2,c4 [--save out.png] [--scatter]
138
+
139
+ - If len(xaxis) == len(yaxis): pair-wise (x_i, y_i) series.
140
+ - If len(xaxis) == 1 and len(yaxis) >= 1: use the same x for all y columns.
141
+ """
142
+ df_raw = _load_table(args.file)
143
+ x_indices = _parse_col_list(args.xaxis)
144
+ y_indices = _parse_col_list(args.yaxis)
145
+ all_indices = x_indices + y_indices
146
+ sub = _extract_numeric_subframe(df_raw, all_indices)
147
+
148
+ # Build series list for single_plot
149
+ series: List[Dict[str, Any]] = []
150
+
151
+ def col_name(idx: int) -> str:
152
+ return f"c{idx + 1}"
153
+
154
+ x_names = [col_name(i) for i in x_indices]
155
+ y_names = [col_name(i) for i in y_indices]
156
+
157
+ # Series construction
158
+ if len(x_indices) == len(y_indices):
159
+ for xi, yi in zip(x_indices, y_indices):
160
+ xn = col_name(xi)
161
+ yn = col_name(yi)
162
+ series.append({
163
+ "x": sub[xn],
164
+ "y": sub[yn],
165
+ "label": f"{yn} vs {xn}",
166
+ })
167
+ elif len(x_indices) == 1 and len(y_indices) >= 1:
168
+ xi = x_indices[0]
169
+ xn = col_name(xi)
170
+ x_series = sub[xn]
171
+ for yi in y_indices:
172
+ yn = col_name(yi)
173
+ series.append({
174
+ "x": x_series,
175
+ "y": sub[yn],
176
+ "label": f"{yn} vs {xn}",
177
+ })
178
+ else:
179
+ raise ValueError(
180
+ "For 'single' plot: either provide the same number of x and y columns "
181
+ "OR one x column with multiple y columns."
182
+ )
183
+
184
+ plot_type = "scatter" if args.scatter else "line"
185
+ title = args.title or "single_plot"
186
+
187
+ if not args.plot and not args.save:
188
+ print("Nothing to do. Use --plot to display or --save filename to export.")
189
+ return 0
190
+
191
+ if args.plot:
192
+ single_plot(
193
+ series=series,
194
+ title=title,
195
+ xlabel=args.xlabel or "x",
196
+ ylabel=args.ylabel or "y",
197
+ legend=True,
198
+ plot_type=plot_type,
199
+ )
200
+ return 0
201
+ elif args.save:
202
+ single_plot(
203
+ series=series,
204
+ title=title,
205
+ xlabel=args.xlabel or "x",
206
+ ylabel=args.ylabel or "y",
207
+ legend=True,
208
+ save=args.save,
209
+ plot_type=plot_type,
210
+ )
211
+ return 0
212
+
213
+
214
+ def _plotter_directed_task(args: argparse.Namespace) -> int:
215
+ """
216
+ reaxkit plotter directed --file summary.txt --xaxis c1 --yaxis c2 [--save dir.png]
217
+ """
218
+ df_raw = _load_table(args.file)
219
+ x_idx = _parse_col_token(args.xaxis)
220
+ y_idx = _parse_col_token(args.yaxis)
221
+ sub = _extract_numeric_subframe(df_raw, [x_idx, y_idx])
222
+
223
+ xn = f"c{x_idx + 1}"
224
+ yn = f"c{y_idx + 1}"
225
+
226
+ if not args.plot and not args.save:
227
+ print("Nothing to do. Use --plot to display or --save filename to export.")
228
+ return 0
229
+
230
+ if args.plot:
231
+ directed_plot(
232
+ x=sub[xn].to_numpy(),
233
+ y=sub[yn].to_numpy(),
234
+ title=args.title or "directed_plot",
235
+ xlabel=args.xlabel or xn,
236
+ ylabel=args.ylabel or yn,
237
+ )
238
+ return 0
239
+ elif args.save:
240
+ directed_plot(
241
+ x=sub[xn].to_numpy(),
242
+ y=sub[yn].to_numpy(),
243
+ title=args.title or "directed_plot",
244
+ xlabel=args.xlabel or xn,
245
+ ylabel=args.ylabel or yn,
246
+ save=args.save,
247
+ )
248
+ return 0
249
+
250
+
251
+ def _plotter_dual_task(args: argparse.Namespace) -> int:
252
+ """
253
+ reaxkit plotter dual --file summary.txt --xaxis c1 --y1 c2 --y2 c3 [--save dual.png]
254
+ """
255
+ df_raw = _load_table(args.file)
256
+ x_idx = _parse_col_token(args.xaxis)
257
+ y1_idx = _parse_col_token(args.y1)
258
+ y2_idx = _parse_col_token(args.y2)
259
+
260
+ sub = _extract_numeric_subframe(df_raw, [x_idx, y1_idx, y2_idx])
261
+
262
+ xn = f"c{x_idx + 1}"
263
+ y1n = f"c{y1_idx + 1}"
264
+ y2n = f"c{y2_idx + 1}"
265
+
266
+ if not args.plot and not args.save:
267
+ print("Nothing to do. Use --plot to display or --save filename to export.")
268
+ return 0
269
+
270
+ if args.plot:
271
+ dual_yaxis_plot(
272
+ x=sub[xn].to_numpy(),
273
+ y1=sub[y1n].to_numpy(),
274
+ y2=sub[y2n].to_numpy(),
275
+ title=args.title or "dual_yaxis_plot",
276
+ xlabel=args.xlabel or xn,
277
+ ylabel1=args.ylabel1 or y1n,
278
+ ylabel2=args.ylabel2 or y2n,
279
+ )
280
+ return 0
281
+ elif args.save:
282
+ dual_yaxis_plot(
283
+ x=sub[xn].to_numpy(),
284
+ y1=sub[y1n].to_numpy(),
285
+ y2=sub[y2n].to_numpy(),
286
+ title=args.title or "dual_yaxis_plot",
287
+ xlabel=args.xlabel or xn,
288
+ ylabel1=args.ylabel1 or y1n,
289
+ ylabel2=args.ylabel2 or y2n,
290
+ save=args.save,
291
+ )
292
+ return 0
293
+
294
+
295
+ def _plotter_tornado_task(args: argparse.Namespace) -> int:
296
+ """
297
+ reaxkit plotter tornado --file summary.txt --label c1 --min c2 --max c3 [--median c4]
298
+ [--top 10] [--vline 0.0] [--save out.png]
299
+ """
300
+ df_raw = _load_table(args.file)
301
+
302
+ label_idx = _parse_col_token(args.label)
303
+ min_idx = _parse_col_token(args.min)
304
+ max_idx = _parse_col_token(args.max)
305
+ median_idx = _parse_col_token(args.median) if args.median else None
306
+
307
+ # Labels stay as strings
308
+ labels = df_raw.iloc[:, label_idx].astype(str)
309
+
310
+ # Numeric columns
311
+ min_series = pd.to_numeric(df_raw.iloc[:, min_idx], errors="coerce")
312
+ max_series = pd.to_numeric(df_raw.iloc[:, max_idx], errors="coerce")
313
+ median_series = (
314
+ pd.to_numeric(df_raw.iloc[:, median_idx], errors="coerce")
315
+ if median_idx is not None
316
+ else None
317
+ )
318
+
319
+ data = pd.DataFrame({
320
+ "label": labels,
321
+ "min": min_series,
322
+ "max": max_series,
323
+ })
324
+ if median_series is not None:
325
+ data["median"] = median_series
326
+
327
+ # Drop rows where min or max is NaN
328
+ data = data.dropna(subset=["min", "max"]).reset_index(drop=True)
329
+ if data.empty:
330
+ raise ValueError("No valid rows for tornado plot after cleaning.")
331
+
332
+ median_vals = data["median"] if "median" in data.columns else None
333
+
334
+ if not args.plot and not args.save:
335
+ print("Nothing to do. Use --plot to display or --save filename to export.")
336
+ return 0
337
+
338
+ if args.plot:
339
+ tornado_plot(
340
+ labels=data["label"].tolist(),
341
+ min_vals=data["min"].to_numpy(),
342
+ max_vals=data["max"].to_numpy(),
343
+ median_vals=median_vals.to_numpy() if median_vals is not None else None,
344
+ title=args.title or "Tornado Plot",
345
+ xlabel=args.xlabel or "Value",
346
+ ylabel=args.ylabel or "Parameter",
347
+ top=args.top or 0,
348
+ vline=args.vline,
349
+ )
350
+ return 0
351
+ elif args.save:
352
+ tornado_plot(
353
+ labels=data["label"].tolist(),
354
+ min_vals=data["min"].to_numpy(),
355
+ max_vals=data["max"].to_numpy(),
356
+ median_vals=median_vals.to_numpy() if median_vals is not None else None,
357
+ title=args.title or "Tornado Plot",
358
+ xlabel=args.xlabel or "Value",
359
+ ylabel=args.ylabel or "Parameter",
360
+ top=args.top or 0,
361
+ vline=args.vline,
362
+ save=args.save,
363
+ )
364
+ return 0
365
+
366
+
367
+ def _plotter_scatter3d_task(args: argparse.Namespace) -> int:
368
+ """
369
+ reaxkit plotter scatter3d --file summary.txt --x c1 --y c2 --z c3 --value c4 [--save out.png]
370
+ """
371
+ df_raw = _load_table(args.file)
372
+ x_idx = _parse_col_token(args.x)
373
+ y_idx = _parse_col_token(args.y)
374
+ z_idx = _parse_col_token(args.z)
375
+ v_idx = _parse_col_token(args.value)
376
+
377
+ sub = _extract_numeric_subframe(df_raw, [x_idx, y_idx, z_idx, v_idx])
378
+
379
+ xn = f"c{x_idx + 1}"
380
+ yn = f"c{y_idx + 1}"
381
+ zn = f"c{z_idx + 1}"
382
+ vn = f"c{v_idx + 1}"
383
+
384
+ coords = np.column_stack(
385
+ [sub[xn].to_numpy(), sub[yn].to_numpy(), sub[zn].to_numpy()]
386
+ )
387
+ values = sub[vn].to_numpy()
388
+
389
+ if not args.plot and not args.save:
390
+ print("Nothing to do. Use --plot to display or --save filename to export.")
391
+ return 0
392
+
393
+ if args.plot:
394
+ scatter3d_points(
395
+ coords=coords,
396
+ values=values,
397
+ title=args.title or "scatter3d",
398
+ )
399
+ return 0
400
+ elif args.save:
401
+ scatter3d_points(
402
+ coords=coords,
403
+ values=values,
404
+ title=args.title or "scatter3d",
405
+ save=args.save,
406
+ )
407
+ return 0
408
+
409
+
410
+ def _plotter_heatmap2d_task(args: argparse.Namespace) -> int:
411
+ """
412
+ reaxkit plotter heatmap2d --file summary.txt --x c1 --y c2 --z c3 --value c4
413
+ [--plane xy|xz|yz] [--bins 50] [--save out.png]
414
+
415
+ Uses heatmap2d_from_3d under the hood.
416
+ """
417
+ df_raw = _load_table(args.file)
418
+ x_idx = _parse_col_token(args.x)
419
+ y_idx = _parse_col_token(args.y)
420
+ z_idx = _parse_col_token(args.z)
421
+ v_idx = _parse_col_token(args.value)
422
+
423
+ sub = _extract_numeric_subframe(df_raw, [x_idx, y_idx, z_idx, v_idx])
424
+
425
+ xn = f"c{x_idx + 1}"
426
+ yn = f"c{y_idx + 1}"
427
+ zn = f"c{z_idx + 1}"
428
+ vn = f"c{v_idx + 1}"
429
+
430
+ coords = np.column_stack(
431
+ [sub[xn].to_numpy(), sub[yn].to_numpy(), sub[zn].to_numpy()]
432
+ )
433
+ values = sub[vn].to_numpy()
434
+
435
+ bins = args.bins
436
+ if "," in str(bins):
437
+ # allow e.g. "--bins 50,100"
438
+ bx, by = (int(p.strip()) for p in str(bins).split(","))
439
+ bins_arg = (bx, by)
440
+ else:
441
+ bins_arg = int(bins)
442
+
443
+ if not args.plot and not args.save:
444
+ print("Nothing to do. Use --plot to display or --save filename to export.")
445
+ return 0
446
+
447
+ if args.plot:
448
+ heatmap2d_from_3d(
449
+ coords=coords,
450
+ values=values,
451
+ plane=args.plane,
452
+ bins=bins_arg,
453
+ title=args.title or "2D aggregated heatmap",
454
+ save=args.save,
455
+ )
456
+ return 0
457
+ elif args.save:
458
+ heatmap2d_from_3d(
459
+ coords=coords,
460
+ values=values,
461
+ plane=args.plane,
462
+ bins=bins_arg,
463
+ title=args.title or "2D aggregated heatmap",
464
+ save=args.save,
465
+ )
466
+ return 0
467
+
468
+ # ---------- Registration ----------
469
+
470
+ def _add_common_io_args(p: argparse.ArgumentParser) -> None:
471
+ p.add_argument(
472
+ "--file",
473
+ required=True,
474
+ help="Path to input txt/csv/tsv table.",
475
+ )
476
+ p.add_argument(
477
+ "--save",
478
+ default=None,
479
+ help="Path to save plot (file or directory). If omitted, show interactively.",
480
+ )
481
+ p.add_argument(
482
+ "--title",
483
+ default=None,
484
+ help="Optional custom plot title.",
485
+ )
486
+ p.add_argument(
487
+ "--plot",
488
+ action="store_true",
489
+ help="Generate and display/save the plot",
490
+ )
491
+
492
+
493
+ def register_tasks(subparsers: argparse._SubParsersAction) -> None:
494
+ """
495
+ Register coordination-related CLI subcommands.
496
+
497
+ This function defines the task-level interface for the
498
+ `reaxkit file` workflow and attaches its tasks (i.e., subcommands).
499
+
500
+ Each task may share common input arguments and defines task-specific options as needed.
501
+ """
502
+
503
+ # ---- single ----
504
+ p_single = subparsers.add_parser(
505
+ "single",
506
+ help="Plot one or multiple y columns vs x columns (line or scatter). || "
507
+ "reaxkit plotter single --file summary.txt --xaxis c1 --yaxis c2 --plot || "
508
+ "reaxkit plotter single --file summary.txt --xaxis c1 --yaxis c2 --scatter --plot || "
509
+ "reaxkit plotter single --file summary.txt --xaxis c1,c3 --yaxis c2,c4 --plot || "
510
+ "reaxkit plotter single --file summary.txt --xaxis c1 --yaxis c2,c3,c4 --scatter --plot",
511
+ )
512
+ _add_common_io_args(p_single)
513
+ p_single.add_argument("--xaxis", required=True,
514
+ help="Comma-separated list of x columns (e.g., 'c1' or 'c1,c3').",
515
+ )
516
+ p_single.add_argument("--yaxis", required=True,
517
+ help="Comma-separated list of y columns (e.g., 'c2' or 'c2,c4').",
518
+ )
519
+ p_single.add_argument("--xlabel", default=None, help="Optional x-axis label.")
520
+ p_single.add_argument("--ylabel", default=None, help="Optional y-axis label.")
521
+ p_single.add_argument("--scatter", action="store_true", help="Use scatter instead of line plot.")
522
+ p_single.set_defaults(_run=_plotter_single_task)
523
+
524
+ # ---- directed ----
525
+ p_directed = subparsers.add_parser(
526
+ "directed",
527
+ help="Line plot with arrows showing direction along the path. || "
528
+ "reaxkit plotter directed --file summary.txt --xaxis c1 --yaxis c2 --save directed.png",
529
+ )
530
+ _add_common_io_args(p_directed)
531
+ p_directed.add_argument("--xaxis", required=True, help="Single x column (e.g., 'c1').")
532
+ p_directed.add_argument("--yaxis", required=True, help="Single y column (e.g., 'c2').")
533
+ p_directed.add_argument("--xlabel", default=None, help="Optional x-axis label.")
534
+ p_directed.add_argument("--ylabel", default=None, help="Optional y-axis label.")
535
+ p_directed.set_defaults(_run=_plotter_directed_task)
536
+
537
+ # ---- dual ----
538
+ p_dual = subparsers.add_parser(
539
+ "dual",
540
+ help="Dual y-axis plot: one x column, two y columns. || "
541
+ "reaxkit plotter dual --file summary.txt --xaxis c1 --y1 c2 --y2 c3 --save dual_plot.png",
542
+ )
543
+ _add_common_io_args(p_dual)
544
+ p_dual.add_argument("--xaxis", required=True, help="Single x column (e.g., 'c1').")
545
+ p_dual.add_argument("--y1", required=True, help="Left y-axis column (e.g., 'c2').")
546
+ p_dual.add_argument("--y2", required=True, help="Right y-axis column (e.g., 'c3').")
547
+ p_dual.add_argument("--xlabel", default=None, help="Optional x-axis label.")
548
+ p_dual.add_argument("--ylabel1", default=None, help="Optional left y-axis label.")
549
+ p_dual.add_argument("--ylabel2", default=None, help="Optional right y-axis label.")
550
+ p_dual.set_defaults(_run=_plotter_dual_task)
551
+
552
+ # ---- tornado ----
553
+ p_tornado = subparsers.add_parser(
554
+ "tornado",
555
+ help="Tornado plot: label + min/max (and optional median). || "
556
+ "reaxkit plotter tornado --file summary.txt --label c1 --min c2 --max c3 --median c4 --top 10 --save tornado.png || "
557
+ "reaxkit plotter tornado --file summary.txt --label c1 --min c2 --max c3",
558
+ )
559
+ _add_common_io_args(p_tornado)
560
+ p_tornado.add_argument("--label", required=True, help="Column for labels (e.g., 'c1').")
561
+ p_tornado.add_argument("--min", required=True, help="Column for minimum values (e.g., 'c2').")
562
+ p_tornado.add_argument("--max", required=True, help="Column for maximum values (e.g., 'c3').")
563
+ p_tornado.add_argument("--median", default=None, help="Optional column for median values (e.g., 'c4').")
564
+ p_tornado.add_argument("--top", type=int, default=0, help="Show only top-N widest bars (0 = all).")
565
+ p_tornado.add_argument("--vline", type=float, default=None, help="Optional vertical reference line (e.g., 0.0).")
566
+ p_tornado.add_argument("--xlabel", default=None, help="Optional x-axis label.")
567
+ p_tornado.add_argument("--ylabel", default=None, help="Optional y-axis label.")
568
+ p_tornado.set_defaults(_run=_plotter_tornado_task)
569
+
570
+ # ---- scatter3d ----
571
+ p_scatter3d = subparsers.add_parser(
572
+ "scatter3d",
573
+ help="3D scatter of (x,y,z) points colored by a value. || "
574
+ "reaxkit plotter scatter3d --file summary.txt --x c1 --y c2 --z c3 --value c4 --save 3dscatter.png",
575
+ )
576
+ _add_common_io_args(p_scatter3d)
577
+ p_scatter3d.add_argument("--x", required=True, help="x coordinate column (e.g., 'c1').")
578
+ p_scatter3d.add_argument("--y", required=True, help="y coordinate column (e.g., 'c2').")
579
+ p_scatter3d.add_argument("--z", required=True, help="z coordinate column (e.g., 'c3').")
580
+ p_scatter3d.add_argument("--value", required=True, help="Value column for coloring (e.g., 'c4').")
581
+ p_scatter3d.set_defaults(_run=_plotter_scatter3d_task)
582
+
583
+ # ---- heatmap2d ----
584
+ p_heatmap2d = subparsers.add_parser(
585
+ "heatmap2d",
586
+ help="2D heatmap from 3D coords + values (projection plane selectable). || "
587
+ "reaxkit plotter heatmap2d --file summary.txt --x c1 --y c2 --z c3 --value c4 --plane xz "
588
+ "--bins 100,80 --save heat_xz.png",
589
+ )
590
+ _add_common_io_args(p_heatmap2d)
591
+ p_heatmap2d.add_argument("--x", required=True, help="x coordinate column (e.g., 'c1').")
592
+ p_heatmap2d.add_argument("--y", required=True, help="y coordinate column (e.g., 'c2').")
593
+ p_heatmap2d.add_argument("--z", required=True, help="z coordinate column (e.g., 'c3').")
594
+ p_heatmap2d.add_argument("--value", required=True, help="value column to aggregate (e.g., 'c4').")
595
+ p_heatmap2d.add_argument("--plane", choices=["xy", "xz", "yz"], default="xy",
596
+ help="Projection plane for heatmap (default: xy).",
597
+ )
598
+ p_heatmap2d.add_argument("--bins", default="50",
599
+ help="Grid resolution: int (e.g., 50) or 'nx,ny' (e.g., '50,100').",
600
+ )
601
+ p_heatmap2d.set_defaults(_run=_plotter_heatmap2d_task)
File without changes