pymdkit 1.5.2__tar.gz → 1.5.3__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.3}/PKG-INFO +22 -4
- {pymdkit-1.5.2 → pymdkit-1.5.3}/README.md +22 -4
- {pymdkit-1.5.2 → pymdkit-1.5.3}/pyproject.toml +1 -1
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.5.3/src/pymdkit/commands/convert.py +488 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gpumd_msd.py +23 -16
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/realloc_occ.py +90 -7
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit.egg-info/PKG-INFO +22 -4
- pymdkit-1.5.2/src/pymdkit/commands/convert.py +0 -186
- {pymdkit-1.5.2 → pymdkit-1.5.3}/LICENSE +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/setup.cfg +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_cifio.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_gpumd.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_precision.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gather_fs.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gpumd_relax.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gpumd_thermo.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit.egg-info/SOURCES.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.5.2 → pymdkit-1.5.3}/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.3
|
|
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.25 0.5 0.75 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,11 @@ 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 row per job folder. Conductivities use two decimal places; diffusion
|
|
218
|
+
coefficients use compact scientific notation with two decimal places. If the
|
|
219
|
+
mobile ion occupies multiple groups, conductivities are summed and diffusion
|
|
220
|
+
coefficients are particle-count-weighted.
|
|
216
221
|
|
|
217
222
|
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
218
223
|
of the final SDC-producing MD run and the official 18-column
|
|
@@ -231,7 +236,9 @@ oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
|
231
236
|
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
232
237
|
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
233
238
|
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
234
|
-
`-formula` options.
|
|
239
|
+
`-formula` options. Add `-occ` followed by the only permitted occupancy values;
|
|
240
|
+
for example, `-occ 0 0.25 0.5 0.75 1` also permits the valid allocation
|
|
241
|
+
`Li1=0.75, Li2=0.75`.
|
|
235
242
|
|
|
236
243
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
237
244
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -318,6 +325,17 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
|
318
325
|
Because Wyckoff metadata describes one crystal structure, each `.cif` output
|
|
319
326
|
file accepts exactly one frame.
|
|
320
327
|
|
|
328
|
+
`convert -d2o` orders partially occupied CIF structures. `-sc-matrix 1x1x1`
|
|
329
|
+
means a 1x1x1 cell; three diagonal factors or nine integer matrix entries are
|
|
330
|
+
also accepted. Folder ordering scans recursively and mirrors input subfolders.
|
|
331
|
+
It uses pymatgen `EnumerateStructureTransformation` with one worker to remove
|
|
332
|
+
symmetry-equivalent orderings and select the lowest Ewald energy, then falls
|
|
333
|
+
back to bounded `OrderDisorderedStructureTransformation` when enumlib is not
|
|
334
|
+
available. Equivalent results from separate disorder files are merged and
|
|
335
|
+
written as `BASE-o1.cif`, `BASE-o2.cif`, and so on. For convenience, supplying
|
|
336
|
+
`-sc-matrix` without `-d2o` automatically enables ordering when a disordered
|
|
337
|
+
CIF is detected; ordered inputs instead receive an ordinary supercell.
|
|
338
|
+
|
|
321
339
|
After `supercell -temp` creates all requested jobs, it checks generated folders
|
|
322
340
|
that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
|
|
323
341
|
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.25 0.5 0.75 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,11 @@ 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 row per job folder. Conductivities use two decimal places; diffusion
|
|
191
|
+
coefficients use compact scientific notation with two decimal places. If the
|
|
192
|
+
mobile ion occupies multiple groups, conductivities are summed and diffusion
|
|
193
|
+
coefficients are particle-count-weighted.
|
|
189
194
|
|
|
190
195
|
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
191
196
|
of the final SDC-producing MD run and the official 18-column
|
|
@@ -204,7 +209,9 @@ oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
|
204
209
|
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
205
210
|
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
206
211
|
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
207
|
-
`-formula` options.
|
|
212
|
+
`-formula` options. Add `-occ` followed by the only permitted occupancy values;
|
|
213
|
+
for example, `-occ 0 0.25 0.5 0.75 1` also permits the valid allocation
|
|
214
|
+
`Li1=0.75, Li2=0.75`.
|
|
208
215
|
|
|
209
216
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
210
217
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -291,6 +298,17 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
|
291
298
|
Because Wyckoff metadata describes one crystal structure, each `.cif` output
|
|
292
299
|
file accepts exactly one frame.
|
|
293
300
|
|
|
301
|
+
`convert -d2o` orders partially occupied CIF structures. `-sc-matrix 1x1x1`
|
|
302
|
+
means a 1x1x1 cell; three diagonal factors or nine integer matrix entries are
|
|
303
|
+
also accepted. Folder ordering scans recursively and mirrors input subfolders.
|
|
304
|
+
It uses pymatgen `EnumerateStructureTransformation` with one worker to remove
|
|
305
|
+
symmetry-equivalent orderings and select the lowest Ewald energy, then falls
|
|
306
|
+
back to bounded `OrderDisorderedStructureTransformation` when enumlib is not
|
|
307
|
+
available. Equivalent results from separate disorder files are merged and
|
|
308
|
+
written as `BASE-o1.cif`, `BASE-o2.cif`, and so on. For convenience, supplying
|
|
309
|
+
`-sc-matrix` without `-d2o` automatically enables ordering when a disordered
|
|
310
|
+
CIF is detected; ordered inputs instead receive an ordinary supercell.
|
|
311
|
+
|
|
294
312
|
After `supercell -temp` creates all requested jobs, it checks generated folders
|
|
295
313
|
that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
|
|
296
314
|
lines retain their temperature parameters but are rewritten with three
|
|
@@ -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()))
|
|
@@ -391,32 +391,39 @@ def analyze_job(root, folder, thermo_path, args):
|
|
|
391
391
|
return records
|
|
392
392
|
|
|
393
393
|
|
|
394
|
+
def _compact_scientific(value):
|
|
395
|
+
mantissa, exponent = f"{float(value):.2e}".split("e")
|
|
396
|
+
return f"{mantissa}e{int(exponent)}"
|
|
397
|
+
|
|
398
|
+
|
|
394
399
|
def write_summary(root, records, diffuse_ion):
|
|
395
400
|
columns = [
|
|
396
|
-
"
|
|
397
|
-
"
|
|
398
|
-
"D_x(cm^2/s)", "D_y(cm^2/s)", "D_z(cm^2/s)",
|
|
399
|
-
"sigma_x(mS/cm)", "sigma_y(mS/cm)", "sigma_z(mS/cm)",
|
|
400
|
-
"sigma_total(mS/cm)",
|
|
401
|
+
"Folder", "sigma_total(mS/cm)", "sigma_x(mS/cm)",
|
|
402
|
+
"sigma_y(mS/cm)", "sigma_z(mS/cm)", "D_total(cm^2/s)",
|
|
403
|
+
"D_x(cm^2/s)", "D_y(cm^2/s)", "D_z(cm^2/s)",
|
|
401
404
|
]
|
|
402
405
|
lines = [
|
|
403
406
|
"GPUMD SDC and Nernst-Einstein conductivity summary",
|
|
404
407
|
f"Mobile ion: {diffuse_ion}",
|
|
405
408
|
"SDC average: arithmetic mean over the last 50% of correlation time",
|
|
406
409
|
"\t".join(columns),
|
|
410
|
+
"-" * len("\t".join(columns).expandtabs(4)),
|
|
407
411
|
]
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
+
jobs = {}
|
|
413
|
+
for record in records:
|
|
414
|
+
if record["element"] == diffuse_ion and record["conductivity"] is not None:
|
|
415
|
+
jobs.setdefault(record["job"], []).append(record)
|
|
416
|
+
for job, job_records in sorted(jobs.items()):
|
|
417
|
+
sigma = np.sum([record["conductivity"] for record in job_records], axis=0)
|
|
418
|
+
count = sum(record["count"] for record in job_records)
|
|
419
|
+
diffusion = sum(
|
|
420
|
+
record["diffusion"] * record["count"] for record in job_records
|
|
421
|
+
) / count
|
|
412
422
|
fields = [
|
|
413
|
-
|
|
414
|
-
f"{
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
f"{record['average_start']:.8f}",
|
|
418
|
-
f"{record['average_end']:.8f}",
|
|
419
|
-
*(f"{value:.8f}" for value in record["diffusion"]), *sigma,
|
|
423
|
+
job,
|
|
424
|
+
*(f"{sigma[index]:.2f}" for index in (3, 0, 1, 2)),
|
|
425
|
+
*(_compact_scientific(diffusion[index])
|
|
426
|
+
for index in (3, 0, 1, 2)),
|
|
420
427
|
]
|
|
421
428
|
lines.append("\t".join(fields))
|
|
422
429
|
(Path(root) / SUMMARY_FILE).write_text(
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
Examples:
|
|
4
4
|
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
|
|
5
5
|
pmk realloc-occ -if structures -of chg-balance -formula Li3YCl6
|
|
6
|
+
pmk realloc-occ -i input.cif -of out -formula Li3YCl6 -occ 0 0.5 1
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from __future__ import annotations
|
|
@@ -244,7 +245,73 @@ def _infer_z(parsed, target):
|
|
|
244
245
|
return min(scores)[2]
|
|
245
246
|
|
|
246
247
|
|
|
247
|
-
def
|
|
248
|
+
def _normalize_occupancies(values):
|
|
249
|
+
if values is None:
|
|
250
|
+
return None
|
|
251
|
+
occupancies = []
|
|
252
|
+
for value in values:
|
|
253
|
+
number = float(value)
|
|
254
|
+
if not -TOLERANCE <= number <= 1.0 + TOLERANCE:
|
|
255
|
+
raise ValueError("-occ values must be between 0 and 1")
|
|
256
|
+
rounded = round(number, 2)
|
|
257
|
+
if abs(number - rounded) > TOLERANCE:
|
|
258
|
+
raise ValueError("-occ values support at most two decimal places")
|
|
259
|
+
occupancies.append(min(1.0, max(0.0, rounded)))
|
|
260
|
+
result = tuple(sorted(set(occupancies)))
|
|
261
|
+
if not result:
|
|
262
|
+
raise ValueError("-occ requires at least one occupancy value")
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _grid_occupancy_options(sites, target_count, allowed_occupancies):
|
|
267
|
+
if len(sites) > MAX_ADJUSTABLE_SITES:
|
|
268
|
+
raise ValueError(f"more than {MAX_ADJUSTABLE_SITES} adjustable sites")
|
|
269
|
+
minimum, maximum = min(allowed_occupancies), max(allowed_occupancies)
|
|
270
|
+
options = []
|
|
271
|
+
|
|
272
|
+
def visit(index, total, values):
|
|
273
|
+
if len(options) > MAX_OUTPUTS:
|
|
274
|
+
raise ValueError(f"occupancy allocations exceed {MAX_OUTPUTS}")
|
|
275
|
+
if index == len(sites):
|
|
276
|
+
if abs(total - target_count) <= TOLERANCE:
|
|
277
|
+
options.append(tuple(values))
|
|
278
|
+
return
|
|
279
|
+
remaining = sites[index + 1:]
|
|
280
|
+
minimum_remaining = sum(site.multiplicity * minimum for site in remaining)
|
|
281
|
+
maximum_remaining = sum(site.multiplicity * maximum for site in remaining)
|
|
282
|
+
site = sites[index]
|
|
283
|
+
choices = sorted(
|
|
284
|
+
allowed_occupancies,
|
|
285
|
+
key=lambda value: (abs(value - site.occupancy), value),
|
|
286
|
+
)
|
|
287
|
+
for value in choices:
|
|
288
|
+
subtotal = total + site.multiplicity * value
|
|
289
|
+
if subtotal + minimum_remaining > target_count + TOLERANCE:
|
|
290
|
+
continue
|
|
291
|
+
if subtotal + maximum_remaining < target_count - TOLERANCE:
|
|
292
|
+
continue
|
|
293
|
+
visit(index + 1, subtotal, values + [value])
|
|
294
|
+
|
|
295
|
+
visit(0, 0.0, [])
|
|
296
|
+
options = list(dict.fromkeys(options))
|
|
297
|
+
options.sort(key=lambda values: (
|
|
298
|
+
sum(abs(site.occupancy - value) > TOLERANCE
|
|
299
|
+
for site, value in zip(sites, values)),
|
|
300
|
+
tuple(index for index, (site, value) in enumerate(zip(sites, values))
|
|
301
|
+
if abs(site.occupancy - value) > TOLERANCE),
|
|
302
|
+
values,
|
|
303
|
+
))
|
|
304
|
+
if not options:
|
|
305
|
+
allowed = ", ".join(f"{value:.2f}" for value in allowed_occupancies)
|
|
306
|
+
raise ValueError(
|
|
307
|
+
f"no allocation reaches the target using -occ values: {allowed}"
|
|
308
|
+
)
|
|
309
|
+
return options
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _adjustment_options(sites, target_count, allowed_occupancies=None):
|
|
313
|
+
if allowed_occupancies is not None:
|
|
314
|
+
return _grid_occupancy_options(sites, target_count, allowed_occupancies)
|
|
248
315
|
current = sum(site.multiplicity * site.occupancy for site in sites)
|
|
249
316
|
delta = target_count - current
|
|
250
317
|
if abs(delta) <= TOLERANCE:
|
|
@@ -296,7 +363,7 @@ def _adjustment_options(sites, target_count):
|
|
|
296
363
|
return options
|
|
297
364
|
|
|
298
365
|
|
|
299
|
-
def generate_candidates(parsed, target_formula):
|
|
366
|
+
def generate_candidates(parsed, target_formula, allowed_occupancies=None):
|
|
300
367
|
target = _parse_formula(target_formula)
|
|
301
368
|
sites_by_element = OrderedDict()
|
|
302
369
|
for index, site in enumerate(parsed.sites):
|
|
@@ -313,8 +380,11 @@ def generate_candidates(parsed, target_formula):
|
|
|
313
380
|
|
|
314
381
|
element_options = []
|
|
315
382
|
for element, indexed_sites in sites_by_element.items():
|
|
316
|
-
options = _adjustment_options(
|
|
317
|
-
|
|
383
|
+
options = _adjustment_options(
|
|
384
|
+
[site for _, site in indexed_sites],
|
|
385
|
+
target_counts.get(element, 0.0),
|
|
386
|
+
allowed_occupancies,
|
|
387
|
+
)
|
|
318
388
|
element_options.append((indexed_sites, options))
|
|
319
389
|
output_count = math.prod(len(options) for _, options in element_options)
|
|
320
390
|
if output_count > MAX_OUTPUTS:
|
|
@@ -389,9 +459,11 @@ def _detected_formula(parsed):
|
|
|
389
459
|
return _format_formula(composition)
|
|
390
460
|
|
|
391
461
|
|
|
392
|
-
def process_one(source, output_root, target_formula):
|
|
462
|
+
def process_one(source, output_root, target_formula, allowed_occupancies=None):
|
|
393
463
|
parsed = parse_cif(source)
|
|
394
|
-
target, target_counts, z, candidates = generate_candidates(
|
|
464
|
+
target, target_counts, z, candidates = generate_candidates(
|
|
465
|
+
parsed, target_formula, allowed_occupancies
|
|
466
|
+
)
|
|
395
467
|
destination = Path(output_root) / Path(source).stem
|
|
396
468
|
if destination.exists() and (not destination.is_dir()
|
|
397
469
|
or any(destination.iterdir())):
|
|
@@ -417,6 +489,10 @@ def add_arguments(parser):
|
|
|
417
489
|
help="Top-level output folder; each input gets a subfolder.")
|
|
418
490
|
parser.add_argument("-formula", required=True,
|
|
419
491
|
help="Target reduced formula, for example Li3YCl6.")
|
|
492
|
+
parser.add_argument(
|
|
493
|
+
"-occ", nargs="+", type=float,
|
|
494
|
+
help="Allowed site occupancies, for example 0 0.25 0.5 0.75 1.",
|
|
495
|
+
)
|
|
420
496
|
|
|
421
497
|
|
|
422
498
|
def run(args):
|
|
@@ -432,12 +508,19 @@ def run(args):
|
|
|
432
508
|
if not sources:
|
|
433
509
|
raise SystemExit(f"Error: no CIF files found in {folder}")
|
|
434
510
|
|
|
511
|
+
try:
|
|
512
|
+
allowed_occupancies = _normalize_occupancies(args.occ)
|
|
513
|
+
except ValueError as exc:
|
|
514
|
+
raise SystemExit(f"Error: {exc}") from exc
|
|
515
|
+
|
|
435
516
|
output_root = Path(args.output_folder)
|
|
436
517
|
output_root.mkdir(parents=True, exist_ok=True)
|
|
437
518
|
files_written, inputs_done = 0, 0
|
|
438
519
|
for source in sources:
|
|
439
520
|
try:
|
|
440
|
-
files_written += process_one(
|
|
521
|
+
files_written += process_one(
|
|
522
|
+
source, output_root, args.formula, allowed_occupancies
|
|
523
|
+
)
|
|
441
524
|
inputs_done += 1
|
|
442
525
|
except Exception as exc:
|
|
443
526
|
print(f"{source}: failed ({exc})")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.3
|
|
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.25 0.5 0.75 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,11 @@ 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 row per job folder. Conductivities use two decimal places; diffusion
|
|
218
|
+
coefficients use compact scientific notation with two decimal places. If the
|
|
219
|
+
mobile ion occupies multiple groups, conductivities are summed and diffusion
|
|
220
|
+
coefficients are particle-count-weighted.
|
|
216
221
|
|
|
217
222
|
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
218
223
|
of the final SDC-producing MD run and the official 18-column
|
|
@@ -231,7 +236,9 @@ oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
|
231
236
|
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
232
237
|
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
233
238
|
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
234
|
-
`-formula` options.
|
|
239
|
+
`-formula` options. Add `-occ` followed by the only permitted occupancy values;
|
|
240
|
+
for example, `-occ 0 0.25 0.5 0.75 1` also permits the valid allocation
|
|
241
|
+
`Li1=0.75, Li2=0.75`.
|
|
235
242
|
|
|
236
243
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
237
244
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
@@ -318,6 +325,17 @@ Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
|
318
325
|
Because Wyckoff metadata describes one crystal structure, each `.cif` output
|
|
319
326
|
file accepts exactly one frame.
|
|
320
327
|
|
|
328
|
+
`convert -d2o` orders partially occupied CIF structures. `-sc-matrix 1x1x1`
|
|
329
|
+
means a 1x1x1 cell; three diagonal factors or nine integer matrix entries are
|
|
330
|
+
also accepted. Folder ordering scans recursively and mirrors input subfolders.
|
|
331
|
+
It uses pymatgen `EnumerateStructureTransformation` with one worker to remove
|
|
332
|
+
symmetry-equivalent orderings and select the lowest Ewald energy, then falls
|
|
333
|
+
back to bounded `OrderDisorderedStructureTransformation` when enumlib is not
|
|
334
|
+
available. Equivalent results from separate disorder files are merged and
|
|
335
|
+
written as `BASE-o1.cif`, `BASE-o2.cif`, and so on. For convenience, supplying
|
|
336
|
+
`-sc-matrix` without `-d2o` automatically enables ordering when a disordered
|
|
337
|
+
CIF is detected; ordered inputs instead receive an ordinary supercell.
|
|
338
|
+
|
|
321
339
|
After `supercell -temp` creates all requested jobs, it checks generated folders
|
|
322
340
|
that contain `run.in`, `model.xyz`, and `nep.txt`. Existing `ensemble npt_scr`
|
|
323
341
|
lines retain their temperature parameters but are rewritten with three
|
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
"""Convert structures and trajectories between ASE/pymatgen formats."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
try:
|
|
9
|
-
from . import _cifio, _fileio, _precision
|
|
10
|
-
except ImportError:
|
|
11
|
-
import _cifio
|
|
12
|
-
import _fileio
|
|
13
|
-
import _precision
|
|
14
|
-
|
|
15
|
-
COMMAND = "convert"
|
|
16
|
-
HELP = "Convert structures or trajectories between ASE/pymatgen formats."
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def _read(path, all_frames=False):
|
|
20
|
-
from ase.io import read
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
return read(str(path), index=":" if all_frames else -1)
|
|
24
|
-
except Exception as ase_error:
|
|
25
|
-
if all_frames:
|
|
26
|
-
raise ValueError(f"ASE could not read trajectory {path}: {ase_error}") from ase_error
|
|
27
|
-
try:
|
|
28
|
-
from pymatgen.core import Molecule, Structure
|
|
29
|
-
from pymatgen.io.ase import AseAtomsAdaptor
|
|
30
|
-
|
|
31
|
-
try:
|
|
32
|
-
obj = Structure.from_file(str(path))
|
|
33
|
-
except Exception:
|
|
34
|
-
obj = Molecule.from_file(str(path))
|
|
35
|
-
return AseAtomsAdaptor.get_atoms(obj)
|
|
36
|
-
except Exception as pmg_error:
|
|
37
|
-
raise ValueError(
|
|
38
|
-
f"neither ASE nor pymatgen could read {path}: "
|
|
39
|
-
f"ASE={ase_error}; pymatgen={pmg_error}"
|
|
40
|
-
) from pmg_error
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def _ase_format(path):
|
|
44
|
-
suffix = Path(path).suffix.lower()
|
|
45
|
-
if suffix in {".xyz", ".extxyz"}:
|
|
46
|
-
return "extxyz"
|
|
47
|
-
if suffix == ".vasp":
|
|
48
|
-
return "vasp"
|
|
49
|
-
return None
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _normalize_output(path):
|
|
53
|
-
suffix = Path(path).suffix.lower()
|
|
54
|
-
if suffix in {".xyz", ".extxyz"}:
|
|
55
|
-
_precision.normalize_extxyz(path)
|
|
56
|
-
elif suffix == ".vasp":
|
|
57
|
-
_precision.normalize_vasp(path)
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _write(path, atoms):
|
|
61
|
-
path = Path(path)
|
|
62
|
-
if _cifio.is_cif(path):
|
|
63
|
-
if isinstance(atoms, (list, tuple)) and len(atoms) != 1:
|
|
64
|
-
raise ValueError("CIF output supports exactly one structure per file")
|
|
65
|
-
one = atoms[0] if isinstance(atoms, (list, tuple)) else atoms
|
|
66
|
-
_cifio.write_cif(one, path, data_name=path.stem)
|
|
67
|
-
return
|
|
68
|
-
|
|
69
|
-
from ase.io import write
|
|
70
|
-
|
|
71
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
72
|
-
kwargs = {}
|
|
73
|
-
output_format = _ase_format(path)
|
|
74
|
-
if output_format:
|
|
75
|
-
kwargs["format"] = output_format
|
|
76
|
-
if output_format == "vasp":
|
|
77
|
-
kwargs["direct"] = True
|
|
78
|
-
try:
|
|
79
|
-
write(str(path), atoms, **kwargs)
|
|
80
|
-
_normalize_output(path)
|
|
81
|
-
return
|
|
82
|
-
except Exception as ase_error:
|
|
83
|
-
if isinstance(atoms, (list, tuple)) and len(atoms) != 1:
|
|
84
|
-
raise ValueError(
|
|
85
|
-
f"ASE cannot write this multi-frame trajectory to {path}: {ase_error}"
|
|
86
|
-
) from ase_error
|
|
87
|
-
one = atoms[0] if isinstance(atoms, (list, tuple)) else atoms
|
|
88
|
-
try:
|
|
89
|
-
from pymatgen.io.ase import AseAtomsAdaptor
|
|
90
|
-
|
|
91
|
-
if one.pbc.any():
|
|
92
|
-
obj = AseAtomsAdaptor.get_structure(one)
|
|
93
|
-
else:
|
|
94
|
-
obj = AseAtomsAdaptor.get_molecule(one)
|
|
95
|
-
obj.to(filename=str(path))
|
|
96
|
-
_normalize_output(path)
|
|
97
|
-
except Exception as pmg_error:
|
|
98
|
-
raise ValueError(
|
|
99
|
-
f"neither ASE nor pymatgen could write {path}: "
|
|
100
|
-
f"ASE={ase_error}; pymatgen={pmg_error}"
|
|
101
|
-
) from pmg_error
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def convert_one(input_path, output_path, trajectory=False):
|
|
105
|
-
atoms = _read(input_path, all_frames=trajectory)
|
|
106
|
-
_write(output_path, atoms)
|
|
107
|
-
count = len(atoms) if trajectory else 1
|
|
108
|
-
label = f" ({count} frame(s))" if trajectory else ""
|
|
109
|
-
print(f"{input_path} -> {output_path}{label}")
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
113
|
-
parser.add_argument("-i", "-input", dest="input",
|
|
114
|
-
help="Single input structure.")
|
|
115
|
-
parser.add_argument("-o", "-output", dest="output",
|
|
116
|
-
help="Single output structure; with -if, output folder.")
|
|
117
|
-
parser.add_argument("-it", "-input-trajectory", dest="input_trajectory",
|
|
118
|
-
help="Input trajectory containing multiple structures.")
|
|
119
|
-
parser.add_argument("-ot", "-output-trajectory", dest="output_trajectory",
|
|
120
|
-
help="Output trajectory.")
|
|
121
|
-
parser.add_argument("-if", "-input-folder", dest="input_folder",
|
|
122
|
-
help="Folder of input structures.")
|
|
123
|
-
parser.add_argument("-of", "-output-folder", dest="output_folder",
|
|
124
|
-
help="Output folder for batch conversion.")
|
|
125
|
-
parser.add_argument("-oe", "-output-extension", dest="output_extension",
|
|
126
|
-
default="xyz",
|
|
127
|
-
help="Batch/scan output extension (default: xyz).")
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def _extension(value):
|
|
131
|
-
value = str(value).strip()
|
|
132
|
-
return value if value.startswith(".") else "." + value
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
def run(args) -> int:
|
|
136
|
-
modes = [bool(args.input), bool(args.input_trajectory), bool(args.input_folder)]
|
|
137
|
-
if sum(modes) > 1:
|
|
138
|
-
raise SystemExit("Error: use only one of -i, -it, or -if.")
|
|
139
|
-
|
|
140
|
-
if args.input:
|
|
141
|
-
if not args.output:
|
|
142
|
-
raise SystemExit("Error: -o is required with -i.")
|
|
143
|
-
convert_one(args.input, args.output)
|
|
144
|
-
return 0
|
|
145
|
-
|
|
146
|
-
if args.input_trajectory:
|
|
147
|
-
if not args.output_trajectory:
|
|
148
|
-
raise SystemExit("Error: -ot is required with -it.")
|
|
149
|
-
convert_one(args.input_trajectory, args.output_trajectory, trajectory=True)
|
|
150
|
-
return 0
|
|
151
|
-
|
|
152
|
-
extension = _extension(args.output_extension)
|
|
153
|
-
if args.input_folder:
|
|
154
|
-
input_folder = Path(args.input_folder)
|
|
155
|
-
if args.output and args.output_folder:
|
|
156
|
-
raise SystemExit("Error: use only one of -o or -of with -if.")
|
|
157
|
-
folder_option = args.output_folder or args.output
|
|
158
|
-
output_folder = (
|
|
159
|
-
Path(folder_option) if folder_option
|
|
160
|
-
else input_folder.with_name(input_folder.name + "-converted")
|
|
161
|
-
)
|
|
162
|
-
output_folder.mkdir(parents=True, exist_ok=True)
|
|
163
|
-
files = _fileio.structure_files(input_folder)
|
|
164
|
-
for path in files:
|
|
165
|
-
convert_one(path, output_folder / (path.stem + extension))
|
|
166
|
-
print(f"Done: converted {len(files)} file(s) to {output_folder}")
|
|
167
|
-
return 0
|
|
168
|
-
|
|
169
|
-
count = 0
|
|
170
|
-
for folder in _fileio.subfolders("."):
|
|
171
|
-
for path in sorted(folder.iterdir()):
|
|
172
|
-
if not _fileio.is_structure(path):
|
|
173
|
-
continue
|
|
174
|
-
if path.suffix.lower() == extension.lower():
|
|
175
|
-
continue
|
|
176
|
-
name = args.output if args.output else path.stem + extension
|
|
177
|
-
convert_one(path, folder / name)
|
|
178
|
-
count += 1
|
|
179
|
-
print(f"Done: converted {count} file(s) across sub-folders.")
|
|
180
|
-
return 0
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if __name__ == "__main__":
|
|
184
|
-
_parser = argparse.ArgumentParser(description=__doc__)
|
|
185
|
-
add_arguments(_parser)
|
|
186
|
-
raise SystemExit(run(_parser.parse_args()))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|