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,554 @@
1
+ """
2
+ Geometry (GEO) manipulation workflow for ReaxKit.
3
+
4
+ This workflow provides a collection of utilities for creating, transforming,
5
+ and modifying atomic geometry files used in ReaxFF simulations, with a focus
6
+ on the GEO (XTLGRF) format and ASE-compatible structure files.
7
+
8
+ It supports:
9
+ - Converting XYZ structures to GEO format with explicit cell dimensions
10
+ and angles.
11
+ - Building surface slabs from bulk structures (CIF, POSCAR, etc.), including
12
+ Miller-index selection, supercell expansion, and vacuum padding.
13
+ - Sorting atoms in GEO files by index, coordinate, or atom type.
14
+ - Orthogonalizing hexagonal unit cells (90°, 90°, 120°) into orthorhombic
15
+ representations (90°, 90°, 90°).
16
+ - Randomly placing multiple copies of a molecule into a simulation box or
17
+ around an existing structure using a placement algorithm.
18
+ - Inserting sample or user-defined restraint blocks (bond, angle, torsion,
19
+ mass-center) into GEO files for constrained simulations.
20
+
21
+ The workflow is designed to streamline preparation of ReaxFF input geometries
22
+ and to support reproducible, scriptable structure generation from the command line.
23
+ """
24
+
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ from pathlib import Path
30
+ from typing import List
31
+
32
+ from reaxkit.io.handlers.geo_handler import GeoHandler
33
+ from reaxkit.io.generators.geo_generator import (
34
+ xtob,
35
+ read_structure,
36
+ build_surface,
37
+ make_supercell,
38
+ write_structure,
39
+ _format_crystx,
40
+ _format_hetatm_line,
41
+ orthogonalize_hexagonal_cell,
42
+ place2,
43
+ )
44
+ from reaxkit.io.generators.geo_generator import add_restraints_to_geo
45
+
46
+ # ----------------------------------------------------------------------
47
+ # Helpers
48
+ # ----------------------------------------------------------------------
49
+
50
+ def _parse_csv_floats(value: str, expected: int, name: str) -> List[float]:
51
+ """
52
+ Parse a comma-separated string of floats and validate length.
53
+
54
+ Example: "1,2,3" -> [1.0, 2.0, 3.0]
55
+ """
56
+ parts = [v.strip() for v in value.split(",") if v.strip()]
57
+ if len(parts) != expected:
58
+ raise argparse.ArgumentTypeError(
59
+ f"{name} must contain exactly {expected} comma-separated values, "
60
+ f"got {len(parts)} from {value!r}."
61
+ )
62
+ try:
63
+ return [float(v) for v in parts]
64
+ except ValueError as exc:
65
+ raise argparse.ArgumentTypeError(
66
+ f"{name} must be numeric, could not parse {value!r}."
67
+ ) from exc
68
+
69
+
70
+ def _parse_csv_ints(value: str, expected: int, name: str) -> List[int]:
71
+ """
72
+ Parse a comma-separated string of ints and validate length.
73
+
74
+ Example: "1,0,0" -> [1, 0, 0]
75
+ """
76
+ parts = [v.strip() for v in value.split(",") if v.strip()]
77
+ if len(parts) != expected:
78
+ raise argparse.ArgumentTypeError(
79
+ f"{name} must contain exactly {expected} comma-separated values, "
80
+ f"got {len(parts)} from {value!r}."
81
+ )
82
+ try:
83
+ return [int(v) for v in parts]
84
+ except ValueError as exc:
85
+ raise argparse.ArgumentTypeError(
86
+ f"{name} must be integers, could not parse {value!r}."
87
+ ) from exc
88
+
89
+
90
+ # ----------------------------------------------------------------------
91
+ # Task 1: xyz -> geo (xtob) format conversion
92
+ # ----------------------------------------------------------------------
93
+
94
+ def _xtob_task(args: argparse.Namespace) -> int:
95
+ """
96
+ Convert XYZ → GEO (XTLGRF) using reaxkit.io.geo_generator.xtob.
97
+ """
98
+ xyz_path = Path(args.file)
99
+
100
+ if not xyz_path.is_file():
101
+ raise FileNotFoundError(f"Input XYZ file not found: {xyz_path}")
102
+
103
+ dims = _parse_csv_floats(args.dims, expected=3, name="--dims")
104
+ angles = _parse_csv_floats(args.angles, expected=3, name="--angles")
105
+ output = args.output or "geo"
106
+
107
+ xtob(
108
+ xyz_file=xyz_path,
109
+ geo_file=output,
110
+ box_lengths=dims,
111
+ box_angles=angles,
112
+ sort_by=args.sort,
113
+ ascending=not args.descending,
114
+ )
115
+
116
+ print(f"[Done] Converted {xyz_path} → {output}")
117
+ return 0
118
+
119
+
120
+ # ----------------------------------------------------------------------
121
+ # Task 2: build slab from CIF/POSCAR and write XYZ (make)
122
+ # ----------------------------------------------------------------------
123
+
124
+ def _make_task(args: argparse.Namespace) -> int:
125
+ """
126
+ Build a surface slab from a bulk structure and write it to XYZ (or any ASE format).
127
+
128
+ CLI:
129
+ reaxkit geo make --file X.cif --output Y.xyz \
130
+ --surface 1,0,0 --expand 4,4,6 --vacuum 15
131
+
132
+ Mapping:
133
+ --surface h,k,l -> miller = (h, k, l)
134
+ --expand nx,ny,l -> layers = l
135
+ repetition = (nx, ny, 1)
136
+ --vacuum v -> vacuum thickness in Å
137
+ """
138
+ in_path = Path(args.file)
139
+
140
+ if not in_path.is_file():
141
+ raise FileNotFoundError(f"Input structure file not found: {in_path}")
142
+
143
+ # Parse Miller indices and expansion
144
+ miller = _parse_csv_ints(args.surface, expected=3, name="--surface")
145
+ expand = _parse_csv_ints(args.expand, expected=3, name="--expand")
146
+ nx, ny, layers = expand
147
+
148
+ vacuum = float(args.vacuum)
149
+ output = args.output or "slab.xyz"
150
+
151
+ # 1) Read bulk
152
+ bulk = read_structure(in_path)
153
+
154
+ # 2) Build surface slab
155
+ slab = build_surface(
156
+ bulk,
157
+ miller=tuple(miller),
158
+ layers=layers,
159
+ vacuum=vacuum,
160
+ center=True,
161
+ )
162
+
163
+ # 3) Expand in-plane only: (nx, ny, 1)
164
+ slab_expanded = make_supercell(slab, (nx, ny, 1))
165
+
166
+ # 4) Write output (ASE will infer format from extension, e.g. .xyz)
167
+ write_structure(slab_expanded, output)
168
+
169
+ print(
170
+ f"[Done] Built surface {tuple(miller)} with layers={layers}, "
171
+ f"expanded ({nx}, {ny}, 1) and wrote {output}"
172
+ )
173
+ return 0
174
+
175
+ # ----------------------------------------------------------------------
176
+ # Task 3: sort a geo file
177
+ # ----------------------------------------------------------------------
178
+
179
+ def _sort_task(args: argparse.Namespace) -> int:
180
+ """
181
+ Sort atoms in a GEO file and write a new GEO file.
182
+
183
+ CLI example:
184
+ reaxkit geo sort --file X.geo --output Y.geo --sort m --descending
185
+
186
+ where:
187
+ --sort m → sort by atom index (atom_id)
188
+ --sort x|y|z → sort by coordinate
189
+ --sort atom_type → sort by atom type
190
+ """
191
+ in_path = Path(args.file)
192
+ if not in_path.is_file():
193
+ raise FileNotFoundError(f"Input GEO file not found: {in_path}")
194
+
195
+ handler = GeoHandler(in_path)
196
+ df = handler.dataframe().copy()
197
+ meta = handler.metadata()
198
+
199
+ sort_map = {"m": "atom_id", "x": "x", "y": "y", "z": "z", "atom_type": "atom_type"}
200
+ if args.sort not in sort_map:
201
+ raise ValueError(f"--sort must be one of {list(sort_map.keys())}, got {args.sort!r}")
202
+
203
+ sort_col = sort_map[args.sort]
204
+ ascending = not args.descending
205
+
206
+ df_sorted = df.sort_values(by=sort_col, ascending=ascending).reset_index(drop=True)
207
+ df_sorted["atom_id"] = df_sorted.index + 1 # renumber sequentially
208
+
209
+ descriptor = meta.get("descriptor") or ""
210
+ remark = meta.get("remark")
211
+ cell_lengths = meta.get("cell_lengths")
212
+ cell_angles = meta.get("cell_angles")
213
+
214
+ out_path = Path(args.output)
215
+ direction = "descending" if args.descending else "ascending"
216
+ sort_label_map = {"m": "atom index", "x": "x-coordinate", "y": "y-coordinate", "z": "z-coordinate", "atom_type": "atom type"}
217
+ sort_label = sort_label_map[args.sort]
218
+
219
+ with out_path.open("w") as fh:
220
+ fh.write("XTLGRF 200\n")
221
+ if descriptor:
222
+ fh.write(f"DESCRP {descriptor}\n")
223
+ if remark:
224
+ fh.write(f"REMARK {remark}\n")
225
+ fh.write(f"REMARK Structure sorted by {sort_label} ({direction})\n")
226
+
227
+ if cell_lengths and cell_angles:
228
+ try:
229
+ lengths = [cell_lengths.get(k) for k in ("a", "b", "c")]
230
+ angles = [cell_angles.get(k) for k in ("alpha", "beta", "gamma")]
231
+ if all(v is not None for v in lengths + angles):
232
+ fh.write(_format_crystx(lengths, angles) + "\n")
233
+ except Exception:
234
+ # If anything is weird, just skip CRYSTX instead of crashing
235
+ pass
236
+
237
+ fh.write("FORMAT ATOM (a6,1x,i5,1x,a5,1x,a3,1x,a1,1x,a5,3f10.5,1x,a5,i3,i2,1x,f8.5)\n")
238
+ for row in df_sorted.itertuples(index=False):
239
+ line = _format_hetatm_line(row.atom_id, row.atom_type, row.x, row.y, row.z)
240
+ fh.write(line + "\n")
241
+ fh.write("END\n")
242
+
243
+ print(f"[Done] Sorted {in_path} by {sort_label} ({direction}) → {out_path}")
244
+ return 0
245
+
246
+ # --------------------------------------------------------------------------------------------------
247
+ # Task 4: convert a hexagonal cell (90°, 90°, 120°) into an orthorhombic (90°, 90°, 90°) cell
248
+ # --------------------------------------------------------------------------------------------------
249
+
250
+ def _ortho_task(args: argparse.Namespace) -> int:
251
+ """
252
+ Orthogonalize a hexagonal (90,90,120) cell into an orthorhombic (90,90,90) cell.
253
+
254
+ CLI example:
255
+ reaxkit geo ortho --file AlN_hex.cif --output AlN_ortho.cif
256
+ """
257
+ in_path = Path(args.file)
258
+ out_path = Path(args.output)
259
+
260
+ if not in_path.is_file():
261
+ raise FileNotFoundError(f"Input structure file not found: {in_path}")
262
+
263
+ # 1. Read structure using ASE
264
+ atoms = read_structure(in_path)
265
+
266
+ # 2. Apply orthogonalization transform
267
+ ortho_atoms = orthogonalize_hexagonal_cell(atoms)
268
+
269
+ # 3. Write output (ASE infers format from extension)
270
+ write_structure(ortho_atoms, out_path)
271
+
272
+ print(f"[Done] Converted hexagonal → orthorhombic: {in_path} → {out_path}")
273
+ return 0
274
+
275
+ # ------------------------------------------------------------------------------------
276
+ # task 5: place2 algorithm for placing n instances of a structure into a
277
+ # simulation box or within another structure
278
+ # ------------------------------------------------------------------------------------
279
+ def _place2_task(args: argparse.Namespace) -> int:
280
+ """
281
+ Randomly place copies of an insert molecule into a simulation box,
282
+ optionally around/within a base structure.
283
+
284
+ CLI example:
285
+ reaxkit geo place2 \
286
+ --insert X.xyz \
287
+ --ncopy 40 \
288
+ --dims 1,2,3 \
289
+ --angles 90,90,90 \
290
+ --output Y.xyz \
291
+ [--base base.xyz] \
292
+ [--mindist 3.0] \
293
+ [--baseplace as-is|center|origin] \
294
+ [--maxattempt 50000] \
295
+ [--randomseed 1234]
296
+
297
+ If output is:
298
+ - *.xyz → write XYZ directly.
299
+ - geo or *.bgf or anything non-*.xyz →
300
+ 1) write place2_output.xyz
301
+ 2) run xtob on that to generate requested output.
302
+ """
303
+ insert_path = Path(args.insert)
304
+ if not insert_path.is_file():
305
+ raise FileNotFoundError(f"Insert molecule not found: {insert_path}")
306
+
307
+ base_path = None
308
+ if args.base is not None:
309
+ base_path = Path(args.base)
310
+ if not base_path.is_file():
311
+ raise FileNotFoundError(f"Base structure not found: {base_path}")
312
+
313
+ # Parse box dimensions and angles
314
+ dims = _parse_csv_floats(args.dims, expected=3, name="--dims")
315
+ angles = _parse_csv_floats(args.angles, expected=3, name="--angles")
316
+ a, b, c = dims
317
+ alpha, beta, gamma = angles
318
+
319
+ # Optional parameters
320
+ min_dist = float(args.mindist)
321
+ baseplace = args.baseplace
322
+ max_attempt = int(args.maxattempt)
323
+ random_seed = None if args.randomseed is None else int(args.randomseed)
324
+
325
+ # Run the placement algorithm
326
+ atoms = place2(
327
+ insert_molecule=insert_path,
328
+ base_structure=base_path,
329
+ n_copies=int(args.ncopy),
330
+ box_length_x=a,
331
+ box_length_y=b,
332
+ box_length_z=c,
333
+ alpha=alpha,
334
+ beta=beta,
335
+ gamma=gamma,
336
+ min_interatomic_distance=min_dist,
337
+ base_structure_placement_mode=baseplace,
338
+ max_placement_attempts_per_copy=max_attempt,
339
+ random_seed=random_seed,
340
+ )
341
+
342
+ # Handle output
343
+ out_path = Path(args.output)
344
+ ext = out_path.suffix.lower()
345
+
346
+ if ext == ".xyz":
347
+ # Direct XYZ write
348
+ write_structure(atoms, out_path)
349
+ print(f"[Done] Placed {args.ncopy} copies into box → {out_path}")
350
+ else:
351
+ # Intermediate XYZ then xtob → GEO/BGF/etc.
352
+ tmp_xyz = Path("place2_output.xyz")
353
+ write_structure(atoms, tmp_xyz)
354
+ xtob(
355
+ xyz_file=tmp_xyz,
356
+ geo_file=out_path,
357
+ box_lengths=dims,
358
+ box_angles=angles,
359
+ sort_by=None,
360
+ ascending=True,
361
+ )
362
+ print(
363
+ f"[Done] Placed {args.ncopy} copies into box → {tmp_xyz} "
364
+ f"→ converted to {out_path} via xtob"
365
+ )
366
+
367
+ return 0
368
+
369
+
370
+ # ----------------------------------------------------------------------
371
+ # Task 6: add restraint block to GEO
372
+ # ----------------------------------------------------------------------
373
+ def _add_restraint_task(args: argparse.Namespace) -> int:
374
+ in_path = Path(args.file)
375
+ if not in_path.is_file():
376
+ raise FileNotFoundError(f"Input GEO file not found: {in_path}")
377
+
378
+ out_path = Path(args.output) if args.output else (in_path.parent / f"{in_path.name}_with_restraints")
379
+
380
+ # Build params dict: user can pass per-kind params or nothing -> defaults
381
+ params = {}
382
+ if args.bond is not None:
383
+ params["BOND"] = args.bond.strip()
384
+ if args.angle is not None:
385
+ params["ANGLE"] = args.angle.strip()
386
+ if args.torsion is not None:
387
+ params["TORSION"] = args.torsion.strip()
388
+ if args.mascen is not None:
389
+ params["MASCEN"] = args.mascen.strip()
390
+
391
+ # kinds are inferred from which flags user provided
392
+ kinds = []
393
+ if args.bond is not None:
394
+ kinds.append("BOND")
395
+ if args.angle is not None:
396
+ kinds.append("ANGLE")
397
+ if args.torsion is not None:
398
+ kinds.append("TORSION")
399
+ if args.mascen is not None:
400
+ kinds.append("MASCEN")
401
+
402
+ if not kinds:
403
+ raise ValueError(
404
+ "No restraints requested. Provide at least one of: "
405
+ "--bond, --angle, --torsion, --mascen"
406
+ )
407
+
408
+ out_written = add_restraints_to_geo(
409
+ in_path,
410
+ out_file=out_path,
411
+ kinds=kinds,
412
+ params=params,
413
+ )
414
+
415
+ print(f"[Done] Added restraints to {in_path} and the result is exported as {out_written}")
416
+ return 0
417
+
418
+ # ----------------------------------------------------------------------
419
+ # CLI registration
420
+ # ----------------------------------------------------------------------
421
+
422
+ def register_tasks(subparsers: argparse._SubParsersAction) -> None:
423
+ # ---- xtob ----
424
+ p_xtob = subparsers.add_parser(
425
+ "xtob",
426
+ help="Convert an XYZ file to GEO (XTLGRF) format \n",
427
+ description=(
428
+ "Examples:\n"
429
+ " reaxkit geo xtob --file slab.xyz --dims 11.0,12.0,100.0 --angles 90,90,90 --output slab_geo_from_xyz\n"
430
+ ),
431
+ formatter_class=argparse.RawTextHelpFormatter,
432
+ )
433
+ p_xtob.add_argument("--file", required=True, help="Input XYZ file (X.xyz)")
434
+ p_xtob.add_argument("--dims", required=True, help="Box dimensions a,b,c (e.g., 11.0,12.0,100.0)")
435
+ p_xtob.add_argument("--angles", required=True, help="Box angles alpha,beta,gamma (e.g., 90,90,90)")
436
+ p_xtob.add_argument("--output", default="geo", help="Output GEO file name (default: geo)")
437
+ p_xtob.add_argument("--sort", choices=["x", "y", "z", "atom_type"], help="Sort atoms before writing GEO")
438
+ p_xtob.add_argument("--descending", action="store_true", help="Sort in descending order")
439
+ p_xtob.set_defaults(_run=_xtob_task)
440
+
441
+ # ---- make ----
442
+ p_make = subparsers.add_parser(
443
+ "make",
444
+ help="Build a surface slab from bulk and write XYZ/CIF || ",
445
+ description=(
446
+ "Examples:\n"
447
+ " reaxkit geo make --file AlN.cif --output slab_from_AlN_cif.xyz --surface 1,0,0 --expand 4,4,6 --vacuum 15\n"
448
+ ),
449
+ formatter_class=argparse.RawTextHelpFormatter,
450
+ )
451
+ p_make.add_argument("--file", required=True, help="Input bulk file (CIF, POSCAR, etc.)")
452
+ p_make.add_argument("--output", required=True, help="Output file (XYZ, CIF, etc.)")
453
+ p_make.add_argument("--surface", required=True, help="Miller indices h,k,l (e.g., 1,0,0)")
454
+ p_make.add_argument("--expand", required=True, help="Supercell and layers nx,ny,layers (e.g., 4,4,6)")
455
+ p_make.add_argument("--vacuum", required=True, help="Vacuum thickness in Å (e.g., 15)")
456
+ p_make.set_defaults(_run=_make_task)
457
+
458
+ # ---- sort ----
459
+ p_sort = subparsers.add_parser(
460
+ "sort",
461
+ help="Sort atoms in a GEO file and write a new GEO \n",
462
+ description=(
463
+ "Examples:\n"
464
+ " reaxkit geo sort --file geo --output sorted_geo --sort x\n"
465
+ ),
466
+ formatter_class=argparse.RawTextHelpFormatter,
467
+ )
468
+ p_sort.add_argument("--file", required=True, help="Input GEO file (X.geo)")
469
+ p_sort.add_argument("--output", required=True, help="Output GEO file (Y.geo)")
470
+ p_sort.add_argument("--sort", required=True, choices=["m", "x", "y", "z", "atom_type"],
471
+ help="Sort key: m=atom index, x/y/z=coordinates, atom_type=element")
472
+ p_sort.add_argument("--descending", action="store_true", help="Sort in descending order")
473
+ p_sort.set_defaults(_run=_sort_task)
474
+
475
+ # ---- ortho (orthogonalize) ----
476
+ p_ortho = subparsers.add_parser(
477
+ "ortho",
478
+ help="Convert hexagonal (90,90,120) cell to orthorhombic (90,90,90) \n",
479
+ description=(
480
+ "Examples:\n"
481
+ " reaxkit geo ortho --file AlN.cif --output AlN_ortho_from_hex.cif\n"
482
+ ),
483
+ formatter_class=argparse.RawTextHelpFormatter,
484
+ )
485
+ p_ortho.add_argument("--file", required=True, help="Input CIF/POSCAR/GEO file to orthogonalize")
486
+ p_ortho.add_argument("--output", required=True, help="Output file (e.g., AlN_ortho.cif)")
487
+ p_ortho.set_defaults(_run=_ortho_task)
488
+
489
+ # ---- place2 ----
490
+ p_place2 = subparsers.add_parser(
491
+ "place2",
492
+ help="Randomly place copies of a molecule into a box and optionally around a base structure || ",
493
+ description=(
494
+ "Examples:\n"
495
+ " reaxkit geo place2 --insert template.xyz --ncopy 40 --dims 28.8,33.27,60 "
496
+ "--angles 90,90,90 --output place2_on_template_xyz_with_no_base.xyz\n"
497
+ " reaxkit geo place2 --insert template.xyz --ncopy 40 --dims 28.8,33.27,60 "
498
+ "--angles 90,90,90 --output place2__on_template_xyz_with_base.xyz --base base.xyz\n"
499
+ " reaxkit geo place2 --insert template.xyz --ncopy 40 --dims 28.8,33.27,60 "
500
+ "--angles 90,90,90 --output place2_geo_from_template_xyz --base base.xyz"
501
+ ),
502
+ formatter_class=argparse.RawTextHelpFormatter,
503
+ )
504
+ p_place2.add_argument("--insert", required=True,
505
+ help="Insert molecule (XYZ or any ASE-readable format, e.g., X.xyz)",
506
+ )
507
+ p_place2.add_argument("--ncopy", required=True, help="Number of copies of the insert molecule to place")
508
+ p_place2.add_argument("--dims", required=True, help="Box dimensions a,b,c (e.g., 30,30,60)")
509
+ p_place2.add_argument("--angles", required=True, help="Box angles alpha,beta,gamma (e.g., 90,90,90)")
510
+ p_place2.add_argument("--output", required=True, help="Output file: Y.xyz, Y.bgf, or 'geo'")
511
+ p_place2.add_argument("--base",help="Optional base structure (e.g., slab.xyz) to place molecules around")
512
+ p_place2.add_argument("--mindist", default=2.0,
513
+ help="Minimum interatomic distance between insert copies and base/system (Å), default=2.0",
514
+ )
515
+ p_place2.add_argument("--baseplace", default="as-is", choices=["as-is", "center", "origin"],
516
+ help="How to place the base structure: as-is, center, or origin (default: as-is)"
517
+ )
518
+ p_place2.add_argument("--maxattempt", default=50000, help="Maximum placement attempts per copy (default: 50000)")
519
+ p_place2.add_argument("--randomseed", default=None, help="Random seed for reproducible placement (optional)")
520
+ p_place2.set_defaults(_run=_place2_task)
521
+
522
+ # ---- add-restraint ----
523
+ p_rest = subparsers.add_parser(
524
+ "add-restraint",
525
+ help="Insert a sample restraint block (BOND/ANGLE/TORSION/MASCEN) into a GEO file",
526
+ description=(
527
+ "Examples:\n"
528
+ " reaxkit geo add-restraint --bond \n"
529
+ " reaxkit geo add-restraint --file geo --output geo_r --angle '1 2 3 109.5000 600.00 0.25000 0.0000000'\n"
530
+ ),
531
+ formatter_class=argparse.RawTextHelpFormatter,
532
+ )
533
+
534
+ p_rest.add_argument("--file", default="geo", help="Input GEO file (e.g., geo)")
535
+ p_rest.add_argument("--output", default="reaxkit_generated_inputs/geo_with_restraints",
536
+ help="Output GEO file (default: <input>_with_restraints)")
537
+
538
+ # Each flag can be:
539
+ # - omitted (not requested)
540
+ # - provided with "" (empty) to request a default sample
541
+ # - provided with a params string to use exactly that
542
+ p_rest.add_argument("--bond", nargs="?", const="", default=None,
543
+ help="Add ONE BOND restraint (optional params string; empty => default sample).")
544
+ p_rest.add_argument("--angle", nargs="?", const="", default=None,
545
+ help="Add ONE ANGLE restraint (optional params string; empty => default sample).")
546
+ p_rest.add_argument("--torsion", nargs="?", const="", default=None,
547
+ help="Add ONE TORSION restraint (optional params string; empty => default sample).")
548
+ p_rest.add_argument("--mascen", nargs="?", const="", default=None,
549
+ help="Add ONE MASCEN restraint (optional params string; empty => default sample).")
550
+
551
+ p_rest.set_defaults(_run=_add_restraint_task)
552
+
553
+
554
+