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,587 @@
1
+ """
2
+ Electrostatics workflow for ReaxKit.
3
+
4
+ This workflow provides tools for computing and analyzing electrostatic properties
5
+ from ReaxFF simulations, including dipole moments, polarizations, and
6
+ polarization–electric-field hysteresis behavior.
7
+
8
+ It supports:
9
+ - Dipole moment and polarization calculations for single frames, at both
10
+ total-system and local (core-atom cluster) levels.
11
+ - Polarization–field hysteresis analysis using time-dependent electric fields
12
+ (via fort.78 and control files), including extraction of coercive fields and
13
+ remnant polarizations.
14
+ - Visualization of local electrostatics through 3D scatter plots and 2D
15
+ projected heatmaps based on atomic coordinates.
16
+
17
+ The workflow integrates xmolout, fort.7, fort.78, and control files, and is
18
+ designed to bridge atomistic ReaxFF simulations with experiment-facing
19
+ electrostatic observables.
20
+ """
21
+
22
+
23
+ from __future__ import annotations
24
+ import argparse
25
+ from pathlib import Path
26
+ from typing import Optional, Sequence, Tuple, Union
27
+
28
+ import numpy as np
29
+ import pandas as pd
30
+
31
+ from reaxkit.io.handlers.xmolout_handler import XmoloutHandler
32
+ from reaxkit.io.handlers.fort7_handler import Fort7Handler
33
+ from reaxkit.io.handlers.fort78_handler import Fort78Handler
34
+ from reaxkit.io.handlers.control_handler import ControlHandler
35
+ from reaxkit.analysis.per_file.xmolout_analyzer import get_atom_trajectories
36
+ from reaxkit.utils.media.plotter import scatter3d_points, heatmap2d_from_3d
37
+ from reaxkit.utils.frame_utils import parse_frames, resolve_indices
38
+ from reaxkit.utils.alias import resolve_alias_from_columns
39
+ from reaxkit.utils.path import resolve_output_path
40
+ from reaxkit.utils.media.plotter import single_plot
41
+
42
+ from reaxkit.utils.alias import (
43
+ normalize_choice,
44
+ _resolve_alias,
45
+ )
46
+ from reaxkit.analysis.composed.electrostatics_analyzer import (
47
+ dipoles_polarizations_over_multiple_frames,
48
+ polarization_field_analysis,
49
+ )
50
+
51
+
52
+ # -------------------------------------------------------------------------
53
+ # Tasks
54
+ # -------------------------------------------------------------------------
55
+
56
+
57
+ def _dipole_task(args: argparse.Namespace) -> int:
58
+ """
59
+ reaxkit elect dipole
60
+ --xmolout xmolout --fort7 fort.7
61
+ --frame X --scope {total,local}
62
+ [--core Al,Mg] --export out.csv [--polarization]
63
+
64
+ total scope:
65
+ exports total dipole or polarization for a single frame.
66
+
67
+ local scope:
68
+ exports local dipole or polarization for each core atom cluster for a single frame.
69
+ NOTE: local polarization uses bbox volume by default (fast). Use hull only if you add an option.
70
+ """
71
+ # Handlers
72
+ xh = XmoloutHandler(args.xmolout)
73
+ f7 = Fort7Handler(args.fort7)
74
+
75
+ # Mode: dipole vs polarization
76
+ mode = "polarization" if args.polarization else "dipole"
77
+
78
+ # Scope/core types
79
+ scope = args.scope
80
+ core_types: Optional[list[str]] = None
81
+ if scope == "local":
82
+ if not args.core:
83
+ raise ValueError("When --scope local is used, --core must be provided (e.g. --core Al,Mg).")
84
+ core_types = [c.strip() for c in args.core.split(",") if c.strip()]
85
+
86
+ # Volume method policy (per your new analyzer design)
87
+ # - total polarization: hull (reasonable)
88
+ # - local polarization: bbox (fast default)
89
+ volume_method = None
90
+ if mode == "polarization":
91
+ volume_method = "bbox" if scope == "local" else "hull"
92
+
93
+ # Compute for the requested single frame (by passing frames=[...])
94
+ df = dipoles_polarizations_over_multiple_frames(
95
+ xh,
96
+ f7,
97
+ frames=[args.frame],
98
+ scope=scope,
99
+ core_types=core_types,
100
+ mode=mode,
101
+ volume_method=volume_method,
102
+ )
103
+
104
+ workflow_name = args.kind
105
+
106
+ # Export
107
+ out = resolve_output_path(args.export, workflow_name)
108
+ df.to_csv(out, index=False)
109
+ print(f"\n[Done] Exported data to {out}")
110
+
111
+ return 0
112
+
113
+
114
+ def _hyst_task(args: argparse.Namespace) -> int:
115
+ """
116
+ reaxkit elect hyst
117
+ --xmolout xmolout --fort7 fort.7 --fort78 fort.78 --control control
118
+ [--plot] [--save hyst.png]
119
+ [--yaxis pol_z] [--xaxis time|field_z]
120
+ [--aggregate mean|max|min|last]
121
+ [--export hysteresis.csv]
122
+ [--summary hysteresis_summary.txt]
123
+ [--roots]
124
+
125
+ - Uses polarization_field_analysis to build polarization + field dataset.
126
+ - Aggregates according to --aggregate.
127
+ - Exports aggregated joint DataFrame (if --export is given).
128
+ - Writes coercive fields and remnant polarizations to a text file (summary).
129
+ - If --roots is present, also prints these values to the terminal.
130
+ - Makes a hysteresis / time-series plot according to --xaxis/--yaxis
131
+ and plot/save flags, using alias_utils to map names.
132
+ """
133
+ # Handlers
134
+ xh = XmoloutHandler(args.xmolout)
135
+ f7 = Fort7Handler(args.fort7)
136
+ f78 = Fort78Handler(args.fort78)
137
+ ctrl = ControlHandler(args.control)
138
+
139
+ # Run basic polarization vs field analysis; keep x/y for roots as field_z vs P_z
140
+ full_df, agg_df, coercive_fields, remnant_pols = polarization_field_analysis(
141
+ xh,
142
+ f7,
143
+ f78,
144
+ ctrl,
145
+ field_var="field_z",
146
+ aggregate=args.aggregate,
147
+ x_variable="field_z",
148
+ y_variable="P_z (uC/cm^2)",
149
+ )
150
+
151
+ # ------------------------------------------------------------------
152
+ # Optionally add time information to full_df (for plotting vs time)
153
+ # ------------------------------------------------------------------
154
+ try:
155
+ sim_df = xh.dataframe()
156
+ iter_col = _resolve_alias(sim_df, "iter")
157
+ time_col_name = _resolve_alias(sim_df, "time")
158
+ sim_small = sim_df[[iter_col, time_col_name]].copy()
159
+ sim_small = sim_small.rename(columns={iter_col: "iter", time_col_name: "time"})
160
+ full_df = full_df.merge(sim_small, on="iter", how="left")
161
+ except Exception:
162
+ # If no time is resolvable, we simply skip it; plotting vs time may fail later.
163
+ pass
164
+
165
+ # ------------------------------------------------------------------
166
+ # Export aggregated joint DataFrame
167
+ # ------------------------------------------------------------------
168
+ workflow_name = args.kind
169
+ if args.export:
170
+ # Export aggregated data (agg_df)
171
+ out_csv = resolve_output_path(args.export, workflow_name)
172
+ agg_df.to_csv(out_csv, index=False)
173
+ print(f"\n[Done] Exported aggregated joint hysteresis data to {out_csv}")
174
+
175
+ # Export full dataframe in same directory
176
+ full_save_dir = out_csv.parent # Directory where --export was saved
177
+ full_save_dir.mkdir(parents=True, exist_ok=True) # Ensure directory exists
178
+ full_save_path = full_save_dir / "hysteresis_full_data.csv"
179
+ full_df.to_csv(full_save_path, index=False)
180
+ print(f"[Done] Exported full hysteresis dataset to {full_save_path}\n")
181
+
182
+ # ------------------------------------------------------------------
183
+ # Write coercive fields & remnant polarizations to a text file
184
+ # ------------------------------------------------------------------
185
+ summary_path = Path(args.summary or "hysteresis_summary.txt")
186
+
187
+ def _fmt_list(values) -> str:
188
+ if not values:
189
+ return "None found"
190
+ return ", ".join(f"{v:.6g}" for v in values)
191
+
192
+ with summary_path.open("w", encoding="utf-8") as f:
193
+ f.write("Hysteresis Analysis Summary\n")
194
+ f.write("===========================\n\n")
195
+ f.write("Coercive fields (where polarization crosses zero vs field_z)\n")
196
+ f.write("Units: MV/cm\n")
197
+ f.write(f"Values: {_fmt_list(coercive_fields)}\n\n")
198
+ f.write("Remnant polarizations (where field_z crosses zero vs P_z)\n")
199
+ f.write("Units: µC/cm^2\n")
200
+ f.write(f"Values: {_fmt_list(remnant_pols)}\n")
201
+
202
+ print(f"[Done] Wrote hysteresis summary to {summary_path}")
203
+
204
+ # Also print to terminal if --roots is requested
205
+ if args.roots:
206
+ print("\n[Hysteresis roots]")
207
+ print(" Coercive fields (MV/cm):", _fmt_list(coercive_fields))
208
+ print(" Remnant polarizations (µC/cm^2):", _fmt_list(remnant_pols))
209
+
210
+ # ------------------------------------------------------------------
211
+ # Plot: use alias_utils to map --xaxis / --yaxis
212
+ # - If xaxis == time → use per-frame data (full_df)
213
+ # - Else → use aggregated data (agg_df) for hysteresis
214
+ # ------------------------------------------------------------------
215
+ if args.plot or args.save:
216
+ # Canonical keys
217
+ canonical_x = normalize_choice(args.xaxis or "field_z", domain="xaxis")
218
+ canonical_y = normalize_choice(args.yaxis or "P_z (uC/cm^2)", domain="yaxis")
219
+
220
+ # Choose DataFrame for plotting
221
+ if canonical_x == "time":
222
+ df_plot = full_df
223
+ else:
224
+ df_plot = agg_df
225
+
226
+ # Resolve actual column names present in df_plot
227
+ x_col = _resolve_alias(df_plot, canonical_x)
228
+ y_col = _resolve_alias(df_plot, canonical_y)
229
+
230
+ x = df_plot[x_col]
231
+ y = df_plot[y_col]
232
+
233
+ title = f"{y_col} vs {x_col}"
234
+ xlabel = x_col
235
+ ylabel = y_col
236
+
237
+ single_plot(x, y, title=title, xlabel=xlabel, ylabel=ylabel, save=args.save, figsize=(6,4))
238
+
239
+ return 0
240
+
241
+ # ------------------------- internals -------------------------
242
+
243
+ def _indices_from_spec(xh: XmoloutHandler, frames_spec) -> list[int]:
244
+ """Turn a frames spec (slice or list) into concrete frame indices for xh."""
245
+ return resolve_indices(xh, frames=frames_spec, iterations=None, step=None)
246
+
247
+
248
+ def _local_pol_with_coords(
249
+ xh: XmoloutHandler,
250
+ f7: Fort7Handler,
251
+ *,
252
+ core_types: Sequence[str],
253
+ frames_spec,
254
+ mode: str = "polarization", # or "dipole"
255
+ ) -> pd.DataFrame:
256
+ """
257
+ Build a DataFrame with local electrostatics + core-atom coordinates:
258
+
259
+ columns include:
260
+ frame_index, iter, core_atom_type, core_atom_id,
261
+ x, y, z,
262
+ mu_x (debye), mu_y (debye), mu_z (debye),
263
+ P_x (uC/cm^2), P_y (uC/cm^2), P_z (uC/cm^2), volume (angstrom^3)
264
+ """
265
+ # 1) local dipole/polarization over all frames
266
+ df_local = dipoles_polarizations_over_multiple_frames(
267
+ xh,
268
+ f7,
269
+ scope="local",
270
+ core_types=core_types,
271
+ mode=mode,
272
+ )
273
+ if df_local.empty:
274
+ raise ValueError("No local polarization/dipole data found. Check core types and files.")
275
+
276
+ # Restrict to requested frames
277
+ idx_list = _indices_from_spec(xh, frames_spec)
278
+ df_local = df_local[df_local["frame_index"].isin(idx_list)].copy()
279
+ if df_local.empty:
280
+ raise ValueError("No local data in the requested frames.")
281
+
282
+ # 2) coordinates of atoms from xmolout (long format)
283
+ traj = get_atom_trajectories(
284
+ xh,
285
+ frames=idx_list,
286
+ every=1,
287
+ atoms=None,
288
+ atom_types=None,
289
+ dims=("x", "y", "z"),
290
+ format="long",
291
+ )
292
+ if traj.empty:
293
+ raise ValueError("No trajectory data returned for requested frames.")
294
+
295
+ traj = traj[["frame_index", "atom_id", "x", "y", "z"]].copy()
296
+ traj = traj.rename(columns={"atom_id": "core_atom_id"})
297
+
298
+ # 3) join local pol/dipole with core-atom coordinates
299
+ df = pd.merge(
300
+ df_local,
301
+ traj,
302
+ on=["frame_index", "core_atom_id"],
303
+ how="inner",
304
+ )
305
+ if df.empty:
306
+ raise ValueError("Could not match local polarization entries to core atom coordinates.")
307
+
308
+ return df
309
+
310
+
311
+ # ========================= 3D SCATTER =========================
312
+
313
+ def _local_pol_plot3d_task(args: argparse.Namespace) -> int:
314
+ """
315
+ 3D scatter of local polarization/dipole (one point per core atom) across frames.
316
+
317
+ Example:
318
+ reaxkit elect plot3d --core Al,Mg --component pol_z --frames 0:200:20 --save figs/local_pol3d
319
+ """
320
+ xh = XmoloutHandler(args.xmolout)
321
+ f7 = Fort7Handler(args.fort7)
322
+
323
+ frames_spec = parse_frames(args.frames)
324
+ core_types = [c.strip() for c in args.core.split(",") if c.strip()]
325
+
326
+ df = _local_pol_with_coords(
327
+ xh,
328
+ f7,
329
+ core_types=core_types,
330
+ frames_spec=frames_spec,
331
+ mode="polarization" if args.polarization else "dipole",
332
+ )
333
+
334
+ # Resolve which column to color by (mu_z, P_z, etc.) using alias map
335
+ col = resolve_alias_from_columns(df.columns, args.component)
336
+ if col is None:
337
+ raise ValueError(
338
+ f"Component '{args.component}' not found. "
339
+ f"Available columns include: {list(df.columns)[:12]} ..."
340
+ )
341
+
342
+ # Output dir
343
+ save_dir = Path(args.save) if args.save else None
344
+ if save_dir:
345
+ save_dir.mkdir(parents=True, exist_ok=True)
346
+
347
+ # Per-frame 3D plot
348
+ for fi in sorted(df["frame_index"].unique()):
349
+ sub = df[df["frame_index"] == fi]
350
+ coords = sub[["x", "y", "z"]].to_numpy(float)
351
+ vals = sub[col].to_numpy(float)
352
+
353
+ # skip frames with all-NaN or empty
354
+ if coords.size == 0 or not np.isfinite(vals).any():
355
+ continue
356
+
357
+ vmin = args.vmin if args.vmin is not None else float(np.nanmin(vals))
358
+ vmax = args.vmax if args.vmax is not None else float(np.nanmax(vals))
359
+
360
+ title = f"{col}_local_3D_frame_{fi}"
361
+ scatter3d_points(
362
+ coords,
363
+ vals,
364
+ title=title,
365
+ s=args.size,
366
+ alpha=args.alpha,
367
+ cmap=args.cmap,
368
+ vmin=vmin,
369
+ vmax=vmax,
370
+ elev=args.elev,
371
+ azim=args.azim,
372
+ save=(save_dir / f"{title}.png" if save_dir else None),
373
+ show_message=False,
374
+ )
375
+
376
+ if args.save:
377
+ print(f"[Done] All 3D local polarization plots saved in {args.save}")
378
+ return 0
379
+
380
+
381
+ # ========================= 2D HEATMAP =========================
382
+
383
+ def _parse_bins(bins: str) -> Union[int, Tuple[int, int]]:
384
+ if "," in bins:
385
+ nx, ny = [int(x) for x in bins.split(",")]
386
+ return (nx, ny)
387
+ return int(bins)
388
+
389
+
390
+ def _local_pol_heatmap2d_task(args: argparse.Namespace) -> int:
391
+ """
392
+ 2D heatmap of local polarization/dipole on core-atom positions.
393
+
394
+ Example:
395
+ reaxkit elect heatmap2d --core Al,Mg --component pol_z --plane xz --bins 40 --frames 0:200:20 --save figs/local_pol_heat
396
+ """
397
+ xh = XmoloutHandler(args.xmolout)
398
+ f7 = Fort7Handler(args.fort7)
399
+
400
+ frames_spec = parse_frames(args.frames)
401
+ core_types = [c.strip() for c in args.core.split(",") if c.strip()]
402
+
403
+ df = _local_pol_with_coords(
404
+ xh,
405
+ f7,
406
+ core_types=core_types,
407
+ frames_spec=frames_spec,
408
+ mode="polarization" if args.polarization else "dipole",
409
+ )
410
+
411
+ col = resolve_alias_from_columns(df.columns, args.component)
412
+ if col is None:
413
+ raise ValueError(
414
+ f"Component '{args.component}' not found. "
415
+ f"Available columns include: {list(df.columns)[:12]} ..."
416
+ )
417
+
418
+ bins = _parse_bins(args.bins)
419
+
420
+ save_dir = Path(args.save) if args.save else None
421
+ if save_dir:
422
+ save_dir.mkdir(parents=True, exist_ok=True)
423
+
424
+ for fi in sorted(df["frame_index"].unique()):
425
+ sub = df[df["frame_index"] == fi]
426
+ coords = sub[["x", "y", "z"]].to_numpy(float)
427
+ vals = sub[col].to_numpy(float)
428
+
429
+ if coords.size == 0 or not np.isfinite(vals).any():
430
+ continue
431
+
432
+ m = np.isfinite(vals)
433
+ coords = coords[m]
434
+ vals = vals[m]
435
+
436
+ vmin = args.vmin if args.vmin is not None else float(np.nanmin(vals))
437
+ vmax = args.vmax if args.vmax is not None else float(np.nanmax(vals))
438
+
439
+ title = f"{col}_local_{args.plane}_frame_{fi}"
440
+ out_path = (save_dir / f"{title}.png") if save_dir else None
441
+
442
+ heatmap2d_from_3d(
443
+ coords,
444
+ vals,
445
+ plane=args.plane,
446
+ bins=bins,
447
+ agg=args.agg,
448
+ vmin=vmin,
449
+ vmax=vmax,
450
+ cmap=args.cmap,
451
+ title=title,
452
+ save=out_path,
453
+ show_message=False,
454
+ )
455
+
456
+ if args.save:
457
+ print(f"[Done] All 2D local polarization heatmaps saved in {args.save}")
458
+ return 0
459
+
460
+
461
+ # -------------------------------------------------------------------------
462
+ # Registration
463
+ # -------------------------------------------------------------------------
464
+
465
+ def register_tasks(subparsers: argparse._SubParsersAction) -> None:
466
+ """
467
+ Register coordination-related CLI subcommands.
468
+
469
+ This function defines the task-level interface for the
470
+ `reaxkit file` workflow and attaches its tasks (i.e., subcommands).
471
+
472
+ Each task may share common input arguments and defines task-specific options as needed.
473
+ """
474
+ # ---------------------- dipole ----------------------
475
+ p_dip = subparsers.add_parser(
476
+ "dipole",
477
+ help="Compute dipole moment or polarization for a single frame\n",
478
+ description=(
479
+ "Examples:\n"
480
+ " reaxkit elect dipole --frame 10 --scope total --export dipole_pol_total_frame10.csv --polarization \n"
481
+ " reaxkit elect dipole --frame 10 --scope local --core Al --export dipole_pol_local_frame10.csv --polarization \n"
482
+ ),
483
+ formatter_class=argparse.RawTextHelpFormatter,
484
+ )
485
+ p_dip.add_argument("--xmolout", default="xmolout", help="Path to xmolout file")
486
+ p_dip.add_argument("--fort7", default="fort.7", help="Path to fort.7 file")
487
+ p_dip.add_argument("--frame", type=int, required=True, help="0-based frame index in xmolout")
488
+ p_dip.add_argument("--scope", choices=["total", "local"],
489
+ default="total", help="Electrostatics scope: total or local")
490
+ p_dip.add_argument("--core", default=None,
491
+ help="Comma-separated core atom types for local scope (e.g. Al,Mg)")
492
+ p_dip.add_argument("--export", required=True, help="CSV file to export the dipole/polarization data")
493
+ p_dip.add_argument("--polarization", action="store_true",
494
+ help="If present, compute polarization instead of dipole")
495
+ p_dip.set_defaults(_run=_dipole_task)
496
+
497
+ # ---------------------- hysteresis ----------------------
498
+ p_hyst = subparsers.add_parser(
499
+ "hyst",
500
+ help="Polarization-field hysteresis analysis\n",
501
+ description=(
502
+ "Examples:\n"
503
+ " reaxkit elect hyst --plot --yaxis pol_z --xaxis field_z --aggregate mean --roots"
504
+ ),
505
+ formatter_class=argparse.RawTextHelpFormatter,
506
+ )
507
+ p_hyst.add_argument("--xmolout", default="xmolout", help="Path to xmolout file")
508
+ p_hyst.add_argument("--fort7", default="fort.7", help="Path to fort.7 file")
509
+ p_hyst.add_argument("--fort78", default="fort.78", help="Path to fort.78 file")
510
+ p_hyst.add_argument("--control", default="control", help="Path to control file")
511
+ p_hyst.add_argument("--plot", action="store_true", help="Show the hysteresis or time-series plot")
512
+ p_hyst.add_argument("--save", default='hysteresis_aggregated.png',
513
+ help="If set, save plot to file (e.g. hyst.png)")
514
+ p_hyst.add_argument("--export", default='hysteresis_aggregated.csv',
515
+ help="CSV file to export aggregated hysteresis data")
516
+ p_hyst.add_argument("--summary", default="hysteresis_summary.txt",
517
+ help="Text file to write coercive fields and remnant polarizations")
518
+ p_hyst.add_argument("--yaxis", default="pol_z", help="Quantity for y-axis (e.g. pol_z, mu_z, time, P_z)")
519
+ p_hyst.add_argument("--xaxis", default="field_z", help="Quantity for x-axis (e.g. field_z, time, iter)")
520
+ p_hyst.add_argument("--aggregate", choices=["mean", "max", "min", "last"],
521
+ default="mean", help="Aggregation method")
522
+
523
+ p_hyst.add_argument("--roots", action="store_true", help="Also print coercive and remnant values to terminal")
524
+ p_hyst.set_defaults(_run=_hyst_task)
525
+
526
+ # ---------------------- 3D local polarization scatter ----------------------
527
+ p3d = subparsers.add_parser(
528
+ "plot3d",
529
+ help="3D scatter of local polarization/dipole on core-atom positions\n",
530
+ description=(
531
+ "Examples:\n"
532
+ " reaxkit elect plot3d --core Al --component mu_z --frames 0:3:1 "
533
+ "--save reaxkit_outputs/elect/local_mu_3d/"
534
+ ),
535
+ formatter_class=argparse.RawTextHelpFormatter,
536
+ )
537
+ p3d.add_argument("--xmolout", default="xmolout", help="Path to xmolout file")
538
+ p3d.add_argument("--fort7", default="fort.7", help="Path to fort.7 file")
539
+ p3d.add_argument("--core", required=True, help="Comma-separated core atom types (e.g. Al,Mg)")
540
+ p3d.add_argument("--component", default="pol_z",
541
+ help="Which component to color by (e.g. pol_z, P_z (uC/cm^2), mu_z)")
542
+ p3d.add_argument("--frames", default=None, help='Frames: "0,10,20" or "0:100:5"')
543
+ p3d.add_argument("--polarization", action="store_true",
544
+ help="Use polarization components (P_x/y/z) instead of dipole")
545
+ p3d.add_argument("--save", default=None, help="Directory to save PNGs (one per frame)")
546
+ p3d.add_argument("--vmin", type=float, default=None, help="Color scale min (auto if not set)")
547
+ p3d.add_argument("--vmax", type=float, default=None, help="Color scale max (auto if not set)")
548
+ p3d.add_argument("--size", type=float, default=20.0, help="Marker size")
549
+ p3d.add_argument("--alpha", type=float, default=0.9, help="Marker transparency")
550
+ p3d.add_argument("--cmap", default="coolwarm", help="Matplotlib colormap")
551
+ p3d.add_argument("--elev", type=float, default=22.0, help="3D view elevation")
552
+ p3d.add_argument("--azim", type=float, default=38.0, help="3D view azimuth")
553
+ p3d.set_defaults(_run=_local_pol_plot3d_task)
554
+
555
+ # ---------------------- 2D local polarization heatmap ----------------------
556
+ p2d = subparsers.add_parser(
557
+ "heatmap2d",
558
+ help="2D heatmap of local polarization/dipole on core-atom positions",
559
+ description=(
560
+ "Examples:\n"
561
+ " reaxkit elect heatmap2d --core Al --component mu_z --plane xz --bins 10 --frames 0:2:1 "
562
+ "--save reaxkit_outputs/elect/local_mu_2d"
563
+ ),
564
+ formatter_class=argparse.RawTextHelpFormatter,
565
+ )
566
+ p2d.add_argument("--xmolout", default="xmolout", help="Path to xmolout file")
567
+ p2d.add_argument("--fort7", default="fort.7", help="Path to fort.7 file")
568
+ p2d.add_argument("--core", required=True, help="Comma-separated core atom types (e.g. Al,Mg)")
569
+ p2d.add_argument("--component", default="pol_z",
570
+ help="Which component to aggregate (e.g. pol_z, P_z (uC/cm^2), mu_z)")
571
+ p2d.add_argument("--frames", default=None, help='Frames: "0,10,20" or "0:100:5"')
572
+ p2d.add_argument("--polarization", action="store_true",
573
+ help="Use polarization components (P_x/y/z) instead of dipole")
574
+
575
+ # Projection/grid
576
+ p2d.add_argument("--plane", default="xy", choices=["xy", "xz", "yz"], help="Projection plane")
577
+ p2d.add_argument("--bins", default="40", help='Grid bins: "N" or "Nx,Ny" (e.g., "10,25")')
578
+ p2d.add_argument("--agg", default="mean", help="Aggregation: mean|max|min|sum|count")
579
+
580
+ # Viz
581
+ p2d.add_argument("--vmin", type=float, default=None, help="Color scale min (auto if not set)")
582
+ p2d.add_argument("--vmax", type=float, default=None, help="Color scale max (auto if not set)")
583
+ p2d.add_argument("--cmap", default="viridis", help="Matplotlib colormap")
584
+ p2d.add_argument("--save", default=None, help="Directory to save PNGs (one per frame)")
585
+ p2d.set_defaults(_run=_local_pol_heatmap2d_task)
586
+
587
+