pymdkit 1.6.0__tar.gz → 1.6.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 (58) hide show
  1. {pymdkit-1.6.0 → pymdkit-1.6.2}/PKG-INFO +63 -34
  2. {pymdkit-1.6.0 → pymdkit-1.6.2}/README.md +78 -49
  3. {pymdkit-1.6.0 → pymdkit-1.6.2}/pyproject.toml +1 -1
  4. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/__init__.py +1 -1
  5. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_cifio.py +17 -4
  6. pymdkit-1.6.2/src/pymdkit/commands/_supercell.py +136 -0
  7. pymdkit-1.6.0/src/pymdkit/commands/symmetrize.py → pymdkit-1.6.2/src/pymdkit/commands/_symmetry.py +18 -144
  8. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_vaspset.py +70 -37
  9. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/compute_rmsd.py +3 -0
  10. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/convert.py +327 -87
  11. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_group.py +2 -2
  12. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/realloc_occ.py +6 -0
  13. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/substitute.py +5 -1
  14. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/supercell.py +98 -57
  15. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_relax.py +1 -1
  16. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_static.py +1 -1
  17. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/PKG-INFO +63 -34
  18. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/SOURCES.txt +2 -1
  19. {pymdkit-1.6.0 → pymdkit-1.6.2}/tests/test_convert.py +164 -6
  20. {pymdkit-1.6.0 → pymdkit-1.6.2}/tests/test_realloc_occ.py +4 -0
  21. pymdkit-1.6.2/tests/test_vaspset.py +193 -0
  22. pymdkit-1.6.0/tests/test_vaspset.py +0 -74
  23. {pymdkit-1.6.0 → pymdkit-1.6.2}/LICENSE +0 -0
  24. {pymdkit-1.6.0 → pymdkit-1.6.2}/setup.cfg +0 -0
  25. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/__init__.py +0 -0
  26. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_extxyz.py +0 -0
  27. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_fileio.py +0 -0
  28. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_geometry.py +0 -0
  29. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_gpumd.py +0 -0
  30. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_lattice.py +0 -0
  31. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_oxidation.py +0 -0
  32. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_precision.py +0 -0
  33. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_reduction.py +0 -0
  34. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/_vasp_output.py +0 -0
  35. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/add_config_type.py +0 -0
  36. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
  37. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/compute_ehull.py +0 -0
  38. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/ewald.py +0 -0
  39. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gather_fs.py +0 -0
  40. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_msd.py +0 -0
  41. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_relax.py +0 -0
  42. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_thermo.py +0 -0
  43. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
  44. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/nep_rmse.py +0 -0
  45. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/pca.py +0 -0
  46. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/perturb.py +0 -0
  47. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/submit_vasp.py +0 -0
  48. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/trj_extract.py +0 -0
  49. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/trj_sparse.py +0 -0
  50. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/umap.py +0 -0
  51. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/vasp2xyz.py +0 -0
  52. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/commands/vasp_fe.py +0 -0
  53. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit/pymdkit_main.py +0 -0
  54. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  55. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
  56. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/requires.txt +0 -0
  57. {pymdkit-1.6.0 → pymdkit-1.6.2}/src/pymdkit.egg-info/top_level.txt +0 -0
  58. {pymdkit-1.6.0 → pymdkit-1.6.2}/tests/test_lattice_symmetry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymdkit
3
- Version: 1.6.0
3
+ Version: 1.6.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
@@ -91,11 +91,11 @@ directory for job sub-folders automatically.
91
91
  | `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
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
- | `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
94
+ | `convert` | Convert structures/trajectories/folders, refine CIF symmetry, or extract CSV structures |
95
95
  | `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
96
96
  | `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
97
- | `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
98
- | `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
97
+ | `supercell` | Build a supercell by matrix, maximum length, or nearest target length; optional GPUMD setup |
98
+
99
99
  | `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
100
100
  | `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |
101
101
 
@@ -111,16 +111,22 @@ pmk convert -i NaCl.vasp -o NaCl.cif # CIF defaults t
111
111
  pmk convert -i NaCl.vasp -o NaCl-symm.cif -symm # detect/import symmetry
112
112
  pmk convert -if vasp-opted -of cif-opted -oe cif # folder CIFs default to P1
113
113
  pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
114
- pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -supercell 1x1x1
115
- pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
114
+ pmk convert -i model.cif -o matrix.cif -supercell-matrix 1x1x2
115
+ pmk convert -i model.cif -o capped.cif -supercell-max-abc 40
116
+ pmk convert -i model.cif -o close.cif -supercell-close-abc 20
117
+ pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -supercell-matrix 1x1x1
118
+ pmk supercell -i opted.vasp -o matrix.vasp -matrix 1x1x2
119
+ pmk supercell -i opted.xyz -o grouped.xyz -matrix 2x2x2 -gpumd-group Li Y Cl
120
+ pmk supercell -i opted.vasp -o capped.vasp -max-abc 40
121
+ pmk supercell -i opted.vasp -o close.vasp -close-abc 20 # nearest target: e.g. 21,20,20 A
116
122
  pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
