pymdkit 1.5.0__tar.gz → 1.5.2__tar.gz

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 (51) hide show
  1. {pymdkit-1.5.0 → pymdkit-1.5.2}/PKG-INFO +64 -12
  2. {pymdkit-1.5.0 → pymdkit-1.5.2}/README.md +89 -37
  3. {pymdkit-1.5.0 → pymdkit-1.5.2}/pyproject.toml +7 -7
  4. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/__init__.py +1 -1
  5. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_cifio.py +106 -16
  6. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_gpumd.py +19 -8
  7. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/convert.py +8 -3
  8. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_group.py +3 -3
  9. pymdkit-1.5.2/src/pymdkit/commands/gpumd_msd.py +461 -0
  10. pymdkit-1.5.2/src/pymdkit/commands/realloc_occ.py +452 -0
  11. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/supercell.py +116 -23
  12. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/symmetrize.py +1 -1
  13. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/PKG-INFO +64 -12
  14. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/SOURCES.txt +2 -1
  15. pymdkit-1.5.0/src/pymdkit/commands/compute_msd_all_groups.py +0 -375
  16. {pymdkit-1.5.0 → pymdkit-1.5.2}/LICENSE +0 -0
  17. {pymdkit-1.5.0 → pymdkit-1.5.2}/setup.cfg +0 -0
  18. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/__init__.py +0 -0
  19. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_extxyz.py +0 -0
  20. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_fileio.py +0 -0
  21. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_geometry.py +0 -0
  22. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_precision.py +0 -0
  23. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_reduction.py +0 -0
  24. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_vasp_output.py +0 -0
  25. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/_vaspset.py +0 -0
  26. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/add_config_type.py +0 -0
  27. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
  28. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/compute_ehull.py +0 -0
  29. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/compute_rmsd.py +0 -0
  30. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/ewald.py +0 -0
  31. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/gather_fs.py +0 -0
  32. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_relax.py +0 -0
  33. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_thermo.py +0 -0
  34. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
  35. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/nep_rmse.py +0 -0
  36. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/pca.py +0 -0
  37. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/perturb.py +0 -0
  38. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/submit_vasp.py +0 -0
  39. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/substitute.py +0 -0
  40. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/trj_extract.py +0 -0
  41. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/trj_sparse.py +0 -0
  42. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/umap.py +0 -0
  43. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/vasp2xyz.py +0 -0
  44. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_fe.py +0 -0
  45. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_relax.py +0 -0
  46. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_static.py +0 -0
  47. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit/pymdkit_main.py +0 -0
  48. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  49. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
  50. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/requires.txt +0 -0
  51. {pymdkit-1.5.0 → pymdkit-1.5.2}/src/pymdkit.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.5.0
3
+ Version: 1.5.2
4
4
  Summary: A unified command-line toolkit for atomistic / MD structure workflows.
5
5
  Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
6
6
  License-Expression: GPL-3.0-or-later
@@ -84,7 +84,7 @@ directory for job sub-folders automatically.
84
84
  | `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
85
85
  | `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
86
86
  | `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
87
- | `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
87
+ | `gpumd-msd` | Recursively extract directional SDC and Nernst-Einstein conductivity from GPUMD jobs |
88
88
  | `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
89
89
  | `perturb` | Generate perturbed structures with dpdata |
90
90
  | `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
@@ -92,6 +92,7 @@ directory for job sub-folders automatically.
92
92
  | `rmsd` | Compare structures by RMSD and optionally remove higher-energy duplicates |
93
93
  | `chemsys-entry` | Download stable Materials Project structures for a chemical system |
94
94
  | `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
95
+ | `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
95
96
  | `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
96
97
  | `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
97
98
  | `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
@@ -111,7 +112,7 @@ pmk convert -if vasp-opted -of cif-opted -oe cif # folder convers
111
112
  pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
112
113
  pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
113
114
  pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
114
- pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -add-groups Li Y Cl
115
+ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
115
116
  # GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
116
117
  pmk vasp-relax -i opted.vasp # relax inputs in current dir
117
118
  pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
@@ -119,8 +120,8 @@ pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run
119
120
  pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
120
121
  pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
121
122
  pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
122
- pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
123
- pmk msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion for conductivity
123
+ pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
124
+ pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
124
125
  pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
125
126
  pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
126
127
  pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
@@ -132,6 +133,8 @@ pmk vasp2xyz # scans ./ for VASP output folde
132
133
  pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
133
134
  pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
134
135
  nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
136
+ pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
137
+ pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
135
138
  pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
136
139
  pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
137
140
  pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
@@ -200,6 +203,36 @@ dump_xyz -1 0 1 relaxed.xyz force
200
203
  run 1
201
204
  ```
202
205
 
