pymdkit 1.4.6__tar.gz → 1.4.8__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.4.6 → pymdkit-1.4.8}/PKG-INFO +35 -7
- {pymdkit-1.4.6 → pymdkit-1.4.8}/README.md +36 -8
- {pymdkit-1.4.6 → pymdkit-1.4.8}/pyproject.toml +1 -1
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/__init__.py +1 -1
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_gpumd.py +6 -7
- pymdkit-1.4.8/src/pymdkit/commands/gather_fs.py +196 -0
- pymdkit-1.4.8/src/pymdkit/commands/gpumd_relax.py +127 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/gpumd_trjcat.py +92 -46
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit.egg-info/PKG-INFO +35 -7
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit.egg-info/SOURCES.txt +2 -1
- pymdkit-1.4.6/src/pymdkit/commands/gather_contcar.py +0 -106
- {pymdkit-1.4.6 → pymdkit-1.4.8}/LICENSE +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/setup.cfg +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/gpumd_thermo.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.4.6 → pymdkit-1.4.8}/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.4.
|
|
3
|
+
Version: 1.4.8
|
|
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
|
|
@@ -65,12 +65,13 @@ directory for job sub-folders automatically.
|
|
|
65
65
|
|---|---|
|
|
66
66
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
67
67
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
68
|
+
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
68
69
|
| `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
|
|
69
70
|
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
|
|
70
71
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
71
|
-
| `gather-
|
|
72
|
+
| `gather-fs` | Recursively gather final structures from converged VASP or GPUMD jobs |
|
|
72
73
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
73
|
-
| `gpumd-trjcat` | Merge
|
|
74
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
74
75
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
75
76
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
76
77
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -106,6 +107,8 @@ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if inpu
|
|
|
106
107
|
# GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
|
|
107
108
|
pmk vasp-relax -i opted.vasp # relax inputs in current dir
|
|
108
109
|
pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
|
|
110
|
+
pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run.in + NEP per job folder
|
|
111
|
+
pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
|
|
109
112
|
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
110
113
|
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
111
114
|
pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
|
|
@@ -114,8 +117,9 @@ pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta
|
|
|
114
117
|
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
115
118
|
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
116
119
|
pmk vasp-fe # scans ./ for VASP jobs -> final-energy.txt with convergence status
|
|
117
|
-
pmk gather-
|
|
118
|
-
pmk gather-
|
|
120
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted
|
|
121
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted -ehull 0.028
|
|
122
|
+
pmk gather-fs -job gpumd -fs-name relaxed.xyz -of gpumd-opted
|
|
119
123
|
pmk vasp2xyz # scans ./ for VASP output folders -> scf-converged.xyz
|
|
120
124
|
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
121
125
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
@@ -137,6 +141,7 @@ pmk gpumd-thermo # current job, or recursively scan
|
|
|
137
141
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
138
142
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
139
143
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
144
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
140
145
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
146
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
142
147
|
|
|
@@ -168,6 +173,29 @@ multi-structure trajectory and writes one job sub-folder per frame
|
|
|
168
173
|
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
|
|
169
174
|
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
|
|
170
175
|
`vasp2xyz`.
|
|
176
|
+
`gpumd-relax` accepts the same `-i`, `-if`, and `-it` structure modes. `-nep`
|
|
177
|
+
is required and the selected potential is copied into every job folder together
|
|
178
|
+
with `model.xyz` and `run.in`. `-in FILE` supplies a custom GPUMD input; its
|
|
179
|
+
first `potential` filename is synchronized to the copied NEP. Without `-in`,
|
|
180
|
+
the generated job-local input is:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
potential <NEP filename>
|
|
184
|
+
|
|
185
|
+
minimize fire 2.0e-2 1000000
|
|
186
|
+
|
|
187
|
+
ensemble nve
|
|
188
|
+
dump_xyz -1 0 1 relaxed.xyz force
|
|
189
|
+
run 1
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`gather-fs` recursively scans all job folders below the current directory. For
|
|
193
|
+
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
194
|
+
requires full convergence before copying the selected `-fs-name` as `.vasp`.
|
|
195
|
+
For GPUMD it requires `gpumd.out` to report a force tolerance and a final
|
|
196
|
+
`f_max` no greater than that tolerance before copying the final structure.
|
|
197
|
+
`-fs-name` defaults to `CONTCAR` for VASP and `relaxed.xyz` for GPUMD. The
|
|
198
|
+
`-ehull` and `-ehull-file` filters are available only with `-job vasp`.
|
|
171
199
|
|
|
172
200
|
`add-config-type` updates its input atomically in place. With `-i`, the file must contain exactly one XYZ structure; use `-it` for a trajectory. Existing `Config_type` values are replaced, missing values are added, and all other extended-XYZ bytes remain unchanged.
|
|
173
201
|
|
|
@@ -177,11 +205,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
205
|
|
|
178
206
|
For folder conversion, `convert -if INPUT -of OUTPUT` writes `.xyz` by default. Set another target extension with `-oe`, for example `-oe cif` or `-oe vasp`. Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
179
207
|
|
|
180
|
-
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two
|
|
208
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. An abnormal classification always requires a mapped potential-energy change. When at least two lattice-parameter or volume signals show the same mapped transition, they may enable a more sensitive noise-relative energy test; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Cell signals can corroborate and refine the energy interval, but can never classify a run by themselves. Temperature and all six pressure components remain diagnostics only and cannot classify an otherwise stationary run as abnormal by themselves. A lone cell-axis drift, an isolated spike, or an empty anomaly mask never triggers a whole-run fallback. Thermo plots use Arial when available and Matplotlib's built-in qualitative `Dark2` color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and show abnormal regions as shaded legend entries on potential energy, lattice parameters, and volume. Potential energy and volume are divided by 1000 and labeled ×10³ only when their absolute plotted values reach 1000. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
|
|
181
209
|
|
|
182
210
|
The thermo parser follows the [official GPUMD thermo.out format](https://gpumd.org/gpumd/output_files/thermo_out.html): 18 columns are required, `pressure.txt` exports `Pxx Pyy Pzz Pyz Pxz Pxy`, box vectors are interpreted as a full 3×3 matrix, `time_step` propagates between runs, and `dump_thermo` does not.
|
|
183
211
|
|
|
184
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
212
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. By default, normal trajectories are merged in deterministic folder order into `train.xyz` (or `-o`), while only frames from the earliest mapped abnormal interval of each abnormal job are written to `abnormal.xyz` (or `-ao`). With `-all`, every readable full trajectory is merged into `train.xyz` regardless of normal, abnormal, missing-thermo, or failed-thermo status; an analyzable abnormal job still contributes its earliest abnormal interval to `abnormal.xyz`. If no mapped abnormal frames are found, `abnormal.xyz` is not created and a stale file at the selected `-ao` path is removed. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. In default mode, missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
|
|
185
213
|
|
|
186
214
|
`trj-sparse` keeps `max(1, floor(frame_count × ratio))` frames without rewriting their extended-XYZ content. Selection is deterministic and starts with frame zero; for 100 frames and `-r 0.5`, it writes source indices `0, 2, ..., 98`.
|
|
187
215
|
|
|
@@ -39,12 +39,13 @@ directory for job sub-folders automatically.
|
|
|
39
39
|
|---|---|
|
|
40
40
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
41
41
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
42
|
+
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
42
43
|
| `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
|
|
43
44
|
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
|
|
44
45
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
45
|
-
| `gather-
|
|
46
|
+
| `gather-fs` | Recursively gather final structures from converged VASP or GPUMD jobs |
|
|
46
47
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
47
|
-
| `gpumd-trjcat` | Merge
|
|
48
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
48
49
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
49
50
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
50
51
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -80,6 +81,8 @@ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if inpu
|
|
|
80
81
|
# GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
|
|
81
82
|
pmk vasp-relax -i opted.vasp # relax inputs in current dir
|
|
82
83
|
pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
|
|
84
|
+
pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run.in + NEP per job folder
|
|
85
|
+
pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
|
|
83
86
|
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
84
87
|
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
85
88
|
pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
|
|
@@ -88,8 +91,9 @@ pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta
|
|
|
88
91
|
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
89
92
|
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
90
93
|
pmk vasp-fe # scans ./ for VASP jobs -> final-energy.txt with convergence status
|
|
91
|
-
pmk gather-
|
|
92
|
-
pmk gather-
|
|
94
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted
|
|
95
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted -ehull 0.028
|
|
96
|
+
pmk gather-fs -job gpumd -fs-name relaxed.xyz -of gpumd-opted
|
|
93
97
|
pmk vasp2xyz # scans ./ for VASP output folders -> scf-converged.xyz
|
|
94
98
|
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
95
99
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
@@ -110,7 +114,8 @@ pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
|
|
|
110
114
|
pmk gpumd-thermo # current job, or recursively scan job folders
|
|
111
115
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
112
116
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
113
|
-
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
117
|
+
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
118
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
114
119
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
115
120
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
116
121
|
|
|
@@ -141,7 +146,30 @@ custom_settings = {
|
|
|
141
146
|
multi-structure trajectory and writes one job sub-folder per frame
|
|
142
147
|
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
|
|
143
148
|
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
|
|
144
|
-
`vasp2xyz`.
|
|
149
|
+
`vasp2xyz`.
|
|
150
|
+
`gpumd-relax` accepts the same `-i`, `-if`, and `-it` structure modes. `-nep`
|
|
151
|
+
is required and the selected potential is copied into every job folder together
|
|
152
|
+
with `model.xyz` and `run.in`. `-in FILE` supplies a custom GPUMD input; its
|
|
153
|
+
first `potential` filename is synchronized to the copied NEP. Without `-in`,
|
|
154
|
+
the generated job-local input is:
|
|
155
|
+
|
|
156
|
+
```text
|
|
157
|
+
potential <NEP filename>
|
|
158
|
+
|
|
159
|
+
minimize fire 2.0e-2 1000000
|
|
160
|
+
|
|
161
|
+
ensemble nve
|
|
162
|
+
dump_xyz -1 0 1 relaxed.xyz force
|
|
163
|
+
run 1
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`gather-fs` recursively scans all job folders below the current directory. For
|
|
167
|
+
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
168
|
+
requires full convergence before copying the selected `-fs-name` as `.vasp`.
|
|
169
|
+
For GPUMD it requires `gpumd.out` to report a force tolerance and a final
|
|
170
|
+
`f_max` no greater than that tolerance before copying the final structure.
|
|
171
|
+
`-fs-name` defaults to `CONTCAR` for VASP and `relaxed.xyz` for GPUMD. The
|
|
172
|
+
`-ehull` and `-ehull-file` filters are available only with `-job vasp`.
|
|
145
173
|
|
|
146
174
|
`add-config-type` updates its input atomically in place. With `-i`, the file must contain exactly one XYZ structure; use `-it` for a trajectory. Existing `Config_type` values are replaced, missing values are added, and all other extended-XYZ bytes remain unchanged.
|
|
147
175
|
|
|
@@ -151,11 +179,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
151
179
|
|
|
152
180
|
For folder conversion, `convert -if INPUT -of OUTPUT` writes `.xyz` by default. Set another target extension with `-oe`, for example `-oe cif` or `-oe vasp`. Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
153
181
|
|
|
154
|
-
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two
|
|
182
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. An abnormal classification always requires a mapped potential-energy change. When at least two lattice-parameter or volume signals show the same mapped transition, they may enable a more sensitive noise-relative energy test; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Cell signals can corroborate and refine the energy interval, but can never classify a run by themselves. Temperature and all six pressure components remain diagnostics only and cannot classify an otherwise stationary run as abnormal by themselves. A lone cell-axis drift, an isolated spike, or an empty anomaly mask never triggers a whole-run fallback. Thermo plots use Arial when available and Matplotlib's built-in qualitative `Dark2` color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and show abnormal regions as shaded legend entries on potential energy, lattice parameters, and volume. Potential energy and volume are divided by 1000 and labeled ×10³ only when their absolute plotted values reach 1000. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
|
|
155
183
|
|
|
156
184
|
The thermo parser follows the [official GPUMD thermo.out format](https://gpumd.org/gpumd/output_files/thermo_out.html): 18 columns are required, `pressure.txt` exports `Pxx Pyy Pzz Pyz Pxz Pxy`, box vectors are interpreted as a full 3×3 matrix, `time_step` propagates between runs, and `dump_thermo` does not.
|
|
157
185
|
|
|
158
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
186
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. By default, normal trajectories are merged in deterministic folder order into `train.xyz` (or `-o`), while only frames from the earliest mapped abnormal interval of each abnormal job are written to `abnormal.xyz` (or `-ao`). With `-all`, every readable full trajectory is merged into `train.xyz` regardless of normal, abnormal, missing-thermo, or failed-thermo status; an analyzable abnormal job still contributes its earliest abnormal interval to `abnormal.xyz`. If no mapped abnormal frames are found, `abnormal.xyz` is not created and a stale file at the selected `-ao` path is removed. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. In default mode, missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
|
|
159
187
|
|
|
160
188
|
`trj-sparse` keeps `max(1, floor(frame_count × ratio))` frames without rewriting their extended-XYZ content. Selection is deterministic and starts with frame zero; for 100 frames and `-r 0.5`, it writes source indices `0, 2, ..., 98`.
|
|
161
189
|
|
|
@@ -389,8 +389,8 @@ def load_thermo(thermo_path, run_path):
|
|
|
389
389
|
segment_mask = potential_abnormal_mask(potential[start:])
|
|
390
390
|
warning_prefix = "potential-energy: localized"
|
|
391
391
|
|
|
392
|
-
#
|
|
393
|
-
#
|
|
392
|
+
# Potential energy is mandatory evidence. Coherent cell changes may lower
|
|
393
|
+
# its threshold and refine the interval, but cannot classify a run alone.
|
|
394
394
|
if not segment_mask.any() and len(cell_warnings) >= 2:
|
|
395
395
|
structural_mask = structural_abnormal_mask(
|
|
396
396
|
lengths[start:], volume[start:]
|
|
@@ -401,13 +401,12 @@ def load_thermo(thermo_path, run_path):
|
|
|
401
401
|
)
|
|
402
402
|
overlap = relaxed_energy & structural_mask
|
|
403
403
|
if overlap.any():
|
|
404
|
-
|
|
404
|
+
corroborated = _keep_major_regions(
|
|
405
405
|
_bridge_mask(overlap, max(1, segment_length // 20))
|
|
406
406
|
)
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
warning_prefix = "structure: coherently mapped"
|
|
407
|
+
if corroborated.any():
|
|
408
|
+
segment_mask = corroborated
|
|
409
|
+
warning_prefix = "potential-energy: structurally corroborated"
|
|
411
410
|
|
|
412
411
|
abnormal_mask = np.zeros(len(data), dtype=bool)
|
|
413
412
|
abnormal_mask[start:] = segment_mask
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Gather converged final structures from recursive VASP or GPUMD jobs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
from . import _fileio, _vasp_output
|
|
13
|
+
except ImportError:
|
|
14
|
+
import _fileio
|
|
15
|
+
import _vasp_output
|
|
16
|
+
|
|
17
|
+
COMMAND = "gather-fs"
|
|
18
|
+
HELP = "Gather final structures from converged VASP or GPUMD jobs."
|
|
19
|
+
|
|
20
|
+
_NUMBER = r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[Ee][+-]?\d+)?"
|
|
21
|
+
_TOLERANCE_RE = re.compile(
|
|
22
|
+
rf"force\s+tolerance\s+of\s+({_NUMBER})\s*eV\s*/\s*(?:A|Angstrom)",
|
|
23
|
+
re.IGNORECASE,
|
|
24
|
+
)
|
|
25
|
+
_FMAX_RE = re.compile(
|
|
26
|
+
rf"f_max\s*=\s*({_NUMBER})\s*eV\s*/\s*(?:A|Angstrom)",
|
|
27
|
+
re.IGNORECASE,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_ehull_file(path):
|
|
32
|
+
result = {}
|
|
33
|
+
for line in Path(path).read_text(encoding="utf-8").splitlines():
|
|
34
|
+
parts = line.split()
|
|
35
|
+
if len(parts) < 2:
|
|
36
|
+
continue
|
|
37
|
+
try:
|
|
38
|
+
result[parts[0]] = float(parts[-1])
|
|
39
|
+
except ValueError:
|
|
40
|
+
continue
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def gpumd_convergence(path):
|
|
45
|
+
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
46
|
+
tolerances = [float(value) for value in _TOLERANCE_RE.findall(text)]
|
|
47
|
+
maxima = [float(value) for value in _FMAX_RE.findall(text)]
|
|
48
|
+
if not tolerances or not maxima:
|
|
49
|
+
return False, "force tolerance or final f_max not found"
|
|
50
|
+
tolerance = tolerances[-1]
|
|
51
|
+
f_max = maxima[-1]
|
|
52
|
+
converged = f_max <= tolerance
|
|
53
|
+
detail = f"f_max={f_max:.10g} eV/A, tolerance={tolerance:.10g} eV/A"
|
|
54
|
+
return converged, detail
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def vasp_convergence(folder):
|
|
58
|
+
output = _vasp_output.read_vasp_output(folder, require=())
|
|
59
|
+
if output is None:
|
|
60
|
+
return False, "no readable vaspout.h5, vasprun.xml, or OUTCAR"
|
|
61
|
+
return output.converged, f"source={output.source}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
65
|
+
parser.add_argument("-job", required=True, choices=("vasp", "gpumd"),
|
|
66
|
+
help="Job type used for convergence checking.")
|
|
67
|
+
parser.add_argument("-fs-name", dest="final_structure_name",
|
|
68
|
+
help="Final-structure filename (defaults: CONTCAR or relaxed.xyz).")
|
|
69
|
+
parser.add_argument("-of", "-output-folder", dest="output_folder",
|
|
70
|
+
required=True, metavar="DIR",
|
|
71
|
+
help="Folder receiving the gathered final structures.")
|
|
72
|
+
parser.add_argument("-ehull", type=float, default=None, metavar="EV",
|
|
73
|
+
help="VASP only: require E_hull below this eV/atom value.")
|
|
74
|
+
parser.add_argument("-ehull-file", default="ehull.txt",
|
|
75
|
+
help="VASP E_hull report used with -ehull (default: ehull.txt).")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def discover_final_structures(root, filename, output_folder):
|
|
79
|
+
root = Path(root).resolve()
|
|
80
|
+
output_folder = Path(output_folder).resolve()
|
|
81
|
+
paths = []
|
|
82
|
+
for path in root.rglob(filename):
|
|
83
|
+
if not path.is_file() or path.parent.resolve() == root:
|
|
84
|
+
continue
|
|
85
|
+
resolved = path.resolve()
|
|
86
|
+
if resolved == output_folder or output_folder in resolved.parents:
|
|
87
|
+
continue
|
|
88
|
+
paths.append(path)
|
|
89
|
+
return sorted(
|
|
90
|
+
paths,
|
|
91
|
+
key=lambda path: _fileio.folder_sort_key(
|
|
92
|
+
str(path.parent.relative_to(root))
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def ehull_value(ehull_map, folder, root):
|
|
98
|
+
relative = str(folder.relative_to(root)).replace("\\", "/")
|
|
99
|
+
for key in (relative, folder.name):
|
|
100
|
+
if key in ehull_map:
|
|
101
|
+
return ehull_map[key]
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def destination_name(path, root, duplicate_names, job):
|
|
106
|
+
relative_folder = path.parent.relative_to(root)
|
|
107
|
+
if duplicate_names[path.parent.name] > 1:
|
|
108
|
+
stem = "__".join(relative_folder.parts)
|
|
109
|
+
else:
|
|
110
|
+
stem = path.parent.name
|
|
111
|
+
suffix = ".vasp" if job == "vasp" else (path.suffix or ".xyz")
|
|
112
|
+
return stem + suffix
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def run(args) -> int:
|
|
116
|
+
if args.job == "gpumd" and args.ehull is not None:
|
|
117
|
+
raise SystemExit("Error: -ehull is only available with -job vasp.")
|
|
118
|
+
|
|
119
|
+
final_name = args.final_structure_name or (
|
|
120
|
+
"CONTCAR" if args.job == "vasp" else "relaxed.xyz"
|
|
121
|
+
)
|
|
122
|
+
if Path(final_name).name != final_name:
|
|
123
|
+
raise SystemExit("Error: -fs-name must be a filename, not a path.")
|
|
124
|
+
|
|
125
|
+
root = Path(".").resolve()
|
|
126
|
+
output_folder = Path(args.output_folder).resolve()
|
|
127
|
+
candidates = discover_final_structures(root, final_name, output_folder)
|
|
128
|
+
duplicate_names = Counter(path.parent.name for path in candidates)
|
|
129
|
+
|
|
130
|
+
ehull_map = None
|
|
131
|
+
if args.ehull is not None:
|
|
132
|
+
report = Path(args.ehull_file)
|
|
133
|
+
if not report.is_file():
|
|
134
|
+
raise SystemExit(f"Error: {report} not found (needed for -ehull).")
|
|
135
|
+
ehull_map = parse_ehull_file(report)
|
|
136
|
+
|
|
137
|
+
output_folder.mkdir(parents=True, exist_ok=True)
|
|
138
|
+
gathered = 0
|
|
139
|
+
unconverged = 0
|
|
140
|
+
filtered = 0
|
|
141
|
+
missing_ehull = 0
|
|
142
|
+
for final_structure in candidates:
|
|
143
|
+
folder = final_structure.parent.resolve()
|
|
144
|
+
if args.job == "vasp":
|
|
145
|
+
converged, detail = vasp_convergence(folder)
|
|
146
|
+
else:
|
|
147
|
+
gpumd_output = folder / "gpumd.out"
|
|
148
|
+
if not gpumd_output.is_file():
|
|
149
|
+
converged, detail = False, "gpumd.out not found"
|
|
150
|
+
else:
|
|
151
|
+
converged, detail = gpumd_convergence(gpumd_output)
|
|
152
|
+
|
|
153
|
+
relative = folder.relative_to(root)
|
|
154
|
+
if not converged:
|
|
155
|
+
unconverged += 1
|
|
156
|
+
print(f"{relative}: skipped - not converged ({detail})")
|
|
157
|
+
continue
|
|
158
|
+
|
|
159
|
+
tag = ""
|
|
160
|
+
if ehull_map is not None:
|
|
161
|
+
value = ehull_value(ehull_map, folder, root)
|
|
162
|
+
if value is None:
|
|
163
|
+
missing_ehull += 1
|
|
164
|
+
print(f"{relative}: skipped - no E_hull entry")
|
|
165
|
+
continue
|
|
166
|
+
if not value < args.ehull:
|
|
167
|
+
filtered += 1
|
|
168
|
+
print(f"{relative}: skipped - E_hull={value:.6g} eV/atom")
|
|
169
|
+
continue
|
|
170
|
+
tag = f" (E_hull={value:.6g} eV/atom)"
|
|
171
|
+
|
|
172
|
+
destination = output_folder / destination_name(
|
|
173
|
+
final_structure, root, duplicate_names, args.job
|
|
174
|
+
)
|
|
175
|
+
shutil.copy2(final_structure, destination)
|
|
176
|
+
print(f"{final_structure} -> {destination}{tag}")
|
|
177
|
+
gathered += 1
|
|
178
|
+
|
|
179
|
+
print(
|
|
180
|
+
f"Done: gathered {gathered} converged {args.job.upper()} final "
|
|
181
|
+
f"structure(s) into {output_folder}"
|
|
182
|
+
)
|
|
183
|
+
if unconverged:
|
|
184
|
+
print(f"Skipped {unconverged} unconverged or unreadable job(s).")
|
|
185
|
+
if missing_ehull or filtered:
|
|
186
|
+
print(
|
|
187
|
+
f"E_hull filtering skipped {missing_ehull} missing and "
|
|
188
|
+
f"{filtered} above-threshold job(s)."
|
|
189
|
+
)
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
195
|
+
add_arguments(_parser)
|
|
196
|
+
raise SystemExit(run(_parser.parse_args()))
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Write GPUMD energy-minimization jobs from structures or trajectories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from . import _fileio, convert
|
|
12
|
+
except ImportError:
|
|
13
|
+
import _fileio
|
|
14
|
+
import convert
|
|
15
|
+
|
|
16
|
+
COMMAND = "gpumd-relax"
|
|
17
|
+
HELP = "Write GPUMD energy-minimization jobs for a file, folder, or trajectory."
|
|
18
|
+
|
|
19
|
+
DEFAULT_RUN_INPUT = """potential {nep_name}
|
|
20
|
+
|
|
21
|
+
minimize fire 2.0e-2 1000000
|
|
22
|
+
|
|
23
|
+
ensemble nve
|
|
24
|
+
dump_xyz -1 0 1 relaxed.xyz force
|
|
25
|
+
run 1
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
30
|
+
parser.add_argument("-i", "-input", dest="input",
|
|
31
|
+
help="Single input structure file.")
|
|
32
|
+
parser.add_argument("-o", "-output", dest="output", default=".",
|
|
33
|
+
help="Output job folder for -i (default: current directory).")
|
|
34
|
+
parser.add_argument("-if", "-input-folder", dest="input_folder",
|
|
35
|
+
help="Folder of structures; one ./<name>/ job per file.")
|
|
36
|
+
parser.add_argument("-it", "-input-trajectory", dest="input_trajectory",
|
|
37
|
+
help="Trajectory; one ./frame_N/ job per frame.")
|
|
38
|
+
parser.add_argument("-frame-prefix", dest="frame_prefix", default="frame",
|
|
39
|
+
help="Trajectory job-folder prefix (default: frame).")
|
|
40
|
+
parser.add_argument("-in", dest="run_input",
|
|
41
|
+
help="Custom GPUMD run input (default: built-in relaxation run.in).")
|
|
42
|
+
parser.add_argument("-nep", required=True,
|
|
43
|
+
help="NEP potential file copied into every job folder.")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def prepare_run_input(path, nep_name):
|
|
47
|
+
if path is None:
|
|
48
|
+
return DEFAULT_RUN_INPUT.format(nep_name=nep_name)
|
|
49
|
+
|
|
50
|
+
source = Path(path)
|
|
51
|
+
if not source.is_file():
|
|
52
|
+
raise SystemExit(f"Error: run input not found: {source}")
|
|
53
|
+
text = source.read_text(encoding="utf-8")
|
|
54
|
+
pattern = re.compile(r"^(\s*potential\s+)(\S+)(.*)$", re.MULTILINE)
|
|
55
|
+
if pattern.search(text):
|
|
56
|
+
text = pattern.sub(
|
|
57
|
+
lambda match: match.group(1) + nep_name + match.group(3),
|
|
58
|
+
text,
|
|
59
|
+
count=1,
|
|
60
|
+
)
|
|
61
|
+
else:
|
|
62
|
+
text = f"potential {nep_name}\n\n{text}"
|
|
63
|
+
return text.rstrip() + "\n"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def write_job(atoms, folder, nep_source, run_text):
|
|
67
|
+
folder = Path(folder)
|
|
68
|
+
folder.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
convert._write(folder / "model.xyz", atoms)
|
|
70
|
+
|
|
71
|
+
nep_destination = folder / nep_source.name
|
|
72
|
+
if nep_source.resolve() != nep_destination.resolve():
|
|
73
|
+
shutil.copy2(nep_source, nep_destination)
|
|
74
|
+
with (folder / "run.in").open("w", encoding="utf-8", newline="\n") as handle:
|
|
75
|
+
handle.write(run_text)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run(args) -> int:
|
|
79
|
+
modes = [bool(args.input), bool(args.input_folder), bool(args.input_trajectory)]
|
|
80
|
+
if sum(modes) != 1:
|
|
81
|
+
raise SystemExit("Error: provide exactly one of -i, -if, or -it.")
|
|
82
|
+
|
|
83
|
+
nep_source = Path(args.nep)
|
|
84
|
+
if not nep_source.is_file():
|
|
85
|
+
raise SystemExit(f"Error: NEP potential not found: {nep_source}")
|
|
86
|
+
run_text = prepare_run_input(args.run_input, nep_source.name)
|
|
87
|
+
|
|
88
|
+
jobs = []
|
|
89
|
+
if args.input:
|
|
90
|
+
source = Path(args.input)
|
|
91
|
+
if not source.is_file():
|
|
92
|
+
raise SystemExit(f"Error: input structure not found: {source}")
|
|
93
|
+
jobs.append((convert._read(source), Path(args.output), str(source)))
|
|
94
|
+
elif args.input_folder:
|
|
95
|
+
for source in _fileio.structure_files(args.input_folder):
|
|
96
|
+
jobs.append((convert._read(source), Path(source.stem), str(source)))
|
|
97
|
+
else:
|
|
98
|
+
trajectory = Path(args.input_trajectory)
|
|
99
|
+
if not trajectory.is_file():
|
|
100
|
+
raise SystemExit(f"Error: input trajectory not found: {trajectory}")
|
|
101
|
+
frames = convert._read(trajectory, all_frames=True)
|
|
102
|
+
if not isinstance(frames, list):
|
|
103
|
+
frames = [frames]
|
|
104
|
+
jobs.extend(
|
|
105
|
+
(atoms, Path(f"{args.frame_prefix}_{index}"),
|
|
106
|
+
f"{trajectory}[{index}]")
|
|
107
|
+
for index, atoms in enumerate(frames, start=1)
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
written = 0
|
|
111
|
+
for atoms, folder, label in jobs:
|
|
112
|
+
try:
|
|
113
|
+
write_job(atoms, folder, nep_source, run_text)
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
print(f"{label}: cannot prepare job ({exc}) -> skipping")
|
|
116
|
+
continue
|
|
117
|
+
print(f"{label} -> {folder}/")
|
|
118
|
+
written += 1
|
|
119
|
+
|
|
120
|
+
print(f"Done: wrote {written} GPUMD relaxation job folder(s).")
|
|
121
|
+
return 0 if written else 1
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
126
|
+
add_arguments(_parser)
|
|
127
|
+
raise SystemExit(run(_parser.parse_args()))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Merge
|
|
1
|
+
"""Merge GPUMD trajectories and extract each job's first abnormal region."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
@@ -16,7 +16,7 @@ except ImportError:
|
|
|
16
16
|
from _gpumd import load_thermo, mask_intervals, trajectory_interval
|
|
17
17
|
|
|
18
18
|
COMMAND = "gpumd-trjcat"
|
|
19
|
-
HELP = "Merge
|
|
19
|
+
HELP = "Merge normal or all GPUMD trajectories; extract first abnormal regions."
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
@dataclass(frozen=True)
|
|
@@ -44,6 +44,11 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
|
44
44
|
help="Structure filename supplying Config_type.")
|
|
45
45
|
|
|
46
46
|
|
|
47
|
+
parser.add_argument(
|
|
48
|
+
"-all", dest="all", action="store_true",
|
|
49
|
+
help="Merge every readable trajectory, regardless of thermo status.",
|
|
50
|
+
)
|
|
51
|
+
|
|
47
52
|
def find_trajectories(root, input_name, outputs):
|
|
48
53
|
root = Path(root).resolve()
|
|
49
54
|
excluded = {Path(path).resolve() for path in outputs}
|
|
@@ -60,18 +65,6 @@ def find_trajectories(root, input_name, outputs):
|
|
|
60
65
|
|
|
61
66
|
def inspect_job(trajectory, args):
|
|
62
67
|
folder = trajectory.parent
|
|
63
|
-
thermo_path = folder / Path(args.thermo).name
|
|
64
|
-
run_path = folder / Path(args.run_input).name
|
|
65
|
-
missing = [path.name for path in (thermo_path, run_path) if not path.is_file()]
|
|
66
|
-
if missing:
|
|
67
|
-
return None, None, f"missing {', '.join(missing)}"
|
|
68
|
-
|
|
69
|
-
try:
|
|
70
|
-
thermo = load_thermo(thermo_path, run_path)
|
|
71
|
-
_, _, interval_ps = trajectory_interval(run_path, trajectory)
|
|
72
|
-
except Exception as exc:
|
|
73
|
-
return None, None, f"thermo check failed ({exc})"
|
|
74
|
-
|
|
75
68
|
try:
|
|
76
69
|
blocks, _ = read_blocks(trajectory)
|
|
77
70
|
except Exception as exc:
|
|
@@ -88,21 +81,31 @@ def inspect_job(trajectory, args):
|
|
|
88
81
|
print(f"{folder}: warning: could not read Config_type ({exc})")
|
|
89
82
|
|
|
90
83
|
job = TrajectoryJob(trajectory, len(blocks), config_type)
|
|
91
|
-
|
|
84
|
+
thermo_path = folder / Path(args.thermo).name
|
|
85
|
+
run_path = folder / Path(args.run_input).name
|
|
86
|
+
missing = [path.name for path in (thermo_path, run_path) if not path.is_file()]
|
|
87
|
+
if missing:
|
|
88
|
+
return job, None, f"missing {', '.join(missing)}"
|
|
92
89
|
|
|
90
|
+
try:
|
|
91
|
+
thermo = load_thermo(thermo_path, run_path)
|
|
92
|
+
_, _, interval_ps = trajectory_interval(run_path, trajectory)
|
|
93
|
+
except Exception as exc:
|
|
94
|
+
return job, None, f"thermo check failed ({exc})"
|
|
95
|
+
return job, (thermo, interval_ps), None
|
|
93
96
|
|
|
94
97
|
def abnormal_frame_indices(thermo, frame_count, interval_ps):
|
|
95
98
|
intervals = mask_intervals(thermo.time, thermo.abnormal_mask)
|
|
99
|
+
if not intervals:
|
|
100
|
+
return ()
|
|
101
|
+
|
|
102
|
+
begin, end = intervals[0]
|
|
96
103
|
tolerance = max(1e-12, interval_ps * 1e-9)
|
|
97
104
|
return tuple(
|
|
98
105
|
index for index in range(frame_count)
|
|
99
|
-
if
|
|
100
|
-
begin - tolerance <= index * interval_ps <= end + tolerance
|
|
101
|
-
for begin, end in intervals
|
|
102
|
-
)
|
|
106
|
+
if begin - tolerance <= index * interval_ps <= end + tolerance
|
|
103
107
|
)
|
|
104
108
|
|
|
105
|
-
|
|
106
109
|
def write_merged(output, jobs):
|
|
107
110
|
output = Path(output)
|
|
108
111
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
@@ -138,60 +141,103 @@ def run(args) -> int:
|
|
|
138
141
|
f"Error: no {Path(args.input).name} files found in subfolders of {root}."
|
|
139
142
|
)
|
|
140
143
|
|
|
141
|
-
|
|
144
|
+
merged_jobs = []
|
|
142
145
|
abnormal_jobs = []
|
|
143
146
|
skipped = 0
|
|
144
147
|
print(f"Found {len(trajectories)} trajectory file(s).")
|
|
145
148
|
for trajectory in trajectories:
|
|
146
149
|
job, analysis, reason = inspect_job(trajectory, args)
|
|
147
150
|
relative_folder = trajectory.parent.relative_to(root)
|
|
148
|
-
if
|
|
151
|
+
if job is None:
|
|
149
152
|
skipped += 1
|
|
150
153
|
print(f"{relative_folder}: skipped - {reason}")
|
|
151
154
|
continue
|
|
152
155
|
|
|
153
|
-
thermo, interval_ps = analysis
|
|
154
156
|
config = (
|
|
155
157
|
f", Config_type={job.config_type}" if job.config_type is not None else ""
|
|
156
158
|
)
|
|
159
|
+
if analysis is None:
|
|
160
|
+
if args.all:
|
|
161
|
+
merged_jobs.append(job)
|
|
162
|
+
print(
|
|
163
|
+
f"{relative_folder}: included by -all - {job.frame_count} "
|
|
164
|
+
f"frame(s){config}; thermo not checked ({reason})"
|
|
165
|
+
)
|
|
166
|
+
else:
|
|
167
|
+
skipped += 1
|
|
168
|
+
print(f"{relative_folder}: skipped - {reason}")
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
thermo, interval_ps = analysis
|
|
157
172
|
if not thermo.warnings:
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
merged_jobs.append(job)
|
|
174
|
+
status = "included by -all (normal)" if args.all else "stable"
|
|
175
|
+
print(
|
|
176
|
+
f"{relative_folder}: {status} - {job.frame_count} frame(s){config}"
|
|
177
|
+
)
|
|
160
178
|
continue
|
|
161
179
|
|
|
180
|
+
if args.all:
|
|
181
|
+
merged_jobs.append(job)
|
|
162
182
|
indices = abnormal_frame_indices(thermo, job.frame_count, interval_ps)
|
|
163
183
|
details = "; ".join(thermo.warnings)
|
|
164
184
|
if not indices:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
if args.all:
|
|
186
|
+
print(
|
|
187
|
+
f"{relative_folder}: included by -all - {job.frame_count} "
|
|
188
|
+
f"frame(s){config}; abnormal interval could not be mapped - {details}"
|
|
189
|
+
)
|
|
190
|
+
else:
|
|
191
|
+
skipped += 1
|
|
192
|
+
print(
|
|
193
|
+
f"{relative_folder}: abnormal but no potential-energy region "
|
|
194
|
+
f"mapped to trajectory frames - {details}"
|
|
195
|
+
)
|
|
170
196
|
continue
|
|
197
|
+
|
|
171
198
|
abnormal_jobs.append(TrajectoryJob(
|
|
172
199
|
job.trajectory, job.frame_count, job.config_type, indices
|
|
173
200
|
))
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
201
|
+
if args.all:
|
|
202
|
+
print(
|
|
203
|
+
f"{relative_folder}: included by -all (abnormal) - all "
|
|
204
|
+
f"{job.frame_count} frame(s){config}; extracted {len(indices)} "
|
|
205
|
+
f"frame(s) from the first abnormal region - {details}"
|
|
206
|
+
)
|
|
207
|
+
else:
|
|
208
|
+
print(
|
|
209
|
+
f"{relative_folder}: abnormal - extracted {len(indices)} of "
|
|
210
|
+
f"{job.frame_count} frame(s) from the first abnormal region"
|
|
211
|
+
f"{config} - {details}"
|
|
212
|
+
)
|
|
178
213
|
|
|
179
|
-
write_merged(output,
|
|
180
|
-
|
|
181
|
-
|
|
214
|
+
write_merged(output, merged_jobs)
|
|
215
|
+
if abnormal_jobs:
|
|
216
|
+
write_merged(abnormal_output, abnormal_jobs)
|
|
217
|
+
else:
|
|
218
|
+
abnormal_output.unlink(missing_ok=True)
|
|
219
|
+
merged_frames = sum(job.frame_count for job in merged_jobs)
|
|
182
220
|
abnormal_frames = sum(len(job.indices) for job in abnormal_jobs)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
if args.all:
|
|
222
|
+
print(
|
|
223
|
+
f"Wrote {merged_frames} frame(s) from {len(merged_jobs)} readable "
|
|
224
|
+
f"job(s) to {args.output}"
|
|
225
|
+
)
|
|
226
|
+
else:
|
|
227
|
+
print(
|
|
228
|
+
f"Wrote {merged_frames} frame(s) from {len(merged_jobs)} stable "
|
|
229
|
+
f"job(s) to {args.output}"
|
|
230
|
+
)
|
|
231
|
+
if abnormal_jobs:
|
|
232
|
+
print(
|
|
233
|
+
f"Wrote {abnormal_frames} frame(s) from {len(abnormal_jobs)} "
|
|
234
|
+
f"abnormal job(s) to {args.abnormal_output}"
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
print(f"No abnormal frames found; did not write {args.abnormal_output}")
|
|
191
238
|
print(f"Skipped {skipped} unreadable or unmappable job(s).")
|
|
192
239
|
return 0
|
|
193
240
|
|
|
194
|
-
|
|
195
241
|
if __name__ == "__main__":
|
|
196
242
|
_parser = argparse.ArgumentParser(description=__doc__)
|
|
197
243
|
add_arguments(_parser)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.8
|
|
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
|
|
@@ -65,12 +65,13 @@ directory for job sub-folders automatically.
|
|
|
65
65
|
|---|---|
|
|
66
66
|
| `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
|
|
67
67
|
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
68
|
+
| `gpumd-relax` | Write GPUMD energy-minimization jobs for a file, folder, or trajectory |
|
|
68
69
|
| `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
|
|
69
70
|
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
|
|
70
71
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
71
|
-
| `gather-
|
|
72
|
+
| `gather-fs` | Recursively gather final structures from converged VASP or GPUMD jobs |
|
|
72
73
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
73
|
-
| `gpumd-trjcat` | Merge
|
|
74
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
74
75
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
75
76
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
76
77
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -106,6 +107,8 @@ pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if inpu
|
|
|
106
107
|
# GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
|
|
107
108
|
pmk vasp-relax -i opted.vasp # relax inputs in current dir
|
|
108
109
|
pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
|
|
110
|
+
pmk gpumd-relax -if example -nep nep89_20250409.txt # model.xyz + run.in + NEP per job folder
|
|
111
|
+
pmk gpumd-relax -it train.xyz -nep nep.txt -in run.in # one ./frame_N/ GPUMD job per frame
|
|
109
112
|
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
110
113
|
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
111
114
|
pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
|
|
@@ -114,8 +117,9 @@ pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta
|
|
|
114
117
|
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
115
118
|
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
116
119
|
pmk vasp-fe # scans ./ for VASP jobs -> final-energy.txt with convergence status
|
|
117
|
-
pmk gather-
|
|
118
|
-
pmk gather-
|
|
120
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted
|
|
121
|
+
pmk gather-fs -job vasp -fs-name CONTCAR -of vasp-opted -ehull 0.028
|
|
122
|
+
pmk gather-fs -job gpumd -fs-name relaxed.xyz -of gpumd-opted
|
|
119
123
|
pmk vasp2xyz # scans ./ for VASP output folders -> scf-converged.xyz
|
|
120
124
|
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
121
125
|
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
@@ -137,6 +141,7 @@ pmk gpumd-thermo # current job, or recursively scan
|
|
|
137
141
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
138
142
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
139
143
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
144
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
140
145
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
146
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
142
147
|
|
|
@@ -168,6 +173,29 @@ multi-structure trajectory and writes one job sub-folder per frame
|
|
|
168
173
|
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
|
|
169
174
|
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
|
|
170
175
|
`vasp2xyz`.
|
|
176
|
+
`gpumd-relax` accepts the same `-i`, `-if`, and `-it` structure modes. `-nep`
|
|
177
|
+
is required and the selected potential is copied into every job folder together
|
|
178
|
+
with `model.xyz` and `run.in`. `-in FILE` supplies a custom GPUMD input; its
|
|
179
|
+
first `potential` filename is synchronized to the copied NEP. Without `-in`,
|
|
180
|
+
the generated job-local input is:
|
|
181
|
+
|
|
182
|
+
```text
|
|
183
|
+
potential <NEP filename>
|
|
184
|
+
|
|
185
|
+
minimize fire 2.0e-2 1000000
|
|
186
|
+
|
|
187
|
+
ensemble nve
|
|
188
|
+
dump_xyz -1 0 1 relaxed.xyz force
|
|
189
|
+
run 1
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
`gather-fs` recursively scans all job folders below the current directory. For
|
|
193
|
+
VASP it follows the global `vaspout.h5 > vasprun.xml > OUTCAR` priority and
|
|
194
|
+
requires full convergence before copying the selected `-fs-name` as `.vasp`.
|
|
195
|
+
For GPUMD it requires `gpumd.out` to report a force tolerance and a final
|
|
196
|
+
`f_max` no greater than that tolerance before copying the final structure.
|
|
197
|
+
`-fs-name` defaults to `CONTCAR` for VASP and `relaxed.xyz` for GPUMD. The
|
|
198
|
+
`-ehull` and `-ehull-file` filters are available only with `-job vasp`.
|
|
171
199
|
|
|
172
200
|
`add-config-type` updates its input atomically in place. With `-i`, the file must contain exactly one XYZ structure; use `-it` for a trajectory. Existing `Config_type` values are replaced, missing values are added, and all other extended-XYZ bytes remain unchanged.
|
|
173
201
|
|
|
@@ -177,11 +205,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
205
|
|
|
178
206
|
For folder conversion, `convert -if INPUT -of OUTPUT` writes `.xyz` by default. Set another target extension with `-oe`, for example `-oe cif` or `-oe vasp`. Single-file and trajectory modes infer the target format from `-o` and `-ot`.
|
|
179
207
|
|
|
180
|
-
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two
|
|
208
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. An abnormal classification always requires a mapped potential-energy change. When at least two lattice-parameter or volume signals show the same mapped transition, they may enable a more sensitive noise-relative energy test; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Cell signals can corroborate and refine the energy interval, but can never classify a run by themselves. Temperature and all six pressure components remain diagnostics only and cannot classify an otherwise stationary run as abnormal by themselves. A lone cell-axis drift, an isolated spike, or an empty anomaly mask never triggers a whole-run fallback. Thermo plots use Arial when available and Matplotlib's built-in qualitative `Dark2` color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and show abnormal regions as shaded legend entries on potential energy, lattice parameters, and volume. Potential energy and volume are divided by 1000 and labeled ×10³ only when their absolute plotted values reach 1000. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
|
|
181
209
|
|
|
182
210
|
The thermo parser follows the [official GPUMD thermo.out format](https://gpumd.org/gpumd/output_files/thermo_out.html): 18 columns are required, `pressure.txt` exports `Pxx Pyy Pzz Pyz Pxz Pxy`, box vectors are interpreted as a full 3×3 matrix, `time_step` propagates between runs, and `dump_thermo` does not.
|
|
183
211
|
|
|
184
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
212
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. By default, normal trajectories are merged in deterministic folder order into `train.xyz` (or `-o`), while only frames from the earliest mapped abnormal interval of each abnormal job are written to `abnormal.xyz` (or `-ao`). With `-all`, every readable full trajectory is merged into `train.xyz` regardless of normal, abnormal, missing-thermo, or failed-thermo status; an analyzable abnormal job still contributes its earliest abnormal interval to `abnormal.xyz`. If no mapped abnormal frames are found, `abnormal.xyz` is not created and a stale file at the selected `-ao` path is removed. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. In default mode, missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
|
|
185
213
|
|
|
186
214
|
`trj-sparse` keeps `max(1, floor(frame_count × ratio))` frames without rewriting their extended-XYZ content. Selection is deterministic and starts with frame zero; for 100 frames and `-r 0.5`, it writes source indices `0, 2, ..., 98`.
|
|
187
215
|
|
|
@@ -24,8 +24,9 @@ src/pymdkit/commands/compute_msd_all_groups.py
|
|
|
24
24
|
src/pymdkit/commands/compute_rmsd.py
|
|
25
25
|
src/pymdkit/commands/convert.py
|
|
26
26
|
src/pymdkit/commands/ewald.py
|
|
27
|
-
src/pymdkit/commands/
|
|
27
|
+
src/pymdkit/commands/gather_fs.py
|
|
28
28
|
src/pymdkit/commands/gpumd_group.py
|
|
29
|
+
src/pymdkit/commands/gpumd_relax.py
|
|
29
30
|
src/pymdkit/commands/gpumd_thermo.py
|
|
30
31
|
src/pymdkit/commands/gpumd_trjcat.py
|
|
31
32
|
src/pymdkit/commands/nep_rmse.py
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Gather CONTCAR files from VASP job folders into one folder.
|
|
3
|
-
|
|
4
|
-
Scans the sub-folders of the current directory (like `ehull`) and copies each
|
|
5
|
-
folder's CONTCAR into an output folder, renamed ``<folder-name>.vasp``. For
|
|
6
|
-
example the CONTCAR in ``1-Li3YCl6-164/`` becomes ``1-Li3YCl6-164.vasp``.
|
|
7
|
-
|
|
8
|
-
gather-contcar -of vasp-opted
|
|
9
|
-
gather-contcar -of vasp-opted -ehull 0.028 # only E_hull < 0.028 eV/atom
|
|
10
|
-
|
|
11
|
-
With -ehull, the ehull.txt report in the current directory (produced by
|
|
12
|
-
`pmk ehull`) is read and only structures whose E_hull is below the given
|
|
13
|
-
threshold (eV/atom) are gathered.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
import argparse
|
|
17
|
-
import shutil
|
|
18
|
-
from pathlib import Path
|
|
19
|
-
|
|
20
|
-
try:
|
|
21
|
-
from . import _fileio
|
|
22
|
-
except ImportError: # running as a standalone script
|
|
23
|
-
import _fileio
|
|
24
|
-
|
|
25
|
-
COMMAND = "gather-contcar"
|
|
26
|
-
HELP = "Gather CONTCAR files from VASP job folders into one folder (renamed .vasp)."
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def parse_ehull_file(path):
|
|
30
|
-
"""Return {folder_name: e_hull} from an ehull.txt report.
|
|
31
|
-
|
|
32
|
-
Data rows are ``<folder> <formula> <system> <FinalE> <E_form> <E_hull>``;
|
|
33
|
-
the folder is the first token and E_hull the last. Header / separator lines
|
|
34
|
-
(whose last token is not a number) are ignored.
|
|
35
|
-
"""
|
|
36
|
-
result = {}
|
|
37
|
-
for line in Path(path).read_text().splitlines():
|
|
38
|
-
parts = line.split()
|
|
39
|
-
if len(parts) < 2:
|
|
40
|
-
continue
|
|
41
|
-
try:
|
|
42
|
-
result[parts[0]] = float(parts[-1])
|
|
43
|
-
except ValueError:
|
|
44
|
-
continue
|
|
45
|
-
return result
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def add_arguments(parser):
|
|
49
|
-
parser.add_argument("-of", "-output-folder", dest="output_folder",
|
|
50
|
-
required=True, metavar="DIR",
|
|
51
|
-
help="Folder to create and fill with <name>.vasp files.")
|
|
52
|
-
parser.add_argument("-ehull", dest="ehull", type=float,
|
|
53
|
-
default=None, metavar="EV",
|
|
54
|
-
help="Only gather folders whose E_hull (from the report) "
|
|
55
|
-
"is below this value in eV/atom.")
|
|
56
|
-
parser.add_argument("-entry-path", default=".",
|
|
57
|
-
help="Directory to scan for VASP job folders (default: .).")
|
|
58
|
-
parser.add_argument("-ehull-file", default="ehull.txt",
|
|
59
|
-
help="E_hull report read with -ehull (default: ehull.txt).")
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def run(args):
|
|
63
|
-
root = Path(args.entry_path)
|
|
64
|
-
out_dir = Path(args.output_folder)
|
|
65
|
-
|
|
66
|
-
ehull_map = None
|
|
67
|
-
if args.ehull is not None:
|
|
68
|
-
report = Path(args.ehull_file)
|
|
69
|
-
if not report.is_file():
|
|
70
|
-
raise SystemExit(f"Error: {report} not found (needed for -ehull).")
|
|
71
|
-
ehull_map = parse_ehull_file(report)
|
|
72
|
-
|
|
73
|
-
out_dir.mkdir(parents=True, exist_ok=True)
|
|
74
|
-
gathered = 0
|
|
75
|
-
no_ehull = []
|
|
76
|
-
for sub in _fileio.subfolders(root):
|
|
77
|
-
contcar = sub / "CONTCAR"
|
|
78
|
-
if not contcar.is_file():
|
|
79
|
-
continue
|
|
80
|
-
name = sub.name
|
|
81
|
-
|
|
82
|
-
if ehull_map is not None:
|
|
83
|
-
val = ehull_map.get(name)
|
|
84
|
-
if val is None:
|
|
85
|
-
no_ehull.append(name)
|
|
86
|
-
continue
|
|
87
|
-
if not (val < args.ehull):
|
|
88
|
-
continue
|
|
89
|
-
|
|
90
|
-
dest = out_dir / f"{name}.vasp"
|
|
91
|
-
shutil.copy2(contcar, dest)
|
|
92
|
-
tag = f" (E_hull={ehull_map[name]:.3f})" if ehull_map is not None else ""
|
|
93
|
-
print(f"{contcar} -> {dest}{tag}")
|
|
94
|
-
gathered += 1
|
|
95
|
-
|
|
96
|
-
if no_ehull:
|
|
97
|
-
print(f"Note: {len(no_ehull)} folder(s) had a CONTCAR but no E_hull entry "
|
|
98
|
-
f"in {args.ehull_file}; skipped.")
|
|
99
|
-
print(f"Done: gathered {gathered} CONTCAR file(s) into {out_dir}/")
|
|
100
|
-
return 0
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
if __name__ == "__main__":
|
|
104
|
-
_p = argparse.ArgumentParser(description=__doc__)
|
|
105
|
-
add_arguments(_p)
|
|
106
|
-
raise SystemExit(run(_p.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
|