117
- pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
123
+ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -gpumd-group Li Y Cl
118
124
  # GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
119
- pmk vasp-relax -i opted.vasp -max-abc 20 # expand cell, then write relax inputs
125
+ pmk vasp-relax -i opted.vasp -supercell-max-abc 20 # expand cell, then write relax inputs
120
126
  pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
121
127
  pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run.in + NEP per job folder
122
128
  pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
123
- pmk vasp-static -if cifs/ -max-abc 20 -custom-setting my_incar.txt # expand cell, static inputs
129
+ pmk vasp-static -if cifs/ -supercell-close-abc 20 -custom-setting my_incar.txt
124
130
  pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
125
131
  pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
126
132
  pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
@@ -164,9 +170,9 @@ pmk rmsd -i structure-1.vasp structure-2.vasp # one pair -> rmsd.txt
164
170
  pmk rmsd -if vasp-opted # every pair -> rmsd.txt
165
171
  pmk rmsd -if example -rm-duplicate -of unique-example # RMSD <= 0.1: keep lowest energy
166
172
  pmk rmsd -if example -rm-duplicate -of unique-example -gpu
167
- pmk symmetrize -i opted.cif -add_oxidation yes -o opted-symm.cif
168
- pmk symmetrize -if my_cifs/ -symprec 0.1 -add_oxidation no -of my_cifs-symm
169
- pmk symmetrize -csv output_Li2YCl3_struct.csv -of Li2YCl3
173
+ pmk convert -i opted.cif -o opted-symm.cif -symm -add-oxidation yes
174
+ pmk convert -if my_cifs/ -of my_cifs-symm -oe cif -symm -symprec 0.1 -add-oxidation no
175
+ pmk convert -csv output_Li2YCl3_struct.csv -of Li2YCl3 -symm
170
176
  ```
171
177
 
172
178
  VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
@@ -174,9 +180,11 @@ jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
174
180
  `-if` creates one `./<name>/` folder per structure, and `-it` creates one
175
181
  `./frame_N/` folder per trajectory frame - all directly in the current path.
176
182
 
177
- Both commands accept `-max-abc L` for single files, folders, and trajectories.
178
- Before writing each VASP job, each lattice vector is repeated
179
- `max(1, floor(L / cell_length))` times, matching the `supercell` command.
183
+ Both commands accept exactly one of `-supercell-matrix`,
184
+ `-supercell-max-abc`, or `-supercell-close-abc` for single files, folders, and
185
+ trajectories. The matrix mode accepts `1x1x2`, three diagonal factors, or nine
186
+ integer entries. Maximum mode uses `max(1, floor(L / cell_length))`; close mode
187
+ uses the positive integer repeat nearest `L / cell_length`.
180
188
 
181
189
  They start from sensible default INCAR settings; override them by passing a
182
190
  settings file with `-custom-setting FILE`. The file may be a Python-dict block
@@ -255,8 +263,9 @@ and `0.875`. Generated CIF occupancy fields use two decimal places.
255
263
  `-max-supercell AxBxC` searches bounded diagonal supercells, smallest volume
256
264
  first, until every multiplicity times occupancy times supercell determinant is
257
265
  an integer. Equal-volume choices favor the most isotropic resulting cell.
258
- All selections, site atom counts, oxidation states, charges, and failures are
259
- recorded in `OUTPUT/realloc-occ.txt`.
266
+ All selections, site atom counts, oxidation states, charges, failures, the
267
+ allowed occupancy grid, and the final summary are recorded in
268
+ `OUTPUT/realloc-occ.txt` for both single-file and folder modes.
260
269
 
261
270
  `gather-fs` recursively scans all job folders below the current directory. For
262
271
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
@@ -287,13 +296,13 @@ For the supplied CUDA 12.6 Slurm script, submit the GPU workflow with:
287
296
  sbatch sub_pmk
288
297
  ```
289
298
 
290
- `symmetrize` uses pymatgen's spglib-backed `SpacegroupAnalyzer` for authoritative
299
+ `convert -symm` uses pymatgen's spglib-backed `SpacegroupAnalyzer` for authoritative
291
300
  space-group detection and conventional-cell refinement. Its default Cartesian
292
301
  symmetry tolerance is `0.1 Angstrom` (`-symprec`), with a `5 degree` angle
293
302
  tolerance (`-angle-tolerance`). CIF occupancies are retained by the pymatgen
294
303
  structure and written directly rather than reconstructed after symmetry finding.