206
+ `gpumd-msd` recursively discovers job folders at any depth below `-entry-path`
207
+ (the current directory by default). A job must contain `msd.out`, `model.xyz`,
208
+ `run.in`, and `thermo.out`; the common accidental name `thermos.out` is also
209
+ accepted. Following the [official GPUMD `msd.out` format](https://gpumd.org/gpumd/output_files/msd_out.html), each group block is read as three MSD columns
210
+ followed by three directional SDC columns. PMK takes the arithmetic mean over
211
+ the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
212
+ multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
213
+ Each job receives `msd/group_INDEX_ELEMENT_TEMP_msd.txt`, a matching `_sdc.txt`,
214
+ and a `_results.txt`. The root receives `sigma-sdc-summary.txt` with directional
215
+ and total diffusion coefficients and conductivities.
216
+
217
+ Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
218
+ of the final SDC-producing MD run and the official 18-column
219
+ [`thermo.out` format](https://gpumd.org/gpumd/output_files/thermo_out.html):
220
+ for NPT ensembles (`npt_scr`, `npt_mttk`, and other `npt_*` forms), PMK uses
221
+ the mean triclinic-box volume over that run; for NVT ensembles (`nvt_bdp`,
222
+ `nvt_ber`, and other `nvt_*` forms), it uses the final fixed-cell volume.
223
+ `V(A^3) * 1e-24 -> V(cm^3)`, exact SI values for elementary charge and the
224
+ Boltzmann constant, and a final `* 1000` conversion from S/cm to mS/cm.
225
+
226
+ `realloc-occ -i INPUT.cif -of OUTPUT -formula FORMULA` computes atom counts as
227
+ Wyckoff multiplicity times occupancy and uses `_cell_formula_units_Z` to map the
228
+ target reduced formula into the existing cell. It enumerates finite site-wise
229
+ occupancy allocations, retains only charge-balanced candidates using the CIF
230
+ oxidation-number loop, and preserves the original symmetry, coordinates,
231
+ Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
232
+ the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
233
+ and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
234
+ `-formula` options.
235
+
203
236
  `gather-fs` recursively scans all job folders below the current directory. For
204
237
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
205
238
  requires full convergence before copying the selected `-fs-name` as `.vasp`. The gathered VASP file's first line is replaced with `energy=<final_energy> eV` for later screening.
@@ -241,20 +274,25 @@ per successfully generated CIF: filename, crystal system, space-group symbol,
241
274
  and space-group number. Crystal-system and space-group summaries are sorted
242
275
  from highest to lowest count, with alphabetical ordering for ties, followed by
243
276
  the total number of structures. Single-file mode places the report beside the
244
- output CIF; folder and CSV modes place it inside `-of`.
277
+ output CIF; folder and CSV modes place it inside `-of`. Folder mode keeps each
278
+ input stem unchanged, writing `NAME.cif` rather than `NAME-symm.cif`.
245
279
 
246
280
  Every CIF exported by `symmetrize`, `convert`, `substitute`, `chemsys-entry`,
247
281
  `supercell`, or `gpumd-group` is written through the same symmetry-aware helper.
248
282
  Each CIF places the IUCr-defined `_space_group_crystal_system` item immediately
249
- below its `data_...` header and contains both `_atom_site_symmetry_multiplicity`
250
- and `_atom_site_Wyckoff_symbol` in the atom-site loop. Multiplicity-letter
283
+ below its `data_...` header and contains both
284
+ `_atom_site_site_symmetry_multiplicity` and `_atom_site_Wyckoff_symbol` in the
285
+ atom-site loop. Multiplicity-letter
251
286
  assignments use the conventional
252
287
  International Tables setting from pymatgen/spglib and correspond to the
253
288
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
254
289
  the provenance URL is also recorded inside each generated CIF.
255
290
 
256
- CIF atom types and site labels use per-element inequivalent-site identifiers
257
- such as `Li1`, `Y1`, `Cl1`, and `Cl2`; oxidation numbers remain in the separate
291
+ CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
292
+ inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
293
+ The final atom-site loop starts with `_atom_site_label`, followed by
294
+ `_atom_site_type_symbol`, whose values are plain elements such as `Li`, `Y`,
295
+ and `Cl`. Oxidation numbers remain in the separate
258
296
  `_atom_type_oxidation_number` column. PMK does not add an
259
297
  `_audit_creation_method` block or a generated-by header comment.
260
298
 
@@ -273,8 +311,20 @@ existing trajectory frames continue copying those frame bytes exactly.
273
311
 
274
312
  When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every frame and writes one additional descriptor table per value, such as `pca-descriptor-P-3m1.txt` and `pca-descriptor-Pnma.txt`. With `-fps`, it also writes matching files such as `fps-0.01-pca-descriptor-P-3m1.txt` and `fps-0.01-train-P-3m1.xyz`. These files partition the single global FPS selection; FPS is not rerun independently for each `Config_type`. Frames without `Config_type` remain in the main outputs but do not receive a type-specific file.
275
313
 
276
- For folder conversion, `convert -if INPUT -of OUTPUT` writes `.xyz` by default. Set another target extension with `-oe`, for example `-oe cif` or `-oe vasp`. Single-file and trajectory modes infer the target format from `-o` and `-ot`.
277
- Because Wyckoff metadata describes one crystal structure, each `.cif` output file accepts exactly one frame.
314
+ For folder conversion, `convert -if INPUT -o OUTPUT` and
315
+ `convert -if INPUT -of OUTPUT` are equivalent and write `.xyz` by default. Set
316
+ another target extension with `-oe`, for example `-oe cif` or `-oe vasp`.
317
+ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
318
+ Because Wyckoff metadata describes one crystal structure, each `.cif` output
319
+ file accepts exactly one frame.
320
+
321
+ After `supercell -temp` creates all requested jobs, it checks generated folders
322
+ that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
323
+ lines retain their temperature parameters but are rewritten with three
324
+ pressure/modulus components for 90-degree orthorhombic cells and all six
325
+ components for non-orthorhombic cells, following the
326
+ [official GPUMD ensemble syntax](https://gpumd.org/gpumd/input_parameters/ensemble_standard.html).
327
+ Other ensemble types are not modified. Use `-group Li Y Cl` to add GPUMD groups.
278
328
 
279
329
  `gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. An abnormal classification always requires a mapped potential-energy change. When at least two lattice-parameter or volume signals show the same mapped transition, they may enable a more sensitive noise-relative energy test; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Cell signals can corroborate and refine the energy interval, but can never classify a run by themselves. Temperature and all six pressure components remain diagnostics only and cannot classify an otherwise stationary run as abnormal by themselves. A lone cell-axis drift, an isolated spike, or an empty anomaly mask never triggers a whole-run fallback. Thermo plots use Arial when available and Matplotlib's built-in qualitative `Dark2` color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and show abnormal regions as shaded legend entries on potential energy, lattice parameters, and volume. Potential energy and volume are divided by 1000 and labeled ×10³ only when their absolute plotted values reach 1000. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
280
330
 
@@ -329,6 +379,7 @@ pymdkit/
329
379
  |-- _geometry.py # shared minimum-image geometry helper
330
380
  |-- _vaspset.py # shared VASP input-set helper (not a command)
331
381
  |-- gpumd_group.py
382
+ |-- gpumd_msd.py
332
383
  |-- gpumd_relax.py
333
384
  |-- gpumd_thermo.py
334
385
  |-- gpumd_trjcat.py
@@ -343,6 +394,7 @@ pymdkit/
343
394
  |-- chemsys_entry.py
344
395
  |-- submit_vasp.py
345
396
  |-- convert.py
397
+ |-- realloc_occ.py
346
398
  |-- substitute.py
347
399
  |-- supercell.py
348
400
  |-- vasp2xyz.py
@@ -21,13 +21,13 @@ pip install pymdkit
21
21
  This installs the `pmk` command into the active conda environment, together
22
22
  with its direct dependencies, including ASE, pymatgen, py4vasp-core, dpdata, matplotlib, and UMAP. Some of these packages may install their own transitive dependencies.
23
23
 
24
- GPU-accelerated RMSD screening is optional. For a CUDA 12.x cluster (including
25
- the supplied `module load cuda/12.6` setup), install the matching CuPy extra:
26
-
27
- ```bash
28
- python -m pip install "pymdkit[gpu-cuda12]"
29
- ```
30
-
24
+ GPU-accelerated RMSD screening is optional. For a CUDA 12.x cluster (including
25
+ the supplied `module load cuda/12.6` setup), install the matching CuPy extra:
26
+
27
+ ```bash
28
+ python -m pip install "pymdkit[gpu-cuda12]"
29
+ ```
30
+
31
31
  Verify:
32
32
 
33
33
  ```bash
@@ -57,14 +57,15 @@ directory for job sub-folders automatically.
57
57
  | `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
58
58
  | `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
59
59
  | `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
60
- | `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
60
+ | `gpumd-msd` | Recursively extract directional SDC and Nernst-Einstein conductivity from GPUMD jobs |
61
61
  | `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
62
62
  | `perturb` | Generate perturbed structures with dpdata |
63
63
  | `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
64
64
  | `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
65
65
  | `rmsd` | Compare structures by RMSD and optionally remove higher-energy duplicates |
66
66
  | `chemsys-entry` | Download stable Materials Project structures for a chemical system |
67
- | `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
67
+ | `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
68
+ | `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
68
69
  | `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
69
70
  | `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
70
71
  | `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
@@ -84,7 +85,7 @@ pmk convert -if vasp-opted -of cif-opted -oe cif # folder convers
84
85
  pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
85
86
  pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
86
87
  pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
87
- pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -add-groups Li Y Cl
88
+ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
88
89
  # GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
89
90
  pmk vasp-relax -i opted.vasp # relax inputs in current dir
90
91
  pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
@@ -92,8 +93,8 @@ pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run
92
93
  pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
93
94
  pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
94
95
  pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
95
- pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
96
- pmk msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion for conductivity
96
+ pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
97
+ pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
97
98
  pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
98
99
  pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
99
100
  pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
@@ -105,6 +106,8 @@ pmk vasp2xyz # scans ./ for VASP output folde
105
106
  pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
106
107
  pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
107
108
  nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
109
+ pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
110
+ pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
108
111
  pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
109
112
  pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
110
113
  pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
@@ -128,7 +131,7 @@ pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
128
131
 
129
132
  pmk rmsd -i structure-1.vasp structure-2.vasp # one pair -> rmsd.txt
130
133
  pmk rmsd -if vasp-opted # every pair -> rmsd.txt
131
- pmk rmsd -if example -rm-duplicate -of unique-example # RMSD <= 0.1: keep lowest energy
134
+ pmk rmsd -if example -rm-duplicate -of unique-example # RMSD <= 0.1: keep lowest energy
132
135
  pmk rmsd -if example -rm-duplicate -of unique-example -gpu
133
136
  pmk symmetrize -i opted.cif -add_oxidation yes -o opted-symm.cif
134
137
  pmk symmetrize -if my_cifs/ -symprec 0.1 -add_oxidation no -of my_cifs-symm
@@ -173,6 +176,36 @@ dump_xyz -1 0 1 relaxed.xyz force
173
176
  run 1
174
177
  ```
175
178
 
179
+ `gpumd-msd` recursively discovers job folders at any depth below `-entry-path`
180
+ (the current directory by default). A job must contain `msd.out`, `model.xyz`,
181
+ `run.in`, and `thermo.out`; the common accidental name `thermos.out` is also
182
+ accepted. Following the [official GPUMD `msd.out` format](https://gpumd.org/gpumd/output_files/msd_out.html), each group block is read as three MSD columns
183
+ followed by three directional SDC columns. PMK takes the arithmetic mean over
184
+ the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
185
+ multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
186
+ Each job receives `msd/group_INDEX_ELEMENT_TEMP_msd.txt`, a matching `_sdc.txt`,
187
+ and a `_results.txt`. The root receives `sigma-sdc-summary.txt` with directional
188
+ and total diffusion coefficients and conductivities.
189
+
190
+ Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
191
+ of the final SDC-producing MD run and the official 18-column
192
+ [`thermo.out` format](https://gpumd.org/gpumd/output_files/thermo_out.html):
193
+ for NPT ensembles (`npt_scr`, `npt_mttk`, and other `npt_*` forms), PMK uses
194
+ the mean triclinic-box volume over that run; for NVT ensembles (`nvt_bdp`,
195
+ `nvt_ber`, and other `nvt_*` forms), it uses the final fixed-cell volume.
196
+ `V(A^3) * 1e-24 -> V(cm^3)`, exact SI values for elementary charge and the
197
+ Boltzmann constant, and a final `* 1000` conversion from S/cm to mS/cm.
198
+
199
+ `realloc-occ -i INPUT.cif -of OUTPUT -formula FORMULA` computes atom counts as
200
+ Wyckoff multiplicity times occupancy and uses `_cell_formula_units_Z` to map the
201
+ target reduced formula into the existing cell. It enumerates finite site-wise
202
+ occupancy allocations, retains only charge-balanced candidates using the CIF
203
+ oxidation-number loop, and preserves the original symmetry, coordinates,
204
+ Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
205
+ the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
206
+ and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
207
+ `-formula` options.
208
+
176
209
  `gather-fs` recursively scans all job folders below the current directory. For
177
210
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
178
211
  requires full convergence before copying the selected `-fs-name` as `.vasp`. The gathered VASP file's first line is replaced with `energy=<final_energy> eV` for later screening.
@@ -188,20 +221,20 @@ its RMSD to an already retained lower-energy representative is no greater than `
188
221
  energy from the second-line `energy=` tag. The report records every RMSD,
189
222
  duplicate decision, energy, group, and retained representative.
190
223
 
191
- `rmsd -if FOLDER -rm-duplicate -of OUTPUT -gpu` is intended for a GPU batch
192
- job such as the supplied Slurm `sub_pmk`. CuPy computes permutation-invariant,
193
- periodic distance fingerprints in CUDA batches and skips clearly dissimilar
194
- pairs; pymatgen `StructureMatcher` remains the final authority for every
195
- candidate RMSD and duplicate decision. `rmsd.txt` labels pair rows as `exact`,
196
- `gpu-filtered`, or `incompatible` and records the CUDA device and pair counts.
197
- The `-gpu` option requires both `-if` and `-rm-duplicate`.
198
-
199
- For the supplied CUDA 12.6 Slurm script, submit the GPU workflow with:
200
-
201
- ```bash
202
- sbatch sub_pmk
203
- ```
204
-
224
+ `rmsd -if FOLDER -rm-duplicate -of OUTPUT -gpu` is intended for a GPU batch
225
+ job such as the supplied Slurm `sub_pmk`. CuPy computes permutation-invariant,
226
+ periodic distance fingerprints in CUDA batches and skips clearly dissimilar
227
+ pairs; pymatgen `StructureMatcher` remains the final authority for every
228
+ candidate RMSD and duplicate decision. `rmsd.txt` labels pair rows as `exact`,
229
+ `gpu-filtered`, or `incompatible` and records the CUDA device and pair counts.
230
+ The `-gpu` option requires both `-if` and `-rm-duplicate`.
231
+
232
+ For the supplied CUDA 12.6 Slurm script, submit the GPU workflow with:
233
+
234
+ ```bash
235
+ sbatch sub_pmk
236
+ ```
237
+
205
238
  `symmetrize` uses pymatgen's spglib-backed `SpacegroupAnalyzer` for authoritative
206
239
  space-group detection and conventional-cell refinement. Its default Cartesian
207
240
  symmetry tolerance is `0.1 Angstrom` (`-symprec`), with a `5 degree` angle
@@ -214,26 +247,31 @@ per successfully generated CIF: filename, crystal system, space-group symbol,
214
247
  and space-group number. Crystal-system and space-group summaries are sorted
215
248
  from highest to lowest count, with alphabetical ordering for ties, followed by
216
249
  the total number of structures. Single-file mode places the report beside the
217
- output CIF; folder and CSV modes place it inside `-of`.
250
+ output CIF; folder and CSV modes place it inside `-of`. Folder mode keeps each
251
+ input stem unchanged, writing `NAME.cif` rather than `NAME-symm.cif`.
218
252
 
219
253
  Every CIF exported by `symmetrize`, `convert`, `substitute`, `chemsys-entry`,
220
254
  `supercell`, or `gpumd-group` is written through the same symmetry-aware helper.
221
255
  Each CIF places the IUCr-defined `_space_group_crystal_system` item immediately
222
- below its `data_...` header and contains both `_atom_site_symmetry_multiplicity`
223
- and `_atom_site_Wyckoff_symbol` in the atom-site loop. Multiplicity-letter
256
+ below its `data_...` header and contains both
257
+ `_atom_site_site_symmetry_multiplicity` and `_atom_site_Wyckoff_symbol` in the
258
+ atom-site loop. Multiplicity-letter
224
259
  assignments use the conventional
225
260
  International Tables setting from pymatgen/spglib and correspond to the
226
261
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
227
262
  the provenance URL is also recorded inside each generated CIF.
228
263
 
229
- CIF atom types and site labels use per-element inequivalent-site identifiers
230
- such as `Li1`, `Y1`, `Cl1`, and `Cl2`; oxidation numbers remain in the separate
264
+ CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
265
+ inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
266
+ The final atom-site loop starts with `_atom_site_label`, followed by
267
+ `_atom_site_type_symbol`, whose values are plain elements such as `Li`, `Y`,
268
+ and `Cl`. Oxidation numbers remain in the separate
231
269
  `_atom_type_oxidation_number` column. PMK does not add an
232
270
  `_audit_creation_method` block or a generated-by header comment.
233
271
 
234
272
  Newly serialized floating-point values use eight digits after the decimal
235
273
  point across PMK structure, trajectory, data, report, and screen output. CIF
236
- `_atom_site_occupancy` values use two digits and
274
+ `_atom_site_occupancy` values use two digits and
237
275
  `_atom_type_oxidation_number` values use one digit. In extended XYZ, zero components
238
276
  inside the second-line `Lattice="..."` value remain `0.0`; other generated
239
277
  floating-point values use eight digits. Commands that select or concatenate
@@ -246,8 +284,20 @@ existing trajectory frames continue copying those frame bytes exactly.
246
284
 
247
285
  When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every frame and writes one additional descriptor table per value, such as `pca-descriptor-P-3m1.txt` and `pca-descriptor-Pnma.txt`. With `-fps`, it also writes matching files such as `fps-0.01-pca-descriptor-P-3m1.txt` and `fps-0.01-train-P-3m1.xyz`. These files partition the single global FPS selection; FPS is not rerun independently for each `Config_type`. Frames without `Config_type` remain in the main outputs but do not receive a type-specific file.
248
286
 
249
- For folder conversion, `convert -if INPUT -of OUTPUT` writes `.xyz` by default. Set another target extension with `-oe`, for example `-oe cif` or `-oe vasp`. Single-file and trajectory modes infer the target format from `-o` and `-ot`.
250
- Because Wyckoff metadata describes one crystal structure, each `.cif` output file accepts exactly one frame.
287
+ For folder conversion, `convert -if INPUT -o OUTPUT` and
288
+ `convert -if INPUT -of OUTPUT` are equivalent and write `.xyz` by default. Set
289
+ another target extension with `-oe`, for example `-oe cif` or `-oe vasp`.
290
+ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
291
+ Because Wyckoff metadata describes one crystal structure, each `.cif` output
292
+ file accepts exactly one frame.
293
+
294
+ After `supercell -temp` creates all requested jobs, it checks generated folders
295
+ that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
296
+ lines retain their temperature parameters but are rewritten with three
297
+ pressure/modulus components for 90-degree orthorhombic cells and all six
298
+ components for non-orthorhombic cells, following the
299
+ [official GPUMD ensemble syntax](https://gpumd.org/gpumd/input_parameters/ensemble_standard.html).
300
+ Other ensemble types are not modified. Use `-group Li Y Cl` to add GPUMD groups.
251
301
 
252
302
  `gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. An abnormal classification always requires a mapped potential-energy change. When at least two lattice-parameter or volume signals show the same mapped transition, they may enable a more sensitive noise-relative energy test; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Cell signals can corroborate and refine the energy interval, but can never classify a run by themselves. Temperature and all six pressure components remain diagnostics only and cannot classify an otherwise stationary run as abnormal by themselves. A lone cell-axis drift, an isolated spike, or an empty anomaly mask never triggers a whole-run fallback. Thermo plots use Arial when available and Matplotlib's built-in qualitative `Dark2` color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and show abnormal regions as shaded legend entries on potential energy, lattice parameters, and volume. Potential energy and volume are divided by 1000 and labeled ×10³ only when their absolute plotted values reach 1000. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
253
303
 
@@ -301,7 +351,8 @@ pymdkit/
301
351
  |-- _gpumd.py # shared GPUMD run.in and thermo analysis
302
352
  |-- _geometry.py # shared minimum-image geometry helper
303
353
  |-- _vaspset.py # shared VASP input-set helper (not a command)
304
- |-- gpumd_group.py
354
+ |-- gpumd_group.py
355
+ |-- gpumd_msd.py
305
356
  |-- gpumd_relax.py
306
357
  |-- gpumd_thermo.py
307
358
  |-- gpumd_trjcat.py
@@ -315,7 +366,8 @@ pymdkit/
315
366
  |-- _cifio.py
316
367
  |-- chemsys_entry.py
317
368
  |-- submit_vasp.py
318
- |-- convert.py
369
+ |-- convert.py
370
+ |-- realloc_occ.py
319
371
  |-- substitute.py
320
372
  |-- supercell.py
321
373
  |-- vasp2xyz.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.5.0"
7
+ version = "1.5.2"
8
8
  description = "A unified command-line toolkit for atomistic / MD structure workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -26,7 +26,7 @@ classifiers = [
26
26
  dependencies = [
27
27
  "numpy",
28
28
  "ase",
29
- "pymatgen",
29
+ "pymatgen",
30
30
  "mp-api",
31
31
  "py4vasp-core",
32
32
  "dpdata",
@@ -34,14 +34,14 @@ dependencies = [
34
34
  "umap-learn",
35
35
  ]
36
36
 
37
- [project.optional-dependencies]
38
- # CuPy wheels are CUDA-major-specific, so GPU support stays optional.
39
- gpu-cuda12 = ["cupy-cuda12x"]
40
-
37
+ [project.optional-dependencies]
38
+ # CuPy wheels are CUDA-major-specific, so GPU support stays optional.
39
+ gpu-cuda12 = ["cupy-cuda12x"]
40
+
41
41
  [project.scripts]
42
42
  # The distribution remains pymdkit; the concise public executable is `pmk`.
43
43
  pmk = "pymdkit.pymdkit_main:main"
44
44
 
45
45
  [tool.setuptools.packages.find]
46
46
  where = ["src"]
47
-
47
+
@@ -1,3 +1,3 @@
1
1
  """pymdkit -- a unified CLI for atomistic / MD structure workflows."""
2
2
 
3
- __version__ = "1.5.0"
3
+ __version__ = "1.5.2"
@@ -15,14 +15,18 @@ DEFAULT_SYMPREC = 0.02
15
15
  DEFAULT_ANGLE_TOLERANCE = 5.0
16
16
  BILBAO_WYCKPOS_URL = "https://cryst.ehu.es/cryst/get_wp.html"
17
17
  WYCKOFF_TAG = "_atom_site_Wyckoff_symbol"
18
- MULTIPLICITY_TAG = "_atom_site_symmetry_multiplicity"
18
+ SOURCE_MULTIPLICITY_TAG = "_atom_site_symmetry_multiplicity"
19
+ MULTIPLICITY_TAG = "_atom_site_site_symmetry_multiplicity"
19
20
  CRYSTAL_SYSTEM_TAG = "_space_group_crystal_system"
20
21
  CRYSTAL_SYSTEMS = {
21
22
  "triclinic", "monoclinic", "orthorhombic", "tetragonal",
22
23
  "trigonal", "hexagonal", "cubic",
23
24
  }
24
- OCCUPANCY_TAG = "_atom_site_occupancy"
25
+ OCCUPANCY_TAG = "_atom_site_occupancy"
25
26
  OXIDATION_TAG = "_atom_type_oxidation_number"
27
+ ATOM_TYPE_TAG = "_atom_type_symbol"
28
+ SITE_LABEL_TAG = "_atom_site_label"
29
+ SITE_TYPE_TAG = "_atom_site_type_symbol"
26
30
  CIF_FLOAT_TAGS = {
27
31
  "_cell_length_a", "_cell_length_b", "_cell_length_c",
28
32
  "_cell_angle_alpha", "_cell_angle_beta", "_cell_angle_gamma",
@@ -133,8 +137,27 @@ def _site_wyckoff_columns(block, symmetrized):
133
137
  return letters
134
138
 
135
139
 
140
+ def _rename_block_tag(block, old_tag, new_tag):
141
+ if old_tag not in block.data:
142
+ if new_tag not in block.data:
143
+ raise KeyError(f"CIF block has neither {old_tag} nor {new_tag}")
144
+ return
145
+
146
+ ordered = []
147
+ for tag, value in block.data.items():
148
+ if tag == old_tag:
149
+ ordered.append((new_tag, value))
150
+ elif tag != new_tag:
151
+ ordered.append((tag, value))
152
+ block.data.clear()
153
+ block.data.update(ordered)
154
+ for loop in block.loops:
155
+ loop[:] = [new_tag if tag == old_tag else tag for tag in loop]
156
+
157
+
136
158
  def _add_wyckoff_column(cif_file, symmetrized):
137
159
  block = next(iter(cif_file.data.values()))
160
+ _rename_block_tag(block, SOURCE_MULTIPLICITY_TAG, MULTIPLICITY_TAG)
138
161
  letters = _site_wyckoff_columns(block, symmetrized)
139
162
  block.data[WYCKOFF_TAG] = letters
140
163
  atom_loop = next(
@@ -174,11 +197,11 @@ def _format_cif_scalar(value, precision, force=False):
174
197
 
175
198
  def _format_cif_numbers(block):
176
199
  for tag, value in list(block.data.items()):
177
- if tag == OCCUPANCY_TAG:
178
- precision = 2
179
- elif tag == OXIDATION_TAG:
180
- precision = 1
181
- else:
200
+ if tag == OCCUPANCY_TAG:
201
+ precision = 2
202
+ elif tag == OXIDATION_TAG:
203
+ precision = 1
204
+ else:
182
205
  precision = _precision.FLOAT_PRECISION
183
206
  force = tag == OCCUPANCY_TAG or tag in CIF_FLOAT_TAGS
184
207
  if isinstance(value, (list, tuple)):
@@ -198,25 +221,92 @@ def _element_symbol(type_symbol):
198
221
 
199
222
 
200
223
  def _relabel_atom_types(block):
201
- """Give each inequivalent CIF site an element-index type and label."""
202
- old_types = list(block.data["_atom_type_symbol"])
203
- old_oxidations = list(block.data["_atom_type_oxidation_number"])
224
+ """Separate unique site labels from elemental site type symbols."""
225
+ site_types = list(block.data[SITE_TYPE_TAG])
226
+ old_types = list(block.data.get(ATOM_TYPE_TAG, []))
227
+ old_oxidations = list(block.data.get(OXIDATION_TAG, []))
204
228
  oxidation_by_type = dict(zip(old_types, old_oxidations))
205
- site_types = list(block.data["_atom_site_type_symbol"])
229
+ oxidation_by_element = {}
230
+ for old_type, oxidation in oxidation_by_type.items():
231
+ oxidation_by_element.setdefault(_element_symbol(old_type), oxidation)
206
232
 
207
233
  counts = {}
208
234
  labels = []
235
+ elements = []
209
236
  oxidations = []
210
237
  for old_type in site_types:
211
238
  element = _element_symbol(old_type)
212
239
  counts[element] = counts.get(element, 0) + 1
213
240
  labels.append(f"{element}{counts[element]}")
214
- oxidations.append(oxidation_by_type.get(old_type, 0))
241
+ elements.append(element)
242
+ oxidations.append(
243
+ oxidation_by_type.get(
244
+ old_type,
245
+ oxidation_by_element.get(element, 0),
246
+ )
247
+ )
215
248
 
216
- block.data["_atom_type_symbol"] = labels
217
- block.data["_atom_type_oxidation_number"] = oxidations
218
- block.data["_atom_site_type_symbol"] = labels
219
- block.data["_atom_site_label"] = labels
249
+ atom_site_loop = next(
250
+ (loop for loop in block.loops if SITE_TYPE_TAG in loop),
251
+ None,
252
+ )
253
+ if atom_site_loop is None:
254
+ raise KeyError(f"CIF block has no atom-site loop containing {SITE_TYPE_TAG}")
255
+ atom_site_loop[:] = [
256
+ SITE_LABEL_TAG,
257
+ SITE_TYPE_TAG,
258
+ *(
259
+ tag for tag in atom_site_loop
260
+ if tag not in {SITE_LABEL_TAG, SITE_TYPE_TAG}
261
+ ),
262
+ ]
263
+
264
+ type_loop_index = None
265
+ cleaned_loops = []
266
+ for loop in block.loops:
267
+ if ATOM_TYPE_TAG in loop or OXIDATION_TAG in loop:
268
+ if type_loop_index is None:
269
+ type_loop_index = len(cleaned_loops)
270
+ remaining = [
271
+ tag for tag in loop
272
+ if tag not in {ATOM_TYPE_TAG, OXIDATION_TAG}
273
+ ]
274
+ if remaining:
275
+ cleaned_loops.append(remaining)
276
+ else:
277
+ cleaned_loops.append(loop)
278
+ atom_type_loop = [ATOM_TYPE_TAG, OXIDATION_TAG]
279
+ if type_loop_index is None:
280
+ site_loop_index = cleaned_loops.index(atom_site_loop)
281
+ cleaned_loops.insert(site_loop_index, atom_type_loop)
282
+ else:
283
+ cleaned_loops.insert(type_loop_index, atom_type_loop)
284
+ block.loops[:] = cleaned_loops
285
+
286
+ ordered = []
287
+ atom_types_inserted = False
288
+ for tag, value in block.data.items():
289
+ if tag in {ATOM_TYPE_TAG, OXIDATION_TAG}:
290
+ continue
291
+ if not atom_types_inserted and tag in atom_site_loop:
292
+ ordered.extend([
293
+ (ATOM_TYPE_TAG, labels),
294
+ (OXIDATION_TAG, oxidations),
295
+ ])
296
+ atom_types_inserted = True
297
+ if tag == SITE_LABEL_TAG:
298
+ value = labels
299
+ elif tag == SITE_TYPE_TAG:
300
+ value = elements
301
+ ordered.append((tag, value))
302
+ if not atom_types_inserted:
303
+ ordered.extend([
304
+ (ATOM_TYPE_TAG, labels),
305
+ (OXIDATION_TAG, oxidations),
306
+ ])
307
+
308
+ block.data.clear()
309
+ block.data.update(ordered)
220
310
  return labels
221
311
 
222
312
 
@@ -109,14 +109,13 @@ def equilibrium_start(length, runs):
109
109
  return max(0, length - points)
110
110
  return int(length * 0.2)
111
111
 
112
- def cell_quantities(data):
113
- if data.shape[1] != 18:
114
- raise ValueError(
115
- f"thermo.out has {data.shape[1]} columns; expected the official 18-column format"
116
- )
117
-
118
- vectors = data[:, 9:18].reshape(-1, 3, 3)
119
- lengths = np.linalg.norm(vectors, axis=2)
112
+ def lattice_angles(vectors):
113
+ """Return alpha, beta, gamma in degrees for one or more 3x3 cells."""
114
+ vectors = np.asarray(vectors, dtype=float)
115
+ single = vectors.ndim == 2
116
+ if vectors.shape[-2:] != (3, 3):
117
+ raise ValueError(f"cell vectors must have shape (..., 3, 3), got {vectors.shape}")
118
+ vectors = vectors.reshape(-1, 3, 3)
120
119
 
121
120
  def angle(first, second):
122
121
  denominator = np.linalg.norm(first, axis=1) * np.linalg.norm(second, axis=1)
@@ -131,6 +130,18 @@ def cell_quantities(data):
131
130
  angle(vectors[:, 2], vectors[:, 0]),
132
131
  angle(vectors[:, 0], vectors[:, 1]),
133
132
  ])
133
+ return angles[0] if single else angles
134
+
135
+
136
+ def cell_quantities(data):
137
+ if data.shape[1] != 18:
138
+ raise ValueError(
139
+ f"thermo.out has {data.shape[1]} columns; expected the official 18-column format"
140
+ )
141
+
142
+ vectors = data[:, 9:18].reshape(-1, 3, 3)
143
+ lengths = np.linalg.norm(vectors, axis=2)
144
+ angles = lattice_angles(vectors)
134
145
  return lengths, angles, np.abs(np.linalg.det(vectors))
135
146
 
136
147