pymdkit 1.5.2__tar.gz → 1.5.4__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.
- {pymdkit-1.5.2 → pymdkit-1.5.4}/PKG-INFO +24 -5
- {pymdkit-1.5.2 → pymdkit-1.5.4}/README.md +24 -5
- {pymdkit-1.5.2 → pymdkit-1.5.4}/pyproject.toml +1 -1
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/__init__.py +1 -1
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_cifio.py +1 -1
- pymdkit-1.5.4/src/pymdkit/commands/convert.py +488 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gpumd_msd.py +45 -20
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/realloc_occ.py +93 -10
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit.egg-info/PKG-INFO +24 -5
- pymdkit-1.5.2/src/pymdkit/commands/convert.py +0 -186
- {pymdkit-1.5.2 → pymdkit-1.5.4}/LICENSE +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/setup.cfg +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_gpumd.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_precision.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gather_fs.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gpumd_relax.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gpumd_thermo.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit.egg-info/SOURCES.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.4}/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.
|
|
3
|
+
Version: 1.5.4
|
|
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
|
|
@@ -110,6 +110,7 @@ pmk gpumd-group -elements Li Y Cl # scan subfolders
|
|
|
110
110
|
pmk convert -i opted.vasp -o opted.xyz # convert one structure
|
|
111
111
|
pmk convert -if vasp-opted -of cif-opted -oe cif # folder conversion target format
|
|
112
112
|
pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
|
|
113
|
+
pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -sc-matrix 1x1x1
|
|
113
114
|
pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
|
|
114
115
|
pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
|
|
115
116
|
pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
|
|
@@ -135,6 +136,7 @@ pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
|
135
136
|
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
136
137
|
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
|
|
137
138
|
pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
|
|
139
|
+
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6 -occ 0 0.125 0.25 0.375 0.5 0.625 0.75 0.875 1
|
|
138
140
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
139
141
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
140
142
|
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
@@ -211,8 +213,12 @@ followed by three directional SDC columns. PMK takes the arithmetic mean over
|
|
|
211
213
|
the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
|
|
212
214
|
multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
|
|
213
215
|
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
|
|
215
|
-
|
|
216
|
+
and a `_results.txt`. The root receives a compact `sigma-sdc-summary.txt` with
|
|
217
|
+
one fixed-width, right-aligned row per job folder. Conductivities use two
|
|
218
|
+
decimal places; diffusion coefficients use compact scientific notation with
|
|
219
|
+
two decimal places. If the
|
|
220
|
+
mobile ion occupies multiple groups, conductivities are summed and diffusion
|
|
221
|
+
coefficients are particle-count-weighted.
|
|
216
222
|
|
|
217
223
|
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
218
224
|
of the final SDC-producing MD run and the official 18-column
|
|
@@ -231,7 +237,9 @@ oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
|
231
237
|
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
232
238
|
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
233
239
|
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
234
|
-
`-formula` options.
|
|
240
|
+
`-formula` options. Add `-occ` followed by the only permitted occupancy values;
|
|
241
|
+
three decimal places are supported, so grids containing `0.125`, `0.375`,
|
|
242
|
+
`0.625`, and `0.875` are preserved exactly in generated CIF files.
|
|
235
243
|
|
|
236
244
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
237
245
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -298,7 +306,7 @@ and `Cl`. Oxidation numbers remain in the separate
|
|
|
298
306
|
|
|
299
307
|
Newly serialized floating-point values use eight digits after the decimal
|
|
300
308
|
point across PMK structure, trajectory, data, report, and screen output. CIF
|
|
301
|
-
`_atom_site_occupancy` values use
|
|
309
|
+
`_atom_site_occupancy` values use three digits and
|
|
302
310
|
`_atom_type_oxidation_number` values use one digit. In extended XYZ, zero components
|
|
303
311
|
inside the second-line `Lattice="..."` value remain `0.0`; other generated
|
|
304
312
|
floating-point values use eight digits. Commands that select or concatenate
|
|
@@ -318,6 +326,17 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
|
318
326
|
Because Wyckoff metadata describes one crystal structure, each `.cif` output
|
|
319
327
|
file accepts exactly one frame.
|
|
320
328
|
|
|
329
|
+
`convert -d2o` orders partially occupied CIF structures. `-sc-matrix 1x1x1`
|
|
330
|
+
means a 1x1x1 cell; three diagonal factors or nine integer matrix entries are
|
|
331
|
+
also accepted. Folder ordering scans recursively and mirrors input subfolders.
|
|
332
|
+
It uses pymatgen `EnumerateStructureTransformation` with one worker to remove
|
|
333
|
+
symmetry-equivalent orderings and select the lowest Ewald energy, then falls
|
|
334
|
+
back to bounded `OrderDisorderedStructureTransformation` when enumlib is not
|
|
335
|
+
available. Equivalent results from separate disorder files are merged and
|
|
336
|
+
written as `BASE-o1.cif`, `BASE-o2.cif`, and so on. For convenience, supplying
|
|
337
|
+
`-sc-matrix` without `-d2o` automatically enables ordering when a disordered
|
|
338
|
+
CIF is detected; ordered inputs instead receive an ordinary supercell.
|
|
339
|
+
|
|
321
340
|
After `supercell -temp` creates all requested jobs, it checks generated folders
|
|
322
341
|
that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
|
|
323
342
|
lines retain their temperature parameters but are rewritten with three
|
|
@@ -83,6 +83,7 @@ pmk gpumd-group -elements Li Y Cl # scan subfolders
|
|
|
83
83
|
pmk convert -i opted.vasp -o opted.xyz # convert one structure
|
|
84
84
|
pmk convert -if vasp-opted -of cif-opted -oe cif # folder conversion target format
|
|
85
85
|
pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
|
|
86
|
+
pmk convert -if Li4YCl6-disorder -of Li4YCl6-order -d2o -sc-matrix 1x1x1
|
|
86
87
|
pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
|
|
87
88
|
pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
|
|
88
89
|
pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -group Li Y Cl
|
|
@@ -107,7 +108,8 @@ pmk vasp2xyz -position-only # write positions only, without e
|
|
|
107
108
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
108
109
|
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
109
110
|
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
|
|
110
|
-
pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
|
|
111
|
+
pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
|
|
112
|
+
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6 -occ 0 0.125 0.25 0.375 0.5 0.625 0.75 0.875 1
|
|
111
113
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
112
114
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
113
115
|
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
@@ -184,8 +186,12 @@ followed by three directional SDC columns. PMK takes the arithmetic mean over
|
|
|
184
186
|
the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
|
|
185
187
|
multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
|
|
186
188
|
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
|
|
188
|
-
|
|
189
|
+
and a `_results.txt`. The root receives a compact `sigma-sdc-summary.txt` with
|
|
190
|
+
one fixed-width, right-aligned row per job folder. Conductivities use two
|
|
191
|
+
decimal places; diffusion coefficients use compact scientific notation with
|
|
192
|
+
two decimal places. If the
|
|
193
|
+
mobile ion occupies multiple groups, conductivities are summed and diffusion
|
|
194
|
+
coefficients are particle-count-weighted.
|
|
189
195
|
|
|
190
196
|
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
191
197
|
of the final SDC-producing MD run and the official 18-column
|
|
@@ -204,7 +210,9 @@ oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
|
204
210
|
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
205
211
|
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
206
212
|
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
207
|
-
`-formula` options.
|
|
213
|
+
`-formula` options. Add `-occ` followed by the only permitted occupancy values;
|
|
214
|
+
three decimal places are supported, so grids containing `0.125`, `0.375`,
|
|
215
|
+
`0.625`, and `0.875` are preserved exactly in generated CIF files.
|
|
208
216
|
|
|
209
217
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
210
218
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -271,7 +279,7 @@ and `Cl`. Oxidation numbers remain in the separate
|
|
|
271
279
|
|
|
272
280
|
Newly serialized floating-point values use eight digits after the decimal
|
|
273
281
|
point across PMK structure, trajectory, data, report, and screen output. CIF
|
|
274
|
-
`_atom_site_occupancy` values use
|
|
282
|
+
`_atom_site_occupancy` values use three digits and
|
|
275
283
|
`_atom_type_oxidation_number` values use one digit. In extended XYZ, zero components
|
|
276
284
|
inside the second-line `Lattice="..."` value remain `0.0`; other generated
|
|
277
285
|
floating-point values use eight digits. Commands that select or concatenate
|
|
@@ -291,6 +299,17 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
|
291
299
|
Because Wyckoff metadata describes one crystal structure, each `.cif` output
|
|
292
300
|
file accepts exactly one frame.
|
|
293
301
|
|
|
302
|
+
`convert -d2o` orders partially occupied CIF structures. `-sc-matrix 1x1x1`
|
|
303
|
+
means a 1x1x1 cell; three diagonal factors or nine integer matrix entries are
|
|
304
|
+
also accepted. Folder ordering scans recursively and mirrors input subfolders.
|
|
305
|
+
It uses pymatgen `EnumerateStructureTransformation` with one worker to remove
|
|
306
|
+
symmetry-equivalent orderings and select the lowest Ewald energy, then falls
|
|
307
|
+
back to bounded `OrderDisorderedStructureTransformation` when enumlib is not
|
|
308
|
+
available. Equivalent results from separate disorder files are merged and
|
|
309
|
+
written as `BASE-o1.cif`, `BASE-o2.cif`, and so on. For convenience, supplying
|
|
310
|
+
`-sc-matrix` without `-d2o` automatically enables ordering when a disordered
|
|
311
|
+
CIF is detected; ordered inputs instead receive an ordinary supercell.
|
|
312
|
+
|
|
294
313
|
After `supercell -temp` creates all requested jobs, it checks generated folders
|
|
295
314
|
that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
|
|
296
315
|
lines retain their temperature parameters but are rewritten with three
|
|
@@ -198,7 +198,7 @@ def _format_cif_scalar(value, precision, force=False):
|
|
|
198
198
|
def _format_cif_numbers(block):
|
|
199
199
|
for tag, value in list(block.data.items()):
|
|
200
200
|
if tag == OCCUPANCY_TAG:
|
|
201
|
-
precision =
|
|
201
|
+
precision = 3
|
|
202
202
|
elif tag == OXIDATION_TAG:
|
|
203
203
|
precision = 1
|
|
204
204
|
else:
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
"""Convert structures and trajectories, including disorder-to-order workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
from collections import defaultdict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from . import _cifio, _fileio, _precision
|
|
12
|
+
except ImportError:
|
|
13
|
+
import _cifio
|
|
14
|
+
import _fileio
|
|
15
|
+
import _precision
|
|
16
|
+
|
|
17
|
+
COMMAND = "convert"
|
|
18
|
+
HELP = "Convert structures/trajectories and order disordered structures."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read(path, all_frames=False):
|
|
22
|
+
from ase.io import read
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
return read(str(path), index=":" if all_frames else -1)
|
|
26
|
+
except Exception as ase_error:
|
|
27
|
+
if all_frames:
|
|
28
|
+
raise ValueError(
|
|
29
|
+
f"ASE could not read trajectory {path}: {ase_error}"
|
|
30
|
+
) from ase_error
|
|
31
|
+
try:
|
|
32
|
+
from pymatgen.core import Molecule, Structure
|
|
33
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
obj = Structure.from_file(str(path))
|
|
37
|
+
except Exception:
|
|
38
|
+
obj = Molecule.from_file(str(path))
|
|
39
|
+
return AseAtomsAdaptor.get_atoms(obj)
|
|
40
|
+
except Exception as pmg_error:
|
|
41
|
+
raise ValueError(
|
|
42
|
+
f"neither ASE nor pymatgen could read {path}: "
|
|
43
|
+
f"ASE={ase_error}; pymatgen={pmg_error}"
|
|
44
|
+
) from pmg_error
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _ase_format(path):
|
|
48
|
+
suffix = Path(path).suffix.lower()
|
|
49
|
+
if suffix in {".xyz", ".extxyz"}:
|
|
50
|
+
return "extxyz"
|
|
51
|
+
if suffix == ".vasp":
|
|
52
|
+
return "vasp"
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _normalize_output(path):
|
|
57
|
+
suffix = Path(path).suffix.lower()
|
|
58
|
+
if suffix in {".xyz", ".extxyz"}:
|
|
59
|
+
_precision.normalize_extxyz(path)
|
|
60
|
+
elif suffix == ".vasp":
|
|
61
|
+
_precision.normalize_vasp(path)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _write(path, atoms):
|
|
65
|
+
path = Path(path)
|
|
66
|
+
if _cifio.is_cif(path):
|
|
67
|
+
if isinstance(atoms, (list, tuple)) and len(atoms) != 1:
|
|
68
|
+
raise ValueError("CIF output supports exactly one structure per file")
|
|
69
|
+
one = atoms[0] if isinstance(atoms, (list, tuple)) else atoms
|
|
70
|
+
_cifio.write_cif(one, path, data_name=path.stem)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
from ase.io import write
|
|
74
|
+
|
|
75
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
76
|
+
kwargs = {}
|
|
77
|
+
output_format = _ase_format(path)
|
|
78
|
+
if output_format:
|
|
79
|
+
kwargs["format"] = output_format
|
|
80
|
+
if output_format == "vasp":
|
|
81
|
+
kwargs["direct"] = True
|
|
82
|
+
try:
|
|
83
|
+
write(str(path), atoms, **kwargs)
|
|
84
|
+
_normalize_output(path)
|
|
85
|
+
return
|
|
86
|
+
except Exception as ase_error:
|
|
87
|
+
if isinstance(atoms, (list, tuple)) and len(atoms) != 1:
|
|
88
|
+
raise ValueError(
|
|
89
|
+
f"ASE cannot write this multi-frame trajectory to {path}: "
|
|
90
|
+
f"{ase_error}"
|
|
91
|
+
) from ase_error
|
|
92
|
+
one = atoms[0] if isinstance(atoms, (list, tuple)) else atoms
|
|
93
|
+
try:
|
|
94
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
95
|
+
|
|
96
|
+
if one.pbc.any():
|
|
97
|
+
obj = AseAtomsAdaptor.get_structure(one)
|
|
98
|
+
else:
|
|
99
|
+
obj = AseAtomsAdaptor.get_molecule(one)
|
|
100
|
+
obj.to(filename=str(path))
|
|
101
|
+
_normalize_output(path)
|
|
102
|
+
except Exception as pmg_error:
|
|
103
|
+
raise ValueError(
|
|
104
|
+
f"neither ASE nor pymatgen could write {path}: "
|
|
105
|
+
f"ASE={ase_error}; pymatgen={pmg_error}"
|
|
106
|
+
) from pmg_error
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _read_structure(path):
|
|
110
|
+
from pymatgen.core import Structure
|
|
111
|
+
|
|
112
|
+
return Structure.from_file(str(path))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _write_structure(path, structure, refine=True):
|
|
116
|
+
path = Path(path)
|
|
117
|
+
if _cifio.is_cif(path):
|
|
118
|
+
_cifio.write_cif(
|
|
119
|
+
structure, path, data_name=path.stem, refine=refine
|
|
120
|
+
)
|
|
121
|
+
return
|
|
122
|
+
from pymatgen.io.ase import AseAtomsAdaptor
|
|
123
|
+
|
|
124
|
+
_write(path, AseAtomsAdaptor.get_atoms(structure))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _matrix_determinant(matrix):
|
|
128
|
+
return (
|
|
129
|
+
matrix[0][0] * (matrix[1][1] * matrix[2][2]
|
|
130
|
+
- matrix[1][2] * matrix[2][1])
|
|
131
|
+
- matrix[0][1] * (matrix[1][0] * matrix[2][2]
|
|
132
|
+
- matrix[1][2] * matrix[2][0])
|
|
133
|
+
+ matrix[0][2] * (matrix[1][0] * matrix[2][1]
|
|
134
|
+
- matrix[1][1] * matrix[2][0])
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def parse_supercell_matrix(values):
|
|
139
|
+
if not values:
|
|
140
|
+
return None
|
|
141
|
+
tokens = []
|
|
142
|
+
for value in values:
|
|
143
|
+
tokens.extend(token for token in re.split(r"[ ,xX]+", str(value)) if token)
|
|
144
|
+
try:
|
|
145
|
+
numbers = [int(token) for token in tokens]
|
|
146
|
+
except ValueError as exc:
|
|
147
|
+
raise ValueError("-sc-matrix values must be integers") from exc
|
|
148
|
+
if len(numbers) == 3:
|
|
149
|
+
if any(value <= 0 for value in numbers):
|
|
150
|
+
raise ValueError("diagonal -sc-matrix factors must be positive")
|
|
151
|
+
return [[numbers[0], 0, 0], [0, numbers[1], 0], [0, 0, numbers[2]]]
|
|
152
|
+
if len(numbers) == 9:
|
|
153
|
+
matrix = [numbers[0:3], numbers[3:6], numbers[6:9]]
|
|
154
|
+
if _matrix_determinant(matrix) == 0:
|
|
155
|
+
raise ValueError("-sc-matrix must be nonsingular")
|
|
156
|
+
return matrix
|
|
157
|
+
raise ValueError(
|
|
158
|
+
"-sc-matrix accepts 1x1x1, three diagonal factors, or nine matrix entries"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def convert_one(input_path, output_path, trajectory=False, supercell_matrix=None):
|
|
163
|
+
if supercell_matrix is not None:
|
|
164
|
+
if trajectory:
|
|
165
|
+
raise ValueError("-sc-matrix is not supported with trajectory mode")
|
|
166
|
+
structure = _read_structure(input_path)
|
|
167
|
+
structure.make_supercell(supercell_matrix)
|
|
168
|
+
_write_structure(output_path, structure)
|
|
169
|
+
else:
|
|
170
|
+
atoms = _read(input_path, all_frames=trajectory)
|
|
171
|
+
_write(output_path, atoms)
|
|
172
|
+
count = len(atoms) if trajectory and supercell_matrix is None else 1
|
|
173
|
+
label = f" ({count} frame(s))" if trajectory else ""
|
|
174
|
+
print(f"{input_path} -> {output_path}{label}")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _recursive_structure_files(folder, cif_only=False):
|
|
178
|
+
folder = Path(folder)
|
|
179
|
+
if not folder.is_dir():
|
|
180
|
+
raise SystemExit(f"Error: input folder not found: {folder}")
|
|
181
|
+
files = [
|
|
182
|
+
path for path in folder.rglob("*")
|
|
183
|
+
if _fileio.is_structure(path)
|
|
184
|
+
and (not cif_only or path.suffix.lower() == ".cif")
|
|
185
|
+
]
|
|
186
|
+
files.sort(key=lambda path: path.relative_to(folder).as_posix())
|
|
187
|
+
if not files:
|
|
188
|
+
kind = "CIF" if cif_only else "structure"
|
|
189
|
+
raise SystemExit(f"Error: no {kind} files found recursively in {folder}")
|
|
190
|
+
return files
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _add_oxidation_states(structure, source):
|
|
194
|
+
if all(hasattr(specie, "oxi_state")
|
|
195
|
+
for site in structure for specie in site.species):
|
|
196
|
+
return structure
|
|
197
|
+
if Path(source).suffix.lower() != ".cif":
|
|
198
|
+
raise ValueError("Ewald ordering requires oxidation-state-decorated input")
|
|
199
|
+
try:
|
|
200
|
+
from .realloc_occ import parse_cif
|
|
201
|
+
except ImportError:
|
|
202
|
+
from realloc_occ import parse_cif
|
|
203
|
+
|
|
204
|
+
parsed = parse_cif(source)
|
|
205
|
+
oxidation = {}
|
|
206
|
+
for site in parsed.sites:
|
|
207
|
+
previous = oxidation.setdefault(site.element, site.oxidation)
|
|
208
|
+
if abs(previous - site.oxidation) > 1.0e-8:
|
|
209
|
+
raise ValueError(
|
|
210
|
+
f"{site.element} has multiple oxidation states; "
|
|
211
|
+
"site-specific Ewald decoration is ambiguous"
|
|
212
|
+
)
|
|
213
|
+
structure.add_oxidation_state_by_element(oxidation)
|
|
214
|
+
return structure
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _ranked_structure(result):
|
|
218
|
+
if isinstance(result, list):
|
|
219
|
+
if not result:
|
|
220
|
+
raise ValueError("ordering transformation returned no structures")
|
|
221
|
+
result = result[0]
|
|
222
|
+
return result["structure"] if isinstance(result, dict) else result
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _ewald_energy_per_atom(structure):
|
|
226
|
+
from pymatgen.analysis.ewald import EwaldSummation
|
|
227
|
+
|
|
228
|
+
return float(EwaldSummation(structure).total_energy) / len(structure)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _order_source(source, supercell_matrix):
|
|
232
|
+
structure = _read_structure(source)
|
|
233
|
+
if structure.is_ordered:
|
|
234
|
+
return None
|
|
235
|
+
structure = _add_oxidation_states(structure, source)
|
|
236
|
+
structure.make_supercell(supercell_matrix)
|
|
237
|
+
try:
|
|
238
|
+
from pymatgen.transformations.advanced_transformations import (
|
|
239
|
+
EnumerateStructureTransformation,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
transformation = EnumerateStructureTransformation(
|
|
243
|
+
min_cell_size=1,
|
|
244
|
+
max_cell_size=1,
|
|
245
|
+
symm_prec=0.1,
|
|
246
|
+
refine_structure=False,
|
|
247
|
+
sort_criteria="ewald",
|
|
248
|
+
n_jobs=1,
|
|
249
|
+
)
|
|
250
|
+
result = transformation.apply_transformation(
|
|
251
|
+
structure, return_ranked_list=1
|
|
252
|
+
)
|
|
253
|
+
method = "EnumerateStructureTransformation"
|
|
254
|
+
except Exception as enum_error:
|
|
255
|
+
from pymatgen.transformations.standard_transformations import (
|
|
256
|
+
OrderDisorderedStructureTransformation,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
transformation = OrderDisorderedStructureTransformation(
|
|
260
|
+
algo=OrderDisorderedStructureTransformation.ALGO_BEST_FIRST
|
|
261
|
+
)
|
|
262
|
+
result = transformation.apply_transformation(
|
|
263
|
+
structure, return_ranked_list=1
|
|
264
|
+
)
|
|
265
|
+
method = (
|
|
266
|
+
"OrderDisorderedStructureTransformation fallback "
|
|
267
|
+
f"({type(enum_error).__name__}: {enum_error})"
|
|
268
|
+
)
|
|
269
|
+
ordered = _ranked_structure(result)
|
|
270
|
+
if not ordered.is_ordered:
|
|
271
|
+
raise ValueError("ordering transformation returned a disordered structure")
|
|
272
|
+
return ordered, _ewald_energy_per_atom(ordered), method
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _deduplicate_orderings(candidates):
|
|
276
|
+
from pymatgen.analysis.structure_matcher import StructureMatcher
|
|
277
|
+
|
|
278
|
+
matcher = StructureMatcher()
|
|
279
|
+
unique = []
|
|
280
|
+
for candidate in sorted(candidates, key=lambda item: item[1]):
|
|
281
|
+
structure = candidate[0]
|
|
282
|
+
if any(matcher.fit(structure, kept[0]) for kept in unique):
|
|
283
|
+
continue
|
|
284
|
+
unique.append(candidate)
|
|
285
|
+
return unique
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _ordering_base(path):
|
|
289
|
+
return re.sub(r"-d\d+$", "", Path(path).stem, flags=re.IGNORECASE)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _folder_contains_disorder(folder):
|
|
293
|
+
folder = Path(folder)
|
|
294
|
+
if not folder.is_dir():
|
|
295
|
+
raise SystemExit(f"Error: input folder not found: {folder}")
|
|
296
|
+
for source in sorted(folder.rglob("*.cif")):
|
|
297
|
+
try:
|
|
298
|
+
if not _read_structure(source).is_ordered:
|
|
299
|
+
return True
|
|
300
|
+
except Exception:
|
|
301
|
+
continue
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _single_contains_disorder(source):
|
|
306
|
+
return Path(source).suffix.lower() == ".cif" and not _read_structure(source).is_ordered
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def order_single(source, output, supercell_matrix):
|
|
310
|
+
candidate = _order_source(Path(source), supercell_matrix)
|
|
311
|
+
if candidate is None:
|
|
312
|
+
raise ValueError(f"input structure is already ordered: {source}")
|
|
313
|
+
structure, energy, method = candidate
|
|
314
|
+
output = Path(output)
|
|
315
|
+
if output.suffix.lower() != ".cif":
|
|
316
|
+
raise ValueError("disorder-to-order output must be a .cif file")
|
|
317
|
+
_write_structure(output, structure, refine=False)
|
|
318
|
+
print(f"{source} -> {output} (Ewald={energy:.8f} eV/atom; {method})")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def order_folder(input_folder, output_folder, supercell_matrix):
|
|
322
|
+
input_folder = Path(input_folder)
|
|
323
|
+
output_folder = Path(output_folder)
|
|
324
|
+
groups = defaultdict(list)
|
|
325
|
+
for source in _recursive_structure_files(input_folder, cif_only=True):
|
|
326
|
+
relative_parent = source.parent.relative_to(input_folder)
|
|
327
|
+
groups[(relative_parent, _ordering_base(source))].append(source)
|
|
328
|
+
|
|
329
|
+
written, processed, failed = 0, 0, 0
|
|
330
|
+
for (relative_parent, base), sources in sorted(
|
|
331
|
+
groups.items(), key=lambda item: (item[0][0].as_posix(), item[0][1])):
|
|
332
|
+
candidates = []
|
|
333
|
+
for source in sources:
|
|
334
|
+
try:
|
|
335
|
+
candidate = _order_source(source, supercell_matrix)
|
|
336
|
+
if candidate is None:
|
|
337
|
+
print(f"{source}: already ordered -> skipped")
|
|
338
|
+
continue
|
|
339
|
+
candidates.append((*candidate, source))
|
|
340
|
+
processed += 1
|
|
341
|
+
except Exception as exc:
|
|
342
|
+
failed += 1
|
|
343
|
+
print(f"{source}: ordering failed ({exc})")
|
|
344
|
+
destination = output_folder / relative_parent
|
|
345
|
+
destination.mkdir(parents=True, exist_ok=True)
|
|
346
|
+
for number, candidate in enumerate(
|
|
347
|
+
_deduplicate_orderings(candidates), start=1):
|
|
348
|
+
structure, energy, method, source = candidate
|
|
349
|
+
output = destination / f"{base}-o{number}.cif"
|
|
350
|
+
_write_structure(output, structure, refine=False)
|
|
351
|
+
print(
|
|
352
|
+
f"{source} -> {output} "
|
|
353
|
+
f"(Ewald={energy:.8f} eV/atom; {method})"
|
|
354
|
+
)
|
|
355
|
+
written += 1
|
|
356
|
+
print(
|
|
357
|
+
f"Done: ordered {processed} disordered CIF(s), wrote {written} "
|
|
358
|
+
f"symmetry-distinct lowest-energy CIF(s) to {output_folder}"
|
|
359
|
+
)
|
|
360
|
+
return 0 if processed and written and not failed else 1
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
364
|
+
parser.add_argument("-i", "-input", dest="input",
|
|
365
|
+
help="Single input structure.")
|
|
366
|
+
parser.add_argument("-o", "-output", dest="output",
|
|
367
|
+
help="Single output structure; with -if, output folder.")
|
|
368
|
+
parser.add_argument("-it", "-input-trajectory", dest="input_trajectory",
|
|
369
|
+
help="Input trajectory containing multiple structures.")
|
|
370
|
+
parser.add_argument("-ot", "-output-trajectory", dest="output_trajectory",
|
|
371
|
+
help="Output trajectory.")
|
|
372
|
+
parser.add_argument("-if", "-input-folder", dest="input_folder",
|
|
373
|
+
help="Folder of input structures.")
|
|
374
|
+
parser.add_argument("-of", "-output-folder", dest="output_folder",
|
|
375
|
+
help="Output folder for batch conversion.")
|
|
376
|
+
parser.add_argument("-oe", "-output-extension", dest="output_extension",
|
|
377
|
+
default="xyz",
|
|
378
|
+
help="Batch/scan output extension (default: xyz).")
|
|
379
|
+
parser.add_argument(
|
|
380
|
+
"-d2o", action="store_true",
|
|
381
|
+
help="Order partially occupied structures and retain lowest-Ewald results.",
|
|
382
|
+
)
|
|
383
|
+
parser.add_argument(
|
|
384
|
+
"-sc-matrix", nargs="+", metavar="N",
|
|
385
|
+
help="Supercell as 1x1x1, three diagonal factors, or nine matrix entries.",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _extension(value):
|
|
390
|
+
value = str(value).strip()
|
|
391
|
+
return value if value.startswith(".") else "." + value
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def run(args) -> int:
|
|
395
|
+
modes = [bool(args.input), bool(args.input_trajectory), bool(args.input_folder)]
|
|
396
|
+
if sum(modes) > 1:
|
|
397
|
+
raise SystemExit("Error: use only one of -i, -it, or -if.")
|
|
398
|
+
try:
|
|
399
|
+
supercell_matrix = parse_supercell_matrix(args.sc_matrix)
|
|
400
|
+
except ValueError as exc:
|
|
401
|
+
raise SystemExit(f"Error: {exc}") from exc
|
|
402
|
+
if args.input_trajectory and (args.d2o or supercell_matrix is not None):
|
|
403
|
+
raise SystemExit("Error: -d2o and -sc-matrix do not support -it.")
|
|
404
|
+
|
|
405
|
+
ordering_mode = bool(args.d2o)
|
|
406
|
+
if supercell_matrix is not None and not ordering_mode:
|
|
407
|
+
if args.input:
|
|
408
|
+
ordering_mode = _single_contains_disorder(args.input)
|
|
409
|
+
elif args.input_folder:
|
|
410
|
+
ordering_mode = _folder_contains_disorder(args.input_folder)
|
|
411
|
+
if ordering_mode:
|
|
412
|
+
matrix = supercell_matrix or [[1, 0, 0], [0, 1, 0], [0, 0, 1]]
|
|
413
|
+
if args.input:
|
|
414
|
+
if not args.output:
|
|
415
|
+
raise SystemExit("Error: -o is required with -i -d2o.")
|
|
416
|
+
try:
|
|
417
|
+
order_single(args.input, args.output, matrix)
|
|
418
|
+
except Exception as exc:
|
|
419
|
+
raise SystemExit(f"Error: {exc}") from exc
|
|
420
|
+
return 0
|
|
421
|
+
if args.input_folder:
|
|
422
|
+
if args.output and args.output_folder:
|
|
423
|
+
raise SystemExit("Error: use only one of -o or -of with -if.")
|
|
424
|
+
folder_option = args.output_folder or args.output
|
|
425
|
+
output_folder = (
|
|
426
|
+
Path(folder_option) if folder_option
|
|
427
|
+
else Path(args.input_folder).with_name(
|
|
428
|
+
Path(args.input_folder).name + "-order"
|
|
429
|
+
)
|
|
430
|
+
)
|
|
431
|
+
return order_folder(args.input_folder, output_folder, matrix)
|
|
432
|
+
raise SystemExit("Error: -d2o requires -i or -if.")
|
|
433
|
+
|
|
434
|
+
if args.input:
|
|
435
|
+
if not args.output:
|
|
436
|
+
raise SystemExit("Error: -o is required with -i.")
|
|
437
|
+
convert_one(
|
|
438
|
+
args.input, args.output, supercell_matrix=supercell_matrix
|
|
439
|
+
)
|
|
440
|
+
return 0
|
|
441
|
+
|
|
442
|
+
if args.input_trajectory:
|
|
443
|
+
if not args.output_trajectory:
|
|
444
|
+
raise SystemExit("Error: -ot is required with -it.")
|
|
445
|
+
convert_one(args.input_trajectory, args.output_trajectory, trajectory=True)
|
|
446
|
+
return 0
|
|
447
|
+
|
|
448
|
+
extension = _extension(args.output_extension)
|
|
449
|
+
if args.input_folder:
|
|
450
|
+
input_folder = Path(args.input_folder)
|
|
451
|
+
if args.output and args.output_folder:
|
|
452
|
+
raise SystemExit("Error: use only one of -o or -of with -if.")
|
|
453
|
+
folder_option = args.output_folder or args.output
|
|
454
|
+
output_folder = (
|
|
455
|
+
Path(folder_option) if folder_option
|
|
456
|
+
else input_folder.with_name(input_folder.name + "-converted")
|
|
457
|
+
)
|
|
458
|
+
output_folder.mkdir(parents=True, exist_ok=True)
|
|
459
|
+
files = _fileio.structure_files(input_folder)
|
|
460
|
+
for path in files:
|
|
461
|
+
convert_one(
|
|
462
|
+
path,
|
|
463
|
+
output_folder / (path.stem + extension),
|
|
464
|
+
supercell_matrix=supercell_matrix,
|
|
465
|
+
)
|
|
466
|
+
print(f"Done: converted {len(files)} file(s) to {output_folder}")
|
|
467
|
+
return 0
|
|
468
|
+
|
|
469
|
+
if supercell_matrix is not None:
|
|
470
|
+
raise SystemExit("Error: -sc-matrix requires -i or -if.")
|
|
471
|
+
count = 0
|
|
472
|
+
for folder in _fileio.subfolders("."):
|
|
473
|
+
for path in sorted(folder.iterdir()):
|
|
474
|
+
if not _fileio.is_structure(path):
|
|
475
|
+
continue
|
|
476
|
+
if path.suffix.lower() == extension.lower():
|
|
477
|
+
continue
|
|
478
|
+
name = args.output if args.output else path.stem + extension
|
|
479
|
+
convert_one(path, folder / name)
|
|
480
|
+
count += 1
|
|
481
|
+
print(f"Done: converted {count} file(s) across sub-folders.")
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
if __name__ == "__main__":
|
|
486
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
487
|
+
add_arguments(_parser)
|
|
488
|
+
raise SystemExit(run(_parser.parse_args()))
|