295
- CSV mode reads serialized pymatgen `Structure` dictionaries from a required `cif`
296
- column. `-csv INPUT.csv -of NAME` writes `INDEX-NAME-SPACEGROUP.cif`, for
304
+ With `-symm`, CSV mode reads serialized pymatgen `Structure` dictionaries from a required `cif`
305
+ column. `-csv INPUT.csv -of NAME -symm` writes `INDEX-NAME-SPACEGROUP.cif`, for
297
306
  example `1-Li2YCl3-66.cif`. Every run also writes `symmetry.txt` with one row
298
307
  per successfully generated CIF: filename, crystal system, space-group symbol,
299
308
  and space-group number. Crystal-system and space-group summaries are sorted
@@ -309,7 +318,7 @@ the `_symmetry_equiv_pos_as_xyz` loop is sourced from pymatgen's `SpaceGroup`
309
318
  database instead of serializing noisy numerical translations returned by
310
319
  structure-specific symmetry detection.
311
320
 
312
- Every CIF exported by `symmetrize`, `convert`, `substitute`, `chemsys-entry`,
321
+ Every CIF exported by `convert`, `substitute`, `chemsys-entry`,
313
322
  `supercell`, or `gpumd-group` is written through the same CIF helper. `convert`
314
323
  writes P1 by default and performs symmetry detection only when `-symm` is
315
324
  present; the other commands retain their command-specific symmetry behavior.
@@ -322,6 +331,14 @@ International Tables setting from pymatgen/spglib and correspond to the
322
331
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
323
332
  the provenance URL is also recorded inside each generated CIF.
324
333
 
334
+ For structural conversion, supercell creation, and GPUMD grouping, PMK reads
335
+ CIF input with pymatgen rather than ASE. ASE interprets
336
+ `_space_group_crystal_system` as a setting hint only for rhombohedral groups
337
+ and emits a potentially misleading warning for other crystal systems. Using
338
+ pymatgen avoids that ambiguity while PMK continues to use ASE for non-CIF
339
+ formats. A partially occupied CIF must first be ordered with `convert -d2o`
340
+ before it can be represented as an explicit atom list.
341
+
325
342
  CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
326
343
  inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
327
344
  The final atom-site loop starts with `_atom_site_label`, followed by
@@ -357,18 +374,28 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
357
374
  Because Wyckoff metadata describes one crystal structure, each `.cif` output
358
375
  file accepts exactly one frame.
359
376
 
360
- `convert -d2o` orders partially occupied CIF structures. `-supercell` defaults
361
- to `1x1x1`; three diagonal factors or nine integer matrix entries are also
362
- accepted. Folder ordering scans recursively, groups `BASE-dN.cif` files by
363
- `BASE`, and writes only the lowest-total-Ewald candidate into the flat output
364
- folder as `BASE-oN.cif`, where `N` remains tied to its source `-dN` file.
365
- Pymatgen `EnumerateStructureTransformation` is used when enumlib is available;
366
- the expected missing-enumlib runtime error quietly switches to
377
+ For ordinary conversion, the three supercell options are mutually exclusive and
378
+ are resolved separately for every input structure. For example, a cell with
379
+ `a=3 A`, `b=4 A`, and `c=5 A` plus `-supercell-close-abc 20` uses `7x5x4`,
380
+ giving approximately `21 A`, `20 A`, and `20 A`. The numeric value is the target:
381
+ `-supercell-close-abc 40` targets approximately `40 A`.
382
+
383
+ `convert -d2o` orders partially occupied CIF structures.
384
+ `-supercell-matrix` defaults internally to `1x1x1` in this mode; three
385
+ diagonal factors or nine integer matrix entries are accepted. Because Ewald
386
+ candidates must be compared using one fixed cell, `-supercell-max-abc` and
387
+ `-supercell-close-abc` are not accepted with `-d2o`. Folder ordering scans
388
+ recursively, groups `BASE-dN.cif` files by `BASE`, and writes only the
389
+ lowest-total-Ewald candidate into the flat output folder as `BASE-oN.cif`,
390
+ where `N` remains tied to its source `-dN` file. Pymatgen
391
+ `EnumerateStructureTransformation` is used when enumlib is available; the
392
+ expected missing-enumlib runtime error quietly switches to
367
393
  `OrderDisorderedStructureTransformation`. Every output CIF records total Ewald
368
394
  energy on line two as `# E_e = VALUE eV`, and `d2o.txt` records all candidates,
369
395
  methods, energies, failures, and selections. Add `-symm` to import symmetry into
370
- the ordered CIF; otherwise it is P1. Without `-d2o`, `-supercell` only builds an
371
- ordinary structural supercell and never enables ordering automatically.
396
+ the ordered CIF; otherwise it is P1. Without `-d2o`, the
397
+ `-supercell-matrix`, `-supercell-max-abc`, and `-supercell-close-abc` options
398
+ build an ordinary structural supercell and never enable ordering automatically.
372
399
 
373
400
  After `supercell -temp` creates all requested jobs, it checks generated folders
374
401
  that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
@@ -376,8 +403,10 @@ lines retain their temperature parameters but are rewritten with three
376
403
  pressure/modulus components for 90-degree orthorhombic cells and all six
377
404
  components for non-orthorhombic cells, following the
378
405
  [official GPUMD ensemble syntax](https://gpumd.org/gpumd/input_parameters/ensemble_standard.html).
379
- Other ensemble types are not modified. Use `-group Li Y Cl` to add GPUMD groups.
380
-
406
+ Other ensemble types are not modified. Use `-gpumd-group Li Y Cl` to embed the
407
+ standalone `gpumd-group` tagging behavior in generated `model.xyz` files. The
408
+ same option also tags ordinary `.xyz` or `.extxyz` supercell output; formats
409
+ that cannot preserve GPUMD group arrays are rejected before output is written.
381
410
  `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.
382
411
 
383
412
  GPUMD box matrices are interpreted row by row by
@@ -460,7 +489,7 @@ pymdkit/
460
489
  |-- vasp_relax.py
461
490
  |-- vasp_static.py
462
491
  |-- ...
463
- `-- symmetrize.py
492
+ `-- _symmetry.py # private symmetry conversion/report helper
464
493
  ```
465
494
 
466
495
  Modules whose name starts with `_` are shared helpers and are skipped by the
@@ -498,5 +527,5 @@ fast and a missing optional dependency only affects the one command that needs i
498
527
  Every command module still works directly, which is handy for debugging:
499
528
 
500
529
  ```bash
501
- python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
530
+ python src/pymdkit/commands/supercell.py -i in.cif -close-abc 20 -o sc.vasp
502
531
  ```
@@ -64,11 +64,11 @@ directory for job sub-folders automatically.
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/folders, refine CIF symmetry, or extract CSV structures |
68
68
  | `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
69
69
  | `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
70
- | `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
71
- | `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
70
+ | `supercell` | Build a supercell by matrix, maximum length, or nearest target length; optional GPUMD setup |
71
+
72
72
  | `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
73
73
  | `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |
74
74
 
@@ -83,17 +83,23 @@ pmk gpumd-group -elements Li Y Cl # scan subfolders
83
83
  pmk convert -i NaCl.vasp -o NaCl.cif # CIF defaults to P1
84
84
  pmk convert -i NaCl.vasp -o NaCl-symm.cif -symm # detect/import symmetry
85
85
  pmk convert -if vasp-opted -of cif-opted -oe cif # folder CIFs default to P1
86
- pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
87
- pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -supercell 1x1x1
88
- pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
86
+ pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
87
+ pmk convert -i model.cif -o matrix.cif -supercell-matrix 1x1x2
88
+ pmk convert -i model.cif -o capped.cif -supercell-max-abc 40
89
+ pmk convert -i model.cif -o close.cif -supercell-close-abc 20
90
+ pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -supercell-matrix 1x1x1
91
+ pmk supercell -i opted.vasp -o matrix.vasp -matrix 1x1x2
92
+ pmk supercell -i opted.xyz -o grouped.xyz -matrix 2x2x2 -gpumd-group Li Y Cl
93
+ pmk supercell -i opted.vasp -o capped.vasp -max-abc 40
94
+ pmk supercell -i opted.vasp -o close.vasp -close-abc 20 # nearest target: e.g. 21,20,20 A
89
95
  pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
90
- pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
96
+ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -gpumd-group Li Y Cl
91
97
  # GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
92
- pmk vasp-relax -i opted.vasp -max-abc 20 # expand cell, then write relax inputs
98
+ pmk vasp-relax -i opted.vasp -supercell-max-abc 20 # expand cell, then write relax inputs
93
99
  pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
94
100
  pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run.in + NEP per job folder
95
101
  pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
96
- pmk vasp-static -if cifs/ -max-abc 20 -custom-setting my_incar.txt # expand cell, static inputs
102
+ pmk vasp-static -if cifs/ -supercell-close-abc 20 -custom-setting my_incar.txt
97
103
  pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
98
104
  pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
99
105
  pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
@@ -137,9 +143,9 @@ pmk rmsd -i structure-1.vasp structure-2.vasp # one pair -> rmsd.txt
137
143
  pmk rmsd -if vasp-opted # every pair -> rmsd.txt
138
144
  pmk rmsd -if example -rm-duplicate -of unique-example # RMSD <= 0.1: keep lowest energy
139
145
  pmk rmsd -if example -rm-duplicate -of unique-example -gpu
140
- pmk symmetrize -i opted.cif -add_oxidation yes -o opted-symm.cif
141
- pmk symmetrize -if my_cifs/ -symprec 0.1 -add_oxidation no -of my_cifs-symm
142
- pmk symmetrize -csv output_Li2YCl3_struct.csv -of Li2YCl3
146
+ pmk convert -i opted.cif -o opted-symm.cif -symm -add-oxidation yes
147
+ pmk convert -if my_cifs/ -of my_cifs-symm -oe cif -symm -symprec 0.1 -add-oxidation no
148
+ pmk convert -csv output_Li2YCl3_struct.csv -of Li2YCl3 -symm
143
149
  ```
144
150
 
145
151
  VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
@@ -147,9 +153,11 @@ jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
147
153
  `-if` creates one `./<name>/` folder per structure, and `-it` creates one
148
154
  `./frame_N/` folder per trajectory frame - all directly in the current path.
149
155
 
150
- Both commands accept `-max-abc L` for single files, folders, and trajectories.
151
- Before writing each VASP job, each lattice vector is repeated
152
- `max(1, floor(L / cell_length))` times, matching the `supercell` command.
156
+ Both commands accept exactly one of `-supercell-matrix`,
157
+ `-supercell-max-abc`, or `-supercell-close-abc` for single files, folders, and
158
+ trajectories. The matrix mode accepts `1x1x2`, three diagonal factors, or nine
159
+ integer entries. Maximum mode uses `max(1, floor(L / cell_length))`; close mode
160
+ uses the positive integer repeat nearest `L / cell_length`.
153
161
 
154
162
  They start from sensible default INCAR settings; override them by passing a
155
163
  settings file with `-custom-setting FILE`. The file may be a Python-dict block
@@ -228,9 +236,10 @@ and `0.875`. Generated CIF occupancy fields use two decimal places.
228
236
  `-max-supercell AxBxC` searches bounded diagonal supercells, smallest volume
229
237
  first, until every multiplicity times occupancy times supercell determinant is
230
238
  an integer. Equal-volume choices favor the most isotropic resulting cell.
231
- All selections, site atom counts, oxidation states, charges, and failures are
232
- recorded in `OUTPUT/realloc-occ.txt`.
233
-
239
+ All selections, site atom counts, oxidation states, charges, failures, the
240
+ allowed occupancy grid, and the final summary are recorded in
241
+ `OUTPUT/realloc-occ.txt` for both single-file and folder modes.
242
+
234
243
  `gather-fs` recursively scans all job folders below the current directory. For
235
244
  VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
236
245
  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.
@@ -260,13 +269,13 @@ For the supplied CUDA 12.6 Slurm script, submit the GPU workflow with:
260
269
  sbatch sub_pmk
261
270
  ```
262
271
 
263
- `symmetrize` uses pymatgen's spglib-backed `SpacegroupAnalyzer` for authoritative
272
+ `convert -symm` uses pymatgen's spglib-backed `SpacegroupAnalyzer` for authoritative
264
273
  space-group detection and conventional-cell refinement. Its default Cartesian
265
274
  symmetry tolerance is `0.1 Angstrom` (`-symprec`), with a `5 degree` angle
266
275
  tolerance (`-angle-tolerance`). CIF occupancies are retained by the pymatgen
267
276
  structure and written directly rather than reconstructed after symmetry finding.
268
- CSV mode reads serialized pymatgen `Structure` dictionaries from a required `cif`
269
- column. `-csv INPUT.csv -of NAME` writes `INDEX-NAME-SPACEGROUP.cif`, for
277
+ With `-symm`, CSV mode reads serialized pymatgen `Structure` dictionaries from a required `cif`
278
+ column. `-csv INPUT.csv -of NAME -symm` writes `INDEX-NAME-SPACEGROUP.cif`, for
270
279
  example `1-Li2YCl3-66.cif`. Every run also writes `symmetry.txt` with one row
271
280
  per successfully generated CIF: filename, crystal system, space-group symbol,
272
281
  and space-group number. Crystal-system and space-group summaries are sorted
@@ -282,7 +291,7 @@ the `_symmetry_equiv_pos_as_xyz` loop is sourced from pymatgen's `SpaceGroup`
282
291
  database instead of serializing noisy numerical translations returned by
283
292
  structure-specific symmetry detection.
284
293
 
285
- Every CIF exported by `symmetrize`, `convert`, `substitute`, `chemsys-entry`,
294
+ Every CIF exported by `convert`, `substitute`, `chemsys-entry`,
286
295
  `supercell`, or `gpumd-group` is written through the same CIF helper. `convert`
287
296
  writes P1 by default and performs symmetry detection only when `-symm` is
288
297
  present; the other commands retain their command-specific symmetry behavior.
@@ -293,7 +302,15 @@ atom-site loop. Multiplicity-letter
293
302
  assignments use the conventional
294
303
  International Tables setting from pymatgen/spglib and correspond to the
295
304
  [Bilbao Crystallographic Server WYCKPOS tables](https://cryst.ehu.es/cryst/get_wp.html);
296
- the provenance URL is also recorded inside each generated CIF.
305
+ the provenance URL is also recorded inside each generated CIF.
306
+
307
+ For structural conversion, supercell creation, and GPUMD grouping, PMK reads
308
+ CIF input with pymatgen rather than ASE. ASE interprets
309
+ `_space_group_crystal_system` as a setting hint only for rhombohedral groups
310
+ and emits a potentially misleading warning for other crystal systems. Using
311
+ pymatgen avoids that ambiguity while PMK continues to use ASE for non-CIF
312
+ formats. A partially occupied CIF must first be ordered with `convert -d2o`
313
+ before it can be represented as an explicit atom list.
297
314
 
298
315
  CIF `_atom_type_symbol` and `_atom_site_label` values use per-element
299
316
  inequivalent-site identifiers such as `Li1`, `Y1`, `Cl1`, and `Cl2`.
@@ -327,30 +344,42 @@ For folder conversion, `convert -if INPUT -o OUTPUT` and
327
344
  `convert -if INPUT -of OUTPUT` are equivalent and write `.xyz` by default. Set
328
345
  another target extension with `-oe`, for example `-oe cif` or `-oe vasp`.
329
346
  Single-file and trajectory modes infer the target format from `-o` and `-ot`.
330
- Because Wyckoff metadata describes one crystal structure, each `.cif` output
331
- file accepts exactly one frame.
332
-
333
- `convert -d2o` orders partially occupied CIF structures. `-supercell` defaults
334
- to `1x1x1`; three diagonal factors or nine integer matrix entries are also
335
- accepted. Folder ordering scans recursively, groups `BASE-dN.cif` files by
336
- `BASE`, and writes only the lowest-total-Ewald candidate into the flat output
337
- folder as `BASE-oN.cif`, where `N` remains tied to its source `-dN` file.
338
- Pymatgen `EnumerateStructureTransformation` is used when enumlib is available;
339
- the expected missing-enumlib runtime error quietly switches to
340
- `OrderDisorderedStructureTransformation`. Every output CIF records total Ewald
341
- energy on line two as `# E_e = VALUE eV`, and `d2o.txt` records all candidates,
342
- methods, energies, failures, and selections. Add `-symm` to import symmetry into
343
- the ordered CIF; otherwise it is P1. Without `-d2o`, `-supercell` only builds an
344
- ordinary structural supercell and never enables ordering automatically.
345
-
346
- After `supercell -temp` creates all requested jobs, it checks generated folders
347
- that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
348
- lines retain their temperature parameters but are rewritten with three
349
- pressure/modulus components for 90-degree orthorhombic cells and all six
350
- components for non-orthorhombic cells, following the
351
- [official GPUMD ensemble syntax](https://gpumd.org/gpumd/input_parameters/ensemble_standard.html).
352
- Other ensemble types are not modified. Use `-group Li Y Cl` to add GPUMD groups.
353
-
347
+ Because Wyckoff metadata describes one crystal structure, each `.cif` output
348
+ file accepts exactly one frame.
349
+
350
+ For ordinary conversion, the three supercell options are mutually exclusive and
351
+ are resolved separately for every input structure. For example, a cell with
352
+ `a=3 A`, `b=4 A`, and `c=5 A` plus `-supercell-close-abc 20` uses `7x5x4`,
353
+ giving approximately `21 A`, `20 A`, and `20 A`. The numeric value is the target:
354
+ `-supercell-close-abc 40` targets approximately `40 A`.
355
+
356
+ `convert -d2o` orders partially occupied CIF structures.
357
+ `-supercell-matrix` defaults internally to `1x1x1` in this mode; three
358
+ diagonal factors or nine integer matrix entries are accepted. Because Ewald
359
+ candidates must be compared using one fixed cell, `-supercell-max-abc` and
360
+ `-supercell-close-abc` are not accepted with `-d2o`. Folder ordering scans
361
+ recursively, groups `BASE-dN.cif` files by `BASE`, and writes only the
362
+ lowest-total-Ewald candidate into the flat output folder as `BASE-oN.cif`,
363
+ where `N` remains tied to its source `-dN` file. Pymatgen
364
+ `EnumerateStructureTransformation` is used when enumlib is available; the
365
+ expected missing-enumlib runtime error quietly switches to
366
+ `OrderDisorderedStructureTransformation`. Every output CIF records total Ewald
367
+ energy on line two as `# E_e = VALUE eV`, and `d2o.txt` records all candidates,
368
+ methods, energies, failures, and selections. Add `-symm` to import symmetry into
369
+ the ordered CIF; otherwise it is P1. Without `-d2o`, the
370
+ `-supercell-matrix`, `-supercell-max-abc`, and `-supercell-close-abc` options
371
+ build an ordinary structural supercell and never enable ordering automatically.
372
+
373
+ After `supercell -temp` creates all requested jobs, it checks generated folders
374
+ that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
375
+ lines retain their temperature parameters but are rewritten with three
376
+ pressure/modulus components for 90-degree orthorhombic cells and all six
377
+ components for non-orthorhombic cells, following the
378
+ [official GPUMD ensemble syntax](https://gpumd.org/gpumd/input_parameters/ensemble_standard.html).
379
+ Other ensemble types are not modified. Use `-gpumd-group Li Y Cl` to embed the
380
+ standalone `gpumd-group` tagging behavior in generated `model.xyz` files. The
381
+ same option also tags ordinary `.xyz` or `.extxyz` supercell output; formats
382
+ that cannot preserve GPUMD group arrays are rejected before output is written.
354
383
  `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.
355
384
 
356
385
  GPUMD box matrices are interpreted row by row by
@@ -433,7 +462,7 @@ pymdkit/
433
462
  |-- vasp_relax.py
434
463
  |-- vasp_static.py
435
464
  |-- ...
436
- `-- symmetrize.py
465
+ `-- _symmetry.py # private symmetry conversion/report helper
437
466
  ```
438
467
 
439
468
  Modules whose name starts with `_` are shared helpers and are skipped by the
@@ -471,5 +500,5 @@ fast and a missing optional dependency only affects the one command that needs i
471
500
  Every command module still works directly, which is handy for debugging:
472
501
 
473
502
  ```bash
474
- python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
503
+ python src/pymdkit/commands/supercell.py -i in.cif -close-abc 20 -o sc.vasp
475
504
  ```
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.6.0"
7
+ version = "1.6.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"
@@ -1,3 +1,3 @@
1
1
  """pymdkit -- a unified CLI for atomistic / MD structure workflows."""
2
2
 
3
- __version__ = "1.6.0"
3
+ __version__ = "1.6.2"
@@ -424,13 +424,22 @@ def write_cif(
424
424
  data_name=None,
425
425
  refine=True,
426
426
  symmetrize=True,
427
- standardized=False,
427
+ standardized=False,
428
+ add_oxidation=True,
428
429
  ):
429
430
  """Write a CIF and return ``(symbol, number)``."""
430
431
  from pymatgen.io.cif import CifWriter
431
432
  from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
432
433
 
433
- structure = _oxidation.decorate_structure(as_pymatgen_structure(structure))
434
+ structure = as_pymatgen_structure(structure)
435
+ if add_oxidation:
436
+ structure = _oxidation.decorate_structure(structure)
437
+ else:
438
+ structure = structure.copy()
439
+ try:
440
+ structure.remove_oxidation_states()
441
+ except (AttributeError, ValueError):
442
+ pass
434
443
  if symmetrize:
435
444
  analyzer = SpacegroupAnalyzer(
436
445
  structure,
@@ -442,7 +451,8 @@ def write_cif(
442
451
  structure, symprec, angle_tolerance
443
452
  )
444
453
  standardized = True
445
- structure = _oxidation.decorate_structure(structure)
454
+ if add_oxidation:
455
+ structure = _oxidation.decorate_structure(structure)
446
456
  symbol = analyzer.get_space_group_symbol()
447
457
  number = analyzer.get_space_group_number()
448
458
  crystal_system = analyzer.get_crystal_system()
@@ -460,7 +470,10 @@ def write_cif(
460
470
  cif_file = getattr(writer, "cif_file", None) or writer._cf
461
471
  block = _add_p1_wyckoff_column(cif_file)
462
472
 
463
- structure_oxidations = _oxidation.uniform_oxidation_states(structure)
473
+ structure_oxidations = (
474
+ _oxidation.uniform_oxidation_states(structure)
475
+ if add_oxidation else {}
476
+ )
464
477
  _set_crystal_system(block, crystal_system)
465
478
  _relabel_atom_types(block, structure_oxidations)
466
479
  _format_cif_numbers(block)
@@ -0,0 +1,136 @@
1
+ """Shared supercell sizing and application helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ import re
7
+
8
+
9
+ IDENTITY_MATRIX = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
10
+
11
+
12
+ def matrix_determinant(matrix):
13
+ return (
14
+ matrix[0][0] * (matrix[1][1] * matrix[2][2]
15
+ - matrix[1][2] * matrix[2][1])
16
+ - matrix[0][1] * (matrix[1][0] * matrix[2][2]
17
+ - matrix[1][2] * matrix[2][0])
18
+ + matrix[0][2] * (matrix[1][0] * matrix[2][1]
19
+ - matrix[1][1] * matrix[2][0])
20
+ )
21
+
22
+
23
+ def parse_matrix(values, option_name="-matrix"):
24
+ """Parse diagonal AxBxC or a full nine-integer matrix."""
25
+ if values is None:
26
+ return [row[:] for row in IDENTITY_MATRIX]
27
+ if isinstance(values, str):
28
+ values = [values]
29
+ tokens = []
30
+ for value in values:
31
+ tokens.extend(
32
+ token for token in re.split(r"[ ,xX]+", str(value)) if token
33
+ )
34
+ try:
35
+ numbers = [int(token) for token in tokens]
36
+ except ValueError as exc:
37
+ raise ValueError(f"{option_name} values must be integers") from exc
38
+ if len(numbers) == 3:
39
+ if any(value <= 0 for value in numbers):
40
+ raise ValueError(
41
+ f"diagonal {option_name} factors must be positive"
42
+ )
43
+ return [
44
+ [numbers[0], 0, 0],
45
+ [0, numbers[1], 0],
46
+ [0, 0, numbers[2]],
47
+ ]
48
+ if len(numbers) == 9:
49
+ matrix = [numbers[0:3], numbers[3:6], numbers[6:9]]
50
+ if matrix_determinant(matrix) == 0:
51
+ raise ValueError(f"{option_name} must be nonsingular")
52
+ return matrix
53
+ raise ValueError(
54
+ f"{option_name} accepts 1x1x1, three diagonal factors, "
55
+ "or nine matrix entries"
56
+ )
57
+
58
+
59
+ def is_identity(matrix):
60
+ return matrix == IDENTITY_MATRIX
61
+
62
+
63
+ def diagonal_factors(matrix):
64
+ """Return positive diagonal factors, or None for a general matrix."""
65
+ if any(
66
+ matrix[row][column] != 0
67
+ for row in range(3)
68
+ for column in range(3)
69
+ if row != column
70
+ ):
71
+ return None
72
+ factors = [matrix[index][index] for index in range(3)]
73
+ return factors if all(value > 0 for value in factors) else None
74
+
75
+
76
+ def max_abc_matrix(lengths, maximum):
77
+ """Largest diagonal repeats whose resulting lengths do not exceed L."""
78
+ maximum = float(maximum)
79
+ if maximum <= 0:
80
+ raise ValueError("maximum cell length must be greater than zero")
81
+ factors = [
82
+ max(1, int(maximum // float(length))) if float(length) > 0 else 1
83
+ for length in lengths
84
+ ]
85
+ return parse_matrix(factors)
86
+
87
+
88
+ def close_abc_matrix(lengths, target):
89
+ """Diagonal repeats that place each cell length nearest a target length."""
90
+ target = float(target)
91
+ if target <= 0:
92
+ raise ValueError("target cell length must be greater than zero")
93
+ factors = [
94
+ max(1, int(math.floor(target / float(length) + 0.5)))
95
+ if float(length) > 0 else 1
96
+ for length in lengths
97
+ ]
98
+ return parse_matrix(factors)
99
+
100
+
101
+ def resolve_matrix(
102
+ lengths,
103
+ *,
104
+ matrix=None,
105
+ max_abc=None,
106
+ close_abc=None,
107
+ matrix_option="-matrix",
108
+ ):
109
+ """Resolve one mutually exclusive sizing mode to a 3x3 integer matrix."""
110
+ selected = sum(value is not None for value in (matrix, max_abc, close_abc))
111
+ if selected > 1:
112
+ raise ValueError("choose only one supercell sizing option")
113
+ if matrix is not None:
114
+ return parse_matrix(matrix, option_name=matrix_option)
115
+ if max_abc is not None:
116
+ return max_abc_matrix(lengths, max_abc)
117
+ if close_abc is not None:
118
+ return close_abc_matrix(lengths, close_abc)
119
+ return [row[:] for row in IDENTITY_MATRIX]
120
+
121
+
122
+ def apply_to_atoms(atoms, matrix):
123
+ """Apply a diagonal or general integer supercell matrix to ASE Atoms."""
124
+ factors = diagonal_factors(matrix)
125
+ if factors is not None:
126
+ return atoms.repeat(factors)
127
+ from ase.build import make_supercell
128
+
129
+ return make_supercell(atoms, matrix)
130
+
131
+
132
+ def matrix_text(matrix):
133
+ factors = diagonal_factors(matrix)
134
+ if factors is not None:
135
+ return "x".join(str(value) for value in factors)
136
+ return " ".join(str(value) for row in matrix for value in row)