pymdkit 1.5.1__tar.gz → 1.5.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymdkit-1.5.1 → pymdkit-1.5.2}/PKG-INFO +39 -4
- {pymdkit-1.5.1 → pymdkit-1.5.2}/README.md +41 -6
- {pymdkit-1.5.1 → pymdkit-1.5.2}/pyproject.toml +1 -1
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.5.2/src/pymdkit/commands/gpumd_msd.py +461 -0
- pymdkit-1.5.2/src/pymdkit/commands/realloc_occ.py +452 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/PKG-INFO +39 -4
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/SOURCES.txt +2 -1
- pymdkit-1.5.1/src/pymdkit/commands/compute_msd_all_groups.py +0 -375
- {pymdkit-1.5.1 → pymdkit-1.5.2}/LICENSE +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/setup.cfg +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_cifio.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_gpumd.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_precision.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/gather_fs.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_relax.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_thermo.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.5.1 → pymdkit-1.5.2}/src/pymdkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.2
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -84,7 +84,7 @@ directory for job sub-folders automatically.
|
|
|
84
84
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
85
85
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
86
86
|
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
87
|
-
| `msd` |
|
|
87
|
+
| `gpumd-msd` | Recursively extract directional SDC and Nernst-Einstein conductivity from GPUMD jobs |
|
|
88
88
|
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
89
89
|
| `perturb` | Generate perturbed structures with dpdata |
|
|
90
90
|
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
@@ -92,6 +92,7 @@ directory for job sub-folders automatically.
|
|
|
92
92
|
| `rmsd` | Compare structures by RMSD and optionally remove higher-energy duplicates |
|
|
93
93
|
| `chemsys-entry` | Download stable Materials Project structures for a chemical system |
|
|
94
94
|
| `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
|
|
95
|
+
| `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
|
|
95
96
|
| `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
|
|
96
97
|
| `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
|
|
97
98
|
| `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
|
|
@@ -119,8 +120,8 @@ pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run
|
|
|
119
120
|
pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
|
|
120
121
|
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
121
122
|
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
122
|
-
pmk msd
|
|
123
|
-
pmk msd -diffuse_ion Li -ion_charge 1
|
|
123
|
+
pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
|
|
124
|
+
pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
|
|
124
125
|
pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
|
|
125
126
|
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
126
127
|
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
@@ -132,6 +133,8 @@ pmk vasp2xyz # scans ./ for VASP output folde
|
|
|
132
133
|
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
133
134
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
134
135
|
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
136
|
+
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
|
|
137
|
+
pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
|
|
135
138
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
136
139
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
137
140
|
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
@@ -200,6 +203,36 @@ dump_xyz -1 0 1 relaxed.xyz force
|
|
|
200
203
|
run 1
|
|
201
204
|
```
|
|
202
205
|
|
|
206
|
+
`gpumd-msd` recursively discovers job folders at any depth below `-entry-path`
|
|
207
|
+
(the current directory by default). A job must contain `msd.out`, `model.xyz`,
|
|
208
|
+
`run.in`, and `thermo.out`; the common accidental name `thermos.out` is also
|
|
209
|
+
accepted. Following the [official GPUMD `msd.out` format](https://gpumd.org/gpumd/output_files/msd_out.html), each group block is read as three MSD columns
|
|
210
|
+
followed by three directional SDC columns. PMK takes the arithmetic mean over
|
|
211
|
+
the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
|
|
212
|
+
multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
|
|
213
|
+
Each job receives `msd/group_INDEX_ELEMENT_TEMP_msd.txt`, a matching `_sdc.txt`,
|
|
214
|
+
and a `_results.txt`. The root receives `sigma-sdc-summary.txt` with directional
|
|
215
|
+
and total diffusion coefficients and conductivities.
|
|
216
|
+
|
|
217
|
+
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
218
|
+
of the final SDC-producing MD run and the official 18-column
|
|
219
|
+
[`thermo.out` format](https://gpumd.org/gpumd/output_files/thermo_out.html):
|
|
220
|
+
for NPT ensembles (`npt_scr`, `npt_mttk`, and other `npt_*` forms), PMK uses
|
|
221
|
+
the mean triclinic-box volume over that run; for NVT ensembles (`nvt_bdp`,
|
|
222
|
+
`nvt_ber`, and other `nvt_*` forms), it uses the final fixed-cell volume.
|
|
223
|
+
`V(A^3) * 1e-24 -> V(cm^3)`, exact SI values for elementary charge and the
|
|
224
|
+
Boltzmann constant, and a final `* 1000` conversion from S/cm to mS/cm.
|
|
225
|
+
|
|
226
|
+
`realloc-occ -i INPUT.cif -of OUTPUT -formula FORMULA` computes atom counts as
|
|
227
|
+
Wyckoff multiplicity times occupancy and uses `_cell_formula_units_Z` to map the
|
|
228
|
+
target reduced formula into the existing cell. It enumerates finite site-wise
|
|
229
|
+
occupancy allocations, retains only charge-balanced candidates using the CIF
|
|
230
|
+
oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
231
|
+
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
232
|
+
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
233
|
+
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
234
|
+
`-formula` options.
|
|
235
|
+
|
|
203
236
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
204
237
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
205
238
|
requires full convergence before copying the selected `-fs-name` as `.vasp`. The gathered VASP file's first line is replaced with `energy=<final_energy> eV` for later screening.
|
|
@@ -346,6 +379,7 @@ pymdkit/
|
|
|
346
379
|
|-- _geometry.py # shared minimum-image geometry helper
|
|
347
380
|
|-- _vaspset.py # shared VASP input-set helper (not a command)
|
|
348
381
|
|-- gpumd_group.py
|
|
382
|
+
|-- gpumd_msd.py
|
|
349
383
|
|-- gpumd_relax.py
|
|
350
384
|
|-- gpumd_thermo.py
|
|
351
385
|
|-- gpumd_trjcat.py
|
|
@@ -360,6 +394,7 @@ pymdkit/
|
|
|
360
394
|
|-- chemsys_entry.py
|
|
361
395
|
|-- submit_vasp.py
|
|
362
396
|
|-- convert.py
|
|
397
|
+
|-- realloc_occ.py
|
|
363
398
|
|-- substitute.py
|
|
364
399
|
|-- supercell.py
|
|
365
400
|
|-- vasp2xyz.py
|
|
@@ -57,14 +57,15 @@ directory for job sub-folders automatically.
|
|
|
57
57
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
58
58
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
59
59
|
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
60
|
-
| `msd` |
|
|
60
|
+
| `gpumd-msd` | Recursively extract directional SDC and Nernst-Einstein conductivity from GPUMD jobs |
|
|
61
61
|
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
62
62
|
| `perturb` | Generate perturbed structures with dpdata |
|
|
63
63
|
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
64
64
|
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
65
65
|
| `rmsd` | Compare structures by RMSD and optionally remove higher-energy duplicates |
|
|
66
66
|
| `chemsys-entry` | Download stable Materials Project structures for a chemical system |
|
|
67
|
-
| `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
|
|
67
|
+
| `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
|
|
68
|
+
| `realloc-occ` | Generate charge-balanced CIF occupancy allocations for a target formula |
|
|
68
69
|
| `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
|
|
69
70
|
| `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
|
|
70
71
|
| `symmetrize` | Detect/refine symmetry for structure files, folders, or CSV rows -> CIF |
|
|
@@ -92,8 +93,8 @@ pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run
|
|
|
92
93
|
pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
|
|
93
94
|
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
94
95
|
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
95
|
-
pmk msd
|
|
96
|
-
pmk msd -diffuse_ion Li -ion_charge 1
|
|
96
|
+
pmk gpumd-msd # recursively scans GPUMD jobs -> per-job msd/ + sigma-sdc-summary.txt
|
|
97
|
+
pmk gpumd-msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion and signed valence
|
|
97
98
|
pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
|
|
98
99
|
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
99
100
|
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
@@ -105,6 +106,8 @@ pmk vasp2xyz # scans ./ for VASP output folde
|
|
|
105
106
|
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
106
107
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
107
108
|
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
109
|
+
pmk realloc-occ -i 66-Li4YCl6-5.cif -of chg-balance -formula Li3YCl6
|
|
110
|
+
pmk realloc-occ -if cifs -of chg-balance -formula Li3YCl6
|
|
108
111
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
109
112
|
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
110
113
|
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
@@ -173,6 +176,36 @@ dump_xyz -1 0 1 relaxed.xyz force
|
|
|
173
176
|
run 1
|
|
174
177
|
```
|
|
175
178
|
|
|
179
|
+
`gpumd-msd` recursively discovers job folders at any depth below `-entry-path`
|
|
180
|
+
(the current directory by default). A job must contain `msd.out`, `model.xyz`,
|
|
181
|
+
`run.in`, and `thermo.out`; the common accidental name `thermos.out` is also
|
|
182
|
+
accepted. Following the [official GPUMD `msd.out` format](https://gpumd.org/gpumd/output_files/msd_out.html), each group block is read as three MSD columns
|
|
183
|
+
followed by three directional SDC columns. PMK takes the arithmetic mean over
|
|
184
|
+
the last 50% of the SDC correlation-time range, converts A^2/ps to cm^2/s by
|
|
185
|
+
multiplying by `1e-4`, and reports `D_total = (D_x + D_y + D_z) / 3`.
|
|
186
|
+
Each job receives `msd/group_INDEX_ELEMENT_TEMP_msd.txt`, a matching `_sdc.txt`,
|
|
187
|
+
and a `_results.txt`. The root receives `sigma-sdc-summary.txt` with directional
|
|
188
|
+
and total diffusion coefficients and conductivities.
|
|
189
|
+
|
|
190
|
+
Ionic conductivity uses the Nernst-Einstein relation with the mean temperature
|
|
191
|
+
of the final SDC-producing MD run and the official 18-column
|
|
192
|
+
[`thermo.out` format](https://gpumd.org/gpumd/output_files/thermo_out.html):
|
|
193
|
+
for NPT ensembles (`npt_scr`, `npt_mttk`, and other `npt_*` forms), PMK uses
|
|
194
|
+
the mean triclinic-box volume over that run; for NVT ensembles (`nvt_bdp`,
|
|
195
|
+
`nvt_ber`, and other `nvt_*` forms), it uses the final fixed-cell volume.
|
|
196
|
+
`V(A^3) * 1e-24 -> V(cm^3)`, exact SI values for elementary charge and the
|
|
197
|
+
Boltzmann constant, and a final `* 1000` conversion from S/cm to mS/cm.
|
|
198
|
+
|
|
199
|
+
`realloc-occ -i INPUT.cif -of OUTPUT -formula FORMULA` computes atom counts as
|
|
200
|
+
Wyckoff multiplicity times occupancy and uses `_cell_formula_units_Z` to map the
|
|
201
|
+
target reduced formula into the existing cell. It enumerates finite site-wise
|
|
202
|
+
occupancy allocations, retains only charge-balanced candidates using the CIF
|
|
203
|
+
oxidation-number loop, and preserves the original symmetry, coordinates,
|
|
204
|
+
Wyckoff labels, and CIF layout. Every input receives `OUTPUT/INPUT_STEM/`; for
|
|
205
|
+
the supplied Li4YCl6 example, the two outputs are `...-d1.cif` with `Li1=0.50`
|
|
206
|
+
and `...-d2.cif` with `Li2=0.50`. Folder mode uses `-if` with the same `-of` and
|
|
207
|
+
`-formula` options.
|
|
208
|
+
|
|
176
209
|
`gather-fs` recursively scans all job folders below the current directory. For
|
|
177
210
|
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
178
211
|
requires full convergence before copying the selected `-fs-name` as `.vasp`. The gathered VASP file's first line is replaced with `energy=<final_energy> eV` for later screening.
|
|
@@ -318,7 +351,8 @@ pymdkit/
|
|
|
318
351
|
|-- _gpumd.py # shared GPUMD run.in and thermo analysis
|
|
319
352
|
|-- _geometry.py # shared minimum-image geometry helper
|
|
320
353
|
|-- _vaspset.py # shared VASP input-set helper (not a command)
|
|
321
|
-
|-- gpumd_group.py
|
|
354
|
+
|-- gpumd_group.py
|
|
355
|
+
|-- gpumd_msd.py
|
|
322
356
|
|-- gpumd_relax.py
|
|
323
357
|
|-- gpumd_thermo.py
|
|
324
358
|
|-- gpumd_trjcat.py
|
|
@@ -332,7 +366,8 @@ pymdkit/
|
|
|
332
366
|
|-- _cifio.py
|
|
333
367
|
|-- chemsys_entry.py
|
|
334
368
|
|-- submit_vasp.py
|
|
335
|
-
|-- convert.py
|
|
369
|
+
|-- convert.py
|
|
370
|
+
|-- realloc_occ.py
|
|
336
371
|
|-- substitute.py
|
|
337
372
|
|-- supercell.py
|
|
338
373
|
|-- vasp2xyz.py
|
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
"""Extract directional self-diffusion and conductivity from GPUMD MSD jobs.
|
|
2
|
+
|
|
3
|
+
``pmk gpumd-msd`` recursively scans for job folders containing ``msd.out``,
|
|
4
|
+
``thermo.out`` (or ``thermos.out``), ``model.xyz``, and ``run.in``. GPUMD
|
|
5
|
+
already writes directional self-diffusion coefficients (SDC) in columns 5-7
|
|
6
|
+
of each six-column group block. Reported diffusion coefficients are arithmetic
|
|
7
|
+
means over the last 50% of the correlation-time range.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from ._gpumd import cell_quantities
|
|
22
|
+
except ImportError: # pragma: no cover - direct script execution
|
|
23
|
+
from _gpumd import cell_quantities
|
|
24
|
+
|
|
25
|
+
COMMAND = "gpumd-msd"
|
|
26
|
+
HELP = "Recursively extract GPUMD self-diffusion and ionic conductivity."
|
|
27
|
+
CORE_JOB_FILES = ("msd.out", "model.xyz", "run.in")
|
|
28
|
+
THERMO_NAMES = ("thermo.out", "thermos.out")
|
|
29
|
+
SUMMARY_FILE = "sigma-sdc-summary.txt"
|
|
30
|
+
ELEMENTARY_CHARGE_C = 1.602176634e-19
|
|
31
|
+
BOLTZMANN_J_PER_K = 1.380649e-23
|
|
32
|
+
A2_PER_PS_TO_CM2_PER_S = 1.0e-4
|
|
33
|
+
AVERAGE_FRACTION = 0.5
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class MsdSpec:
|
|
38
|
+
sample_interval: int
|
|
39
|
+
correlation_steps: int
|
|
40
|
+
time_step_fs: float
|
|
41
|
+
group_method: int = 0
|
|
42
|
+
group_id: Optional[int] = None
|
|
43
|
+
all_groups: bool = False
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def maximum_time_ps(self):
|
|
47
|
+
return (self.sample_interval * self.correlation_steps
|
|
48
|
+
* self.time_step_fs / 1000.0)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _parse_compute_msd(parts, time_step_fs):
|
|
52
|
+
if len(parts) < 3:
|
|
53
|
+
raise ValueError("compute_msd requires sample_interval and Nc")
|
|
54
|
+
sample_interval, correlation_steps = int(parts[1]), int(parts[2])
|
|
55
|
+
if sample_interval <= 0 or correlation_steps <= 0:
|
|
56
|
+
raise ValueError("compute_msd sample_interval and Nc must be positive")
|
|
57
|
+
tail = parts[3:]
|
|
58
|
+
group_method, group_id, all_groups = 0, None, False
|
|
59
|
+
if "all_groups" in tail:
|
|
60
|
+
index = tail.index("all_groups")
|
|
61
|
+
if index + 1 >= len(tail):
|
|
62
|
+
raise ValueError("compute_msd all_groups requires a group method")
|
|
63
|
+
group_method, all_groups = int(tail[index + 1]), True
|
|
64
|
+
elif "group" in tail:
|
|
65
|
+
index = tail.index("group")
|
|
66
|
+
if index + 2 >= len(tail):
|
|
67
|
+
raise ValueError("compute_msd group requires a method and group index")
|
|
68
|
+
group_method, group_id = int(tail[index + 1]), int(tail[index + 2])
|
|
69
|
+
return MsdSpec(sample_interval, correlation_steps, float(time_step_fs),
|
|
70
|
+
group_method, group_id, all_groups)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def parse_run_file(path):
|
|
74
|
+
"""Return the final SDC-producing specification, runs, and target run."""
|
|
75
|
+
time_step_fs = 1.0
|
|
76
|
+
active_dump_thermo = None
|
|
77
|
+
active_ensemble = None
|
|
78
|
+
active_msd = None
|
|
79
|
+
runs = []
|
|
80
|
+
for raw in Path(path).read_text(encoding="utf-8").splitlines():
|
|
81
|
+
parts = raw.split("#", 1)[0].split()
|
|
82
|
+
if not parts:
|
|
83
|
+
continue
|
|
84
|
+
key = parts[0].lower()
|
|
85
|
+
if key == "time_step" and len(parts) >= 2:
|
|
86
|
+
time_step_fs = float(parts[1])
|
|
87
|
+
elif key == "dump_thermo" and len(parts) >= 2:
|
|
88
|
+
active_dump_thermo = int(parts[1])
|
|
89
|
+
elif key == "ensemble" and len(parts) >= 2:
|
|
90
|
+
active_ensemble = parts[1].lower()
|
|
91
|
+
elif key == "compute_msd":
|
|
92
|
+
active_msd = _parse_compute_msd(parts, time_step_fs)
|
|
93
|
+
elif key == "run" and len(parts) >= 2:
|
|
94
|
+
runs.append({
|
|
95
|
+
"index": len(runs),
|
|
96
|
+
"steps": int(parts[1]),
|
|
97
|
+
"dump_thermo": active_dump_thermo,
|
|
98
|
+
"time_step_fs": time_step_fs,
|
|
99
|
+
"ensemble": active_ensemble,
|
|
100
|
+
"compute_msd": active_msd,
|
|
101
|
+
})
|
|
102
|
+
active_dump_thermo = None
|
|
103
|
+
active_ensemble = None
|
|
104
|
+
active_msd = None
|
|
105
|
+
sdc_runs = [run for run in runs if run["compute_msd"] is not None]
|
|
106
|
+
if not sdc_runs:
|
|
107
|
+
raise ValueError("run.in contains no compute_msd directive")
|
|
108
|
+
target_run = sdc_runs[-1]
|
|
109
|
+
return target_run["compute_msd"], runs, target_run
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _last_monotonic_segment(data):
|
|
113
|
+
"""Keep the final block if appended GPUMD runs reset correlation time."""
|
|
114
|
+
if len(data) < 2:
|
|
115
|
+
return data
|
|
116
|
+
resets = np.flatnonzero(np.diff(data[:, 0]) <= 0.0)
|
|
117
|
+
return data[resets[-1] + 1:] if len(resets) else data
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _thermo_run_slice(n_points, runs, target_run):
|
|
121
|
+
"""Map the target run to its rows in a concatenated thermo.out."""
|
|
122
|
+
thermo_runs = [run for run in runs if run["dump_thermo"]]
|
|
123
|
+
if target_run["dump_thermo"] is None:
|
|
124
|
+
raise ValueError("the SDC-producing run has no dump_thermo directive")
|
|
125
|
+
counts = {
|
|
126
|
+
run["index"]: max(1, run["steps"] // run["dump_thermo"])
|
|
127
|
+
for run in thermo_runs
|
|
128
|
+
}
|
|
129
|
+
expected_total = sum(counts.values())
|
|
130
|
+
target_count = counts[target_run["index"]]
|
|
131
|
+
if expected_total == n_points:
|
|
132
|
+
start = sum(
|
|
133
|
+
counts[run["index"]]
|
|
134
|
+
for run in thermo_runs
|
|
135
|
+
if run["index"] < target_run["index"]
|
|
136
|
+
)
|
|
137
|
+
return slice(start, start + target_count)
|
|
138
|
+
if thermo_runs[-1]["index"] == target_run["index"]:
|
|
139
|
+
return slice(max(0, n_points - target_count), n_points)
|
|
140
|
+
raise ValueError(
|
|
141
|
+
"thermo.out row count does not match run.in, so the SDC-producing "
|
|
142
|
+
"run cannot be mapped safely"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _thermo_path(folder):
|
|
147
|
+
for name in THERMO_NAMES:
|
|
148
|
+
path = Path(folder) / name
|
|
149
|
+
if path.is_file():
|
|
150
|
+
return path
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def load_thermo(path, runs, target_run):
|
|
155
|
+
"""Return temperature and ensemble-appropriate volume for the SDC run."""
|
|
156
|
+
data = np.atleast_2d(np.loadtxt(Path(path)))
|
|
157
|
+
if data.shape[1] != 18:
|
|
158
|
+
raise ValueError("thermo.out must use the official 18-column GPUMD format")
|
|
159
|
+
selected = data[_thermo_run_slice(len(data), runs, target_run)]
|
|
160
|
+
if not len(selected):
|
|
161
|
+
raise ValueError("the SDC-producing run has no mapped thermo records")
|
|
162
|
+
volumes = cell_quantities(selected)[2]
|
|
163
|
+
ensemble = target_run["ensemble"]
|
|
164
|
+
if ensemble is None:
|
|
165
|
+
raise ValueError("the SDC-producing run has no ensemble directive")
|
|
166
|
+
if ensemble.startswith(("npt_", "nph_")):
|
|
167
|
+
volume = float(np.mean(volumes))
|
|
168
|
+
volume_source = "mean over SDC-producing run"
|
|
169
|
+
elif ensemble == "nve" or ensemble.startswith("nvt_"):
|
|
170
|
+
volume = float(volumes[-1])
|
|
171
|
+
volume_source = "final cell of SDC-producing run"
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError(
|
|
174
|
+
f"unsupported ensemble '{ensemble}' for conductivity volume"
|
|
175
|
+
)
|
|
176
|
+
temperature = float(np.mean(selected[:, 0]))
|
|
177
|
+
if temperature <= 0.0 or volume <= 0.0:
|
|
178
|
+
raise ValueError("temperature and volume must be positive")
|
|
179
|
+
return temperature, volume, ensemble, volume_source
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def load_groups(path, group_method=0):
|
|
183
|
+
"""Return group_id -> {element, count} from model.xyz."""
|
|
184
|
+
from ase.io import read
|
|
185
|
+
|
|
186
|
+
atoms = read(str(path), index=0)
|
|
187
|
+
symbols = np.asarray(atoms.get_chemical_symbols(), dtype=object)
|
|
188
|
+
raw_groups = atoms.arrays.get("group")
|
|
189
|
+
if raw_groups is None:
|
|
190
|
+
group_values = np.zeros(len(atoms), dtype=int)
|
|
191
|
+
else:
|
|
192
|
+
raw_groups = np.asarray(raw_groups)
|
|
193
|
+
if raw_groups.ndim == 1:
|
|
194
|
+
if group_method != 0:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"model.xyz has one grouping method, not method {group_method}")
|
|
197
|
+
group_values = raw_groups.astype(int)
|
|
198
|
+
else:
|
|
199
|
+
if group_method < 0 or group_method >= raw_groups.shape[1]:
|
|
200
|
+
raise ValueError(
|
|
201
|
+
f"model.xyz has {raw_groups.shape[1]} grouping method(s), "
|
|
202
|
+
f"not method {group_method}")
|
|
203
|
+
group_values = raw_groups[:, group_method].astype(int)
|
|
204
|
+
groups = {}
|
|
205
|
+
for group_id in sorted(set(group_values.tolist())):
|
|
206
|
+
selected = symbols[group_values == group_id]
|
|
207
|
+
elements = sorted(set(selected.tolist()))
|
|
208
|
+
groups[int(group_id)] = {
|
|
209
|
+
"element": elements[0] if len(elements) == 1 else "mixed",
|
|
210
|
+
"count": int(len(selected)),
|
|
211
|
+
}
|
|
212
|
+
return groups
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def load_msd(path, spec, groups):
|
|
216
|
+
"""Return group_id -> (time, MSD_xyz, SDC_xyz) from official blocks."""
|
|
217
|
+
data = np.atleast_2d(np.loadtxt(Path(path)))
|
|
218
|
+
if data.shape[1] < 7 or (data.shape[1] - 1) % 6:
|
|
219
|
+
raise ValueError(
|
|
220
|
+
"msd.out must contain time plus one or more six-column MSD/SDC blocks")
|
|
221
|
+
data = _last_monotonic_segment(data)
|
|
222
|
+
if len(data) < 2 or np.any(np.diff(data[:, 0]) <= 0.0):
|
|
223
|
+
raise ValueError("msd.out needs at least two increasing time points")
|
|
224
|
+
block_count = (data.shape[1] - 1) // 6
|
|
225
|
+
if spec.all_groups:
|
|
226
|
+
group_ids = sorted(groups)
|
|
227
|
+
if block_count != len(group_ids):
|
|
228
|
+
raise ValueError(
|
|
229
|
+
f"msd.out has {block_count} blocks, but model.xyz grouping method "
|
|
230
|
+
f"{spec.group_method} has {len(group_ids)} groups")
|
|
231
|
+
elif spec.group_id is not None:
|
|
232
|
+
if block_count != 1 or spec.group_id not in groups:
|
|
233
|
+
raise ValueError("single-group msd.out cannot be mapped to model.xyz")
|
|
234
|
+
group_ids = [spec.group_id]
|
|
235
|
+
elif block_count == 1 and len(groups) == 1:
|
|
236
|
+
group_ids = [next(iter(groups))]
|
|
237
|
+
else:
|
|
238
|
+
raise ValueError(
|
|
239
|
+
"compute_msd without group/all_groups cannot map multiple groups")
|
|
240
|
+
result = {}
|
|
241
|
+
for block_index, group_id in enumerate(group_ids):
|
|
242
|
+
start = 1 + 6 * block_index
|
|
243
|
+
result[group_id] = (data[:, 0], data[:, start:start + 3],
|
|
244
|
+
data[:, start + 3:start + 6])
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def average_sdc(times, sdc_xyz, fraction=AVERAGE_FRACTION):
|
|
249
|
+
"""Average directional SDC over the final correlation-time fraction."""
|
|
250
|
+
if not 0.0 < fraction <= 1.0:
|
|
251
|
+
raise ValueError("SDC averaging fraction must be in (0, 1]")
|
|
252
|
+
start_time = times[0] + (1.0 - fraction) * (times[-1] - times[0])
|
|
253
|
+
mask = times >= start_time
|
|
254
|
+
if np.count_nonzero(mask) < 2:
|
|
255
|
+
mask = np.zeros(len(times), dtype=bool)
|
|
256
|
+
mask[max(0, len(times) // 2):] = True
|
|
257
|
+
averaged = np.mean(sdc_xyz[mask], axis=0)
|
|
258
|
+
diffusion_xyz = averaged * A2_PER_PS_TO_CM2_PER_S
|
|
259
|
+
return np.append(diffusion_xyz, np.mean(diffusion_xyz)), mask, averaged
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def calculate_conductivity(diffusion, num_ions, charge, volume_a3, temperature):
|
|
263
|
+
"""Nernst-Einstein conductivity in mS/cm for D in cm^2/s."""
|
|
264
|
+
number_density_cm3 = int(num_ions) / (float(volume_a3) * 1.0e-24)
|
|
265
|
+
charge_c = float(charge) * ELEMENTARY_CHARGE_C
|
|
266
|
+
factor = (number_density_cm3 * charge_c ** 2
|
|
267
|
+
/ (BOLTZMANN_J_PER_K * float(temperature)))
|
|
268
|
+
return np.asarray(diffusion) * factor * 1000.0
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _safe_name(value):
|
|
272
|
+
return re.sub(r"[^A-Za-z0-9_.+-]+", "_", str(value)).strip("_") or "unknown"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _temperature_label(folder_name, temperature):
|
|
276
|
+
match = re.fullmatch(r"([0-9]+(?:\.[0-9]+)?)K?", str(folder_name), re.I)
|
|
277
|
+
if match:
|
|
278
|
+
value = float(match.group(1))
|
|
279
|
+
return str(int(value)) if value.is_integer() else match.group(1)
|
|
280
|
+
return str(int(round(float(temperature))))
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def save_group_data(output_dir, group_id, element, temp_label,
|
|
284
|
+
times, msd_xyz, sdc_xyz):
|
|
285
|
+
stem = f"group_{group_id}_{_safe_name(element)}_{temp_label}"
|
|
286
|
+
np.savetxt(Path(output_dir) / f"{stem}_msd.txt",
|
|
287
|
+
np.column_stack([times, msd_xyz, np.sum(msd_xyz, axis=1)]),
|
|
288
|
+
fmt="%.8f",
|
|
289
|
+
header="time(ps) MSD_x(A^2) MSD_y(A^2) MSD_z(A^2) MSD_total(A^2)",
|
|
290
|
+
comments="", encoding="utf-8")
|
|
291
|
+
np.savetxt(Path(output_dir) / f"{stem}_sdc.txt",
|
|
292
|
+
np.column_stack([times, sdc_xyz]), fmt="%.8f",
|
|
293
|
+
header="time(ps) SDC_x(A^2/ps) SDC_y(A^2/ps) SDC_z(A^2/ps)",
|
|
294
|
+
comments="", encoding="utf-8")
|
|
295
|
+
return stem
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def save_group_results(output_dir, stem, record, averaged_sdc):
|
|
299
|
+
labels = ("x", "y", "z", "total")
|
|
300
|
+
lines = [
|
|
301
|
+
f"Job: {record['job']}",
|
|
302
|
+
f"Group: {record['group_id']}",
|
|
303
|
+
f"Element: {record['element']}",
|
|
304
|
+
f"Mobile particles: {record['count']}",
|
|
305
|
+
f"Mean temperature (K): {record['temperature']:.8f}",
|
|
306
|
+
f"Ensemble: {record['ensemble']}",
|
|
307
|
+
f"Conductivity volume (A^3): {record['volume']:.8f}",
|
|
308
|
+
f"Volume source: {record['volume_source']}",
|
|
309
|
+
("SDC averaging interval (ps): "
|
|
310
|
+
f"{record['average_start']:.8f} to {record['average_end']:.8f}"),
|
|
311
|
+
"Average SDC (A^2/ps):",
|
|
312
|
+
*(f" SDC_{label} = {value:.8f}"
|
|
313
|
+
for label, value in zip(("x", "y", "z"), averaged_sdc)),
|
|
314
|
+
"Diffusion coefficient (cm^2/s):",
|
|
315
|
+
*(f" D_{label} = {value:.8f}"
|
|
316
|
+
for label, value in zip(labels, record["diffusion"])),
|
|
317
|
+
]
|
|
318
|
+
if record["conductivity"] is not None:
|
|
319
|
+
lines.extend([
|
|
320
|
+
"Ionic conductivity (mS/cm):",
|
|
321
|
+
*(f" sigma_{label} = {value:.8f}"
|
|
322
|
+
for label, value in zip(labels, record["conductivity"])),
|
|
323
|
+
])
|
|
324
|
+
(Path(output_dir) / f"{stem}_results.txt").write_text(
|
|
325
|
+
"\n".join(lines) + "\n", encoding="utf-8")
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def discover_jobs(root):
|
|
329
|
+
"""Recursively find GPUMD MSD job folders at any depth."""
|
|
330
|
+
root = Path(root)
|
|
331
|
+
if not root.is_dir():
|
|
332
|
+
raise SystemExit(f"Error: entry path not found: {root}")
|
|
333
|
+
jobs = []
|
|
334
|
+
for msd_path in root.rglob("msd.out"):
|
|
335
|
+
folder = msd_path.parent
|
|
336
|
+
thermo = _thermo_path(folder)
|
|
337
|
+
if (thermo is not None
|
|
338
|
+
and all((folder / name).is_file() for name in CORE_JOB_FILES)):
|
|
339
|
+
jobs.append((folder, thermo))
|
|
340
|
+
return sorted(jobs, key=lambda item: item[0].relative_to(root).as_posix())
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def analyze_job(root, folder, thermo_path, args):
|
|
344
|
+
spec, runs, target_run = parse_run_file(folder / "run.in")
|
|
345
|
+
temperature, volume, ensemble, volume_source = load_thermo(
|
|
346
|
+
thermo_path, runs, target_run
|
|
347
|
+
)
|
|
348
|
+
groups = load_groups(folder / "model.xyz", spec.group_method)
|
|
349
|
+
blocks = load_msd(folder / "msd.out", spec, groups)
|
|
350
|
+
temp_label = _temperature_label(folder.name, temperature)
|
|
351
|
+
job_label = folder.relative_to(root).as_posix() or "."
|
|
352
|
+
output_dir = folder / "msd"
|
|
353
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
354
|
+
end_time = next(iter(blocks.values()))[0][-1]
|
|
355
|
+
interval_ps = spec.sample_interval * spec.time_step_fs / 1000.0
|
|
356
|
+
if abs(end_time - spec.maximum_time_ps) > max(interval_ps, 1.0e-8):
|
|
357
|
+
print(f" [WARN] {folder}: msd.out ends at {end_time:.8f} ps; "
|
|
358
|
+
f"run.in implies {spec.maximum_time_ps:.8f} ps")
|
|
359
|
+
|
|
360
|
+
records, job_sigma, mobile_found = [], np.zeros(4), False
|
|
361
|
+
for group_id in sorted(blocks):
|
|
362
|
+
times, msd_xyz, sdc_xyz = blocks[group_id]
|
|
363
|
+
info = groups[group_id]
|
|
364
|
+
diffusion, average_mask, averaged_sdc = average_sdc(times, sdc_xyz)
|
|
365
|
+
conductivity = None
|
|
366
|
+
if info["element"] == args.diffuse_ion:
|
|
367
|
+
conductivity = calculate_conductivity(
|
|
368
|
+
diffusion, info["count"], args.ion_charge, volume, temperature)
|
|
369
|
+
job_sigma += conductivity
|
|
370
|
+
mobile_found = True
|
|
371
|
+
stem = save_group_data(output_dir, group_id, info["element"], temp_label,
|
|
372
|
+
times, msd_xyz, sdc_xyz)
|
|
373
|
+
record = {
|
|
374
|
+
"job": job_label, "temperature": temperature, "volume": volume,
|
|
375
|
+
"ensemble": ensemble, "volume_source": volume_source,
|
|
376
|
+
"group_id": group_id, "element": info["element"],
|
|
377
|
+
"count": info["count"],
|
|
378
|
+
"average_start": float(times[average_mask][0]),
|
|
379
|
+
"average_end": float(times[average_mask][-1]),
|
|
380
|
+
"diffusion": diffusion,
|
|
381
|
+
"conductivity": conductivity,
|
|
382
|
+
}
|
|
383
|
+
save_group_results(output_dir, stem, record, averaged_sdc)
|
|
384
|
+
records.append(record)
|
|
385
|
+
sigma_text = (f", total sigma_{args.diffuse_ion}={job_sigma[3]:.8f} mS/cm"
|
|
386
|
+
if mobile_found else f", no {args.diffuse_ion} group")
|
|
387
|
+
print(
|
|
388
|
+
f" {job_label} -> {output_dir} "
|
|
389
|
+
f"({len(records)} group(s), {ensemble}, {volume_source}{sigma_text})"
|
|
390
|
+
)
|
|
391
|
+
return records
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def write_summary(root, records, diffuse_ion):
|
|
395
|
+
columns = [
|
|
396
|
+
"Job", "T(K)", "V(A^3)", "Ensemble", "Volume_source",
|
|
397
|
+
"Group", "Element", "N", "average_start(ps)", "average_end(ps)",
|
|
398
|
+
"D_x(cm^2/s)", "D_y(cm^2/s)", "D_z(cm^2/s)", "D_total(cm^2/s)",
|
|
399
|
+
"sigma_x(mS/cm)", "sigma_y(mS/cm)", "sigma_z(mS/cm)",
|
|
400
|
+
"sigma_total(mS/cm)",
|
|
401
|
+
]
|
|
402
|
+
lines = [
|
|
403
|
+
"GPUMD SDC and Nernst-Einstein conductivity summary",
|
|
404
|
+
f"Mobile ion: {diffuse_ion}",
|
|
405
|
+
"SDC average: arithmetic mean over the last 50% of correlation time",
|
|
406
|
+
"\t".join(columns),
|
|
407
|
+
]
|
|
408
|
+
for record in sorted(records, key=lambda item: (item["job"], item["group_id"])):
|
|
409
|
+
conductivity = record["conductivity"]
|
|
410
|
+
sigma = ([f"{value:.8f}" for value in conductivity]
|
|
411
|
+
if conductivity is not None else ["N/A"] * 4)
|
|
412
|
+
fields = [
|
|
413
|
+
record["job"], f"{record['temperature']:.8f}",
|
|
414
|
+
f"{record['volume']:.8f}", record["ensemble"],
|
|
415
|
+
record["volume_source"], str(record["group_id"]),
|
|
416
|
+
record["element"], str(record["count"]),
|
|
417
|
+
f"{record['average_start']:.8f}",
|
|
418
|
+
f"{record['average_end']:.8f}",
|
|
419
|
+
*(f"{value:.8f}" for value in record["diffusion"]), *sigma,
|
|
420
|
+
]
|
|
421
|
+
lines.append("\t".join(fields))
|
|
422
|
+
(Path(root) / SUMMARY_FILE).write_text(
|
|
423
|
+
"\n".join(lines) + "\n", encoding="utf-8")
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def add_arguments(parser):
|
|
427
|
+
parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter
|
|
428
|
+
parser.add_argument("-entry-path", default=".",
|
|
429
|
+
help="Root recursively scanned for GPUMD MSD jobs.")
|
|
430
|
+
parser.add_argument("-diffuse_ion", default="Li",
|
|
431
|
+
help="Mobile ion used for conductivity.")
|
|
432
|
+
parser.add_argument("-ion_charge", type=int, default=1,
|
|
433
|
+
help="Signed mobile-ion valence Z (the formula uses Z squared).")
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def run(args):
|
|
437
|
+
root = Path(args.entry_path).resolve()
|
|
438
|
+
jobs = discover_jobs(root)
|
|
439
|
+
if not jobs:
|
|
440
|
+
print("No GPUMD MSD jobs found recursively (need msd.out, thermo.out "
|
|
441
|
+
"or thermos.out, model.xyz, and run.in in one folder).")
|
|
442
|
+
return 0
|
|
443
|
+
print(f"Found {len(jobs)} GPUMD MSD job folder(s).")
|
|
444
|
+
records = []
|
|
445
|
+
for folder, thermo_path in jobs:
|
|
446
|
+
try:
|
|
447
|
+
records.extend(analyze_job(root, folder, thermo_path, args))
|
|
448
|
+
except Exception as exc:
|
|
449
|
+
print(f" {folder}: analysis failed ({exc}) -> skipping")
|
|
450
|
+
if records:
|
|
451
|
+
write_summary(root, records, args.diffuse_ion)
|
|
452
|
+
print(f"Wrote {root / SUMMARY_FILE} ({len(records)} group record(s)).")
|
|
453
|
+
return 0
|
|
454
|
+
print("No GPUMD MSD job could be analyzed; summary was not written.")
|
|
455
|
+
return 1
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
if __name__ == "__main__":
|
|
459
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
460
|
+
add_arguments(_parser)
|
|
461
|
+
raise SystemExit(run(_parser.parse_args()))
|