pymdkit 1.4.3__tar.gz → 1.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymdkit-1.4.3/src/pymdkit.egg-info → pymdkit-1.4.4}/PKG-INFO +9 -3
- {pymdkit-1.4.3 → pymdkit-1.4.4}/README.md +10 -4
- {pymdkit-1.4.3 → pymdkit-1.4.4}/pyproject.toml +1 -1
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/__init__.py +1 -1
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_gpumd.py +161 -55
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/compute_msd_all_groups.py +33 -17
- pymdkit-1.4.4/src/pymdkit/commands/gpumd_thermo.py +260 -0
- pymdkit-1.4.4/src/pymdkit/commands/trj_sparse.py +79 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4/src/pymdkit.egg-info}/PKG-INFO +9 -3
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit.egg-info/SOURCES.txt +1 -0
- pymdkit-1.4.3/src/pymdkit/commands/gpumd_thermo.py +0 -131
- {pymdkit-1.4.3 → pymdkit-1.4.4}/LICENSE +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/setup.cfg +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/gather_contcar.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.4.3 → pymdkit-1.4.4}/src/pymdkit.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -73,6 +73,7 @@ directory for job sub-folders automatically.
|
|
|
73
73
|
| `gpumd-trjcat` | Merge stable GPUMD trajectories and extract abnormal time regions |
|
|
74
74
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
75
75
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
76
|
+
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
76
77
|
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
77
78
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
78
79
|
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
@@ -137,6 +138,7 @@ pmk gpumd-thermo -t thermo.out -in run.in
|
|
|
137
138
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
138
139
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
139
140
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
|
+
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
140
142
|
|
|
141
143
|
pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
142
144
|
pmk rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
|
|
@@ -175,9 +177,13 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
175
177
|
|
|
176
178
|
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`.
|
|
177
179
|
|
|
178
|
-
`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. 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. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. 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.
|
|
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. 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 multiple lattice or volume measures independently show a transition, a more sensitive noise-relative potential-energy test is enabled; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. Thermo plots use Arial when available, opaque legends, correct Å units, and shaded abnormal regions 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.
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
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
|
+
|
|
184
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. 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
|
+
|
|
186
|
+
`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`.
|
|
181
187
|
|
|
182
188
|
Each command's full flag list is in `pmk <command> -help`.
|
|
183
189
|
|
|
@@ -46,7 +46,8 @@ directory for job sub-folders automatically.
|
|
|
46
46
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
47
47
|
| `gpumd-trjcat` | Merge stable GPUMD trajectories and extract abnormal time regions |
|
|
48
48
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
49
|
-
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
49
|
+
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
50
|
+
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
50
51
|
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
51
52
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
52
53
|
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
@@ -110,7 +111,8 @@ pmk gpumd-thermo # current job, or recursively scan
|
|
|
110
111
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
111
112
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
112
113
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
113
|
-
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
114
|
+
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
115
|
+
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
114
116
|
|
|
115
117
|
pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
116
118
|
pmk rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
|
|
@@ -149,10 +151,14 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
149
151
|
|
|
150
152
|
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`.
|
|
151
153
|
|
|
152
|
-
`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. 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. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. 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.
|
|
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. 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 multiple lattice or volume measures independently show a transition, a more sensitive noise-relative potential-energy test is enabled; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. Thermo plots use Arial when available, opaque legends, correct Å units, and shaded abnormal regions 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.
|
|
153
155
|
|
|
154
|
-
|
|
156
|
+
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
|
+
|
|
158
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. 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.
|
|
155
159
|
|
|
160
|
+
`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
|
+
|
|
156
162
|
Each command's full flag list is in `pmk <command> -help`.
|
|
157
163
|
|
|
158
164
|
For long VASP batch submission, run `submit-vasp` with `nohup` and `&` if you
|
|
@@ -24,7 +24,8 @@ class ThermoData:
|
|
|
24
24
|
|
|
25
25
|
def parse_run_input(path):
|
|
26
26
|
time_step = 1.0
|
|
27
|
-
|
|
27
|
+
active_dump = None
|
|
28
|
+
last_dump = None
|
|
28
29
|
runs = []
|
|
29
30
|
for raw in Path(path).read_text(encoding="utf-8").splitlines():
|
|
30
31
|
tokens = raw.split("#", 1)[0].split()
|
|
@@ -34,13 +35,16 @@ def parse_run_input(path):
|
|
|
34
35
|
if tokens[0] == "time_step" and len(tokens) >= 2:
|
|
35
36
|
time_step = float(tokens[1])
|
|
36
37
|
elif tokens[0] == "dump_thermo" and len(tokens) >= 2:
|
|
37
|
-
|
|
38
|
+
active_dump = int(tokens[1])
|
|
39
|
+
if active_dump <= 0:
|
|
40
|
+
raise ValueError("dump_thermo interval must be positive")
|
|
41
|
+
last_dump = active_dump
|
|
38
42
|
elif tokens[0] == "run" and len(tokens) >= 2:
|
|
39
|
-
runs.append((int(tokens[1]),
|
|
43
|
+
runs.append((int(tokens[1]), active_dump, time_step))
|
|
44
|
+
active_dump = None
|
|
40
45
|
except ValueError as exc:
|
|
41
46
|
raise ValueError(f"invalid {tokens[0]} setting in {path}: {raw}") from exc
|
|
42
|
-
return time_step,
|
|
43
|
-
|
|
47
|
+
return time_step, last_dump, runs
|
|
44
48
|
|
|
45
49
|
def trajectory_interval(run_input, trajectory):
|
|
46
50
|
"""Return the GPUMD XYZ dump timing as (time_step, steps, ps)."""
|
|
@@ -77,35 +81,38 @@ def trajectory_interval(run_input, trajectory):
|
|
|
77
81
|
|
|
78
82
|
|
|
79
83
|
def time_axis(length, time_step, dump_interval, runs):
|
|
80
|
-
expected = sum(
|
|
84
|
+
expected = sum(
|
|
85
|
+
steps // interval
|
|
86
|
+
for steps, interval, _ in runs
|
|
87
|
+
if interval is not None
|
|
88
|
+
)
|
|
81
89
|
if runs and expected == length:
|
|
82
90
|
values = []
|
|
83
91
|
elapsed = 0.0
|
|
84
92
|
for steps, interval, step_fs in runs:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
93
|
+
if interval is not None:
|
|
94
|
+
count = steps // interval
|
|
95
|
+
increment = step_fs * interval / 1000.0
|
|
96
|
+
values.extend(elapsed + np.arange(count) * increment)
|
|
88
97
|
elapsed += steps * step_fs / 1000.0
|
|
89
98
|
return np.asarray(values, dtype=float)
|
|
99
|
+
if dump_interval is None:
|
|
100
|
+
raise ValueError("no dump_thermo setting found in run.in")
|
|
90
101
|
increment = time_step * dump_interval / 1000.0
|
|
91
102
|
return np.arange(length, dtype=float) * increment
|
|
92
103
|
|
|
93
104
|
|
|
94
105
|
def equilibrium_start(length, runs):
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
106
|
+
for steps, interval, _ in reversed(runs):
|
|
107
|
+
if interval is not None:
|
|
108
|
+
points = steps // interval
|
|
109
|
+
return max(0, length - points)
|
|
110
|
+
return int(length * 0.2)
|
|
100
111
|
|
|
101
112
|
def cell_quantities(data):
|
|
102
|
-
if data.shape[1] == 12:
|
|
103
|
-
lengths = data[:, 9:12]
|
|
104
|
-
angles = np.full((len(data), 3), 90.0)
|
|
105
|
-
return lengths, angles, np.prod(lengths, axis=1)
|
|
106
113
|
if data.shape[1] != 18:
|
|
107
114
|
raise ValueError(
|
|
108
|
-
f"thermo.out has {data.shape[1]} columns; expected
|
|
115
|
+
f"thermo.out has {data.shape[1]} columns; expected the official 18-column format"
|
|
109
116
|
)
|
|
110
117
|
|
|
111
118
|
vectors = data[:, 9:18].reshape(-1, 3, 3)
|
|
@@ -194,8 +201,48 @@ def _clean_mask(mask):
|
|
|
194
201
|
return mask
|
|
195
202
|
|
|
196
203
|
|
|
197
|
-
def
|
|
198
|
-
|
|
204
|
+
def _bridge_mask(mask, max_gap):
|
|
205
|
+
mask = np.asarray(mask, dtype=bool).copy()
|
|
206
|
+
index = 0
|
|
207
|
+
while index < len(mask):
|
|
208
|
+
if mask[index]:
|
|
209
|
+
index += 1
|
|
210
|
+
continue
|
|
211
|
+
end = index
|
|
212
|
+
while end < len(mask) and not mask[end]:
|
|
213
|
+
end += 1
|
|
214
|
+
if index > 0 and end < len(mask) and end - index <= max_gap:
|
|
215
|
+
mask[index:end] = True
|
|
216
|
+
index = end
|
|
217
|
+
return mask
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _keep_major_regions(mask):
|
|
221
|
+
mask = np.asarray(mask, dtype=bool)
|
|
222
|
+
regions = []
|
|
223
|
+
index = 0
|
|
224
|
+
while index < len(mask):
|
|
225
|
+
if not mask[index]:
|
|
226
|
+
index += 1
|
|
227
|
+
continue
|
|
228
|
+
end = index
|
|
229
|
+
while end < len(mask) and mask[end]:
|
|
230
|
+
end += 1
|
|
231
|
+
regions.append((index, end))
|
|
232
|
+
index = end
|
|
233
|
+
if not regions:
|
|
234
|
+
return np.zeros(len(mask), dtype=bool)
|
|
235
|
+
|
|
236
|
+
longest = max(end - begin for begin, end in regions)
|
|
237
|
+
minimum = max(3, len(mask) // 50, longest // 4)
|
|
238
|
+
result = np.zeros(len(mask), dtype=bool)
|
|
239
|
+
for begin, end in regions:
|
|
240
|
+
if end - begin >= minimum:
|
|
241
|
+
result[begin:end] = True
|
|
242
|
+
return result
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _persistent_change_mask(values, corroborated=False, bridge=True):
|
|
199
246
|
values = np.asarray(values, dtype=float)
|
|
200
247
|
size = len(values)
|
|
201
248
|
if size < 20 or not np.all(np.isfinite(values)):
|
|
@@ -207,8 +254,6 @@ def potential_abnormal_mask(values):
|
|
|
207
254
|
noise = max(left_spread, right_spread, 1e-12)
|
|
208
255
|
difference = abs(right_center - left_center)
|
|
209
256
|
|
|
210
|
-
# Average over 1% of the segment so isolated thermal excursions cannot
|
|
211
|
-
# trigger a warning. Raw values are used again to refine the boundaries.
|
|
212
257
|
window = max(5, size // 100)
|
|
213
258
|
if window % 2 == 0:
|
|
214
259
|
window += 1
|
|
@@ -217,33 +262,58 @@ def potential_abnormal_mask(values):
|
|
|
217
262
|
cumulative = np.cumsum(np.insert(padded, 0, 0.0))
|
|
218
263
|
smooth = (cumulative[window:] - cumulative[:-window]) / window
|
|
219
264
|
|
|
265
|
+
effect_multiplier = 0.5 if corroborated else 1.5
|
|
220
266
|
practical_shift = max(
|
|
221
|
-
|
|
267
|
+
effect_multiplier * noise,
|
|
222
268
|
1e-6 * max(abs(left_center), abs(right_center), 1.0),
|
|
223
269
|
)
|
|
224
270
|
if difference > practical_shift:
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
tolerance = max(0.75 * noise, 0.05 * difference)
|
|
271
|
+
tolerance_multiplier = 0.25 if corroborated else 0.75
|
|
272
|
+
tolerance = max(tolerance_multiplier * noise, 0.05 * difference)
|
|
228
273
|
smooth_distance = np.minimum(
|
|
229
274
|
np.abs(smooth - left_center), np.abs(smooth - right_center)
|
|
230
275
|
)
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
276
|
+
candidate = smooth_distance > tolerance
|
|
277
|
+
if not corroborated:
|
|
278
|
+
raw_distance = np.minimum(
|
|
279
|
+
np.abs(values - left_center), np.abs(values - right_center)
|
|
280
|
+
)
|
|
281
|
+
candidate &= raw_distance > tolerance
|
|
282
|
+
else:
|
|
283
|
+
baseline = np.concatenate((values[:edge_count], values[-edge_count:]))
|
|
284
|
+
center, spread = _robust_spread(baseline)
|
|
285
|
+
multiplier = 1.0 if corroborated else 2.0
|
|
286
|
+
threshold = max(
|
|
287
|
+
multiplier * spread, 1e-6 * max(abs(center), 1.0), 1e-12
|
|
236
288
|
)
|
|
289
|
+
candidate = np.abs(smooth - center) > threshold
|
|
290
|
+
if not corroborated:
|
|
291
|
+
candidate &= np.abs(values - center) > threshold
|
|
237
292
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
293
|
+
if corroborated and bridge:
|
|
294
|
+
candidate = _bridge_mask(candidate, max(1, size // 20))
|
|
295
|
+
return _clean_mask(candidate)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def potential_abnormal_mask(values, corroborated=False):
|
|
299
|
+
"""Locate persistent potential-energy transitions or abnormal plateaus."""
|
|
300
|
+
return _persistent_change_mask(values, corroborated=corroborated)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def structural_abnormal_mask(lengths, volume):
|
|
304
|
+
"""Find dominant transition regions supported by multiple cell measures."""
|
|
305
|
+
components = [np.asarray(lengths)[:, index] for index in range(3)]
|
|
306
|
+
components.append(np.asarray(volume))
|
|
307
|
+
masks = [
|
|
308
|
+
_persistent_change_mask(values, corroborated=True, bridge=False)
|
|
309
|
+
for values in components
|
|
310
|
+
]
|
|
311
|
+
votes = np.sum(masks, axis=0)
|
|
312
|
+
candidate = votes >= 2
|
|
313
|
+
if not candidate.any():
|
|
314
|
+
candidate = votes >= 1
|
|
315
|
+
candidate = _bridge_mask(candidate, max(1, len(candidate) // 50))
|
|
316
|
+
return _keep_major_regions(_clean_mask(candidate))
|
|
247
317
|
|
|
248
318
|
def mask_intervals(time, mask):
|
|
249
319
|
"""Return inclusive physical-time intervals for contiguous true mask regions."""
|
|
@@ -269,7 +339,7 @@ def _format_intervals(intervals):
|
|
|
269
339
|
|
|
270
340
|
def find_abnormal(series, start):
|
|
271
341
|
component_names = {
|
|
272
|
-
"pressure": ("
|
|
342
|
+
"pressure": ("xx", "yy", "zz", "yz", "xz", "xy"),
|
|
273
343
|
"lattice-parameter": ("a", "b", "c"),
|
|
274
344
|
"lattice-angle": ("alpha", "beta", "gamma"),
|
|
275
345
|
}
|
|
@@ -292,24 +362,59 @@ def find_abnormal(series, start):
|
|
|
292
362
|
def load_thermo(thermo_path, run_path):
|
|
293
363
|
thermo_path = Path(thermo_path)
|
|
294
364
|
data = np.loadtxt(thermo_path, ndmin=2)
|
|
295
|
-
if data.shape[1]
|
|
296
|
-
raise ValueError(f"{thermo_path} must
|
|
365
|
+
if data.shape[1] != 18:
|
|
366
|
+
raise ValueError(f"{thermo_path} must use the official 18-column GPUMD format")
|
|
297
367
|
|
|
298
368
|
time_step, dump_interval, runs = parse_run_input(run_path)
|
|
299
369
|
time = time_axis(len(data), time_step, dump_interval, runs)
|
|
300
370
|
temperature = data[:, 0]
|
|
301
371
|
potential = data[:, 2]
|
|
302
|
-
pressure = data[:, 3:
|
|
372
|
+
pressure = data[:, 3:9]
|
|
303
373
|
lengths, angles, volume = cell_quantities(data)
|
|
304
374
|
start = equilibrium_start(len(data), runs)
|
|
305
375
|
|
|
376
|
+
structural_warnings = find_abnormal({
|
|
377
|
+
"lattice-parameter": lengths,
|
|
378
|
+
"volume": volume,
|
|
379
|
+
"lattice-angle": angles,
|
|
380
|
+
}, start)
|
|
381
|
+
other_warnings = find_abnormal({
|
|
382
|
+
"temperature": temperature,
|
|
383
|
+
"pressure": pressure,
|
|
384
|
+
}, start)
|
|
385
|
+
|
|
306
386
|
abnormal_mask = np.zeros(len(data), dtype=bool)
|
|
307
|
-
|
|
308
|
-
|
|
387
|
+
segment_mask = potential_abnormal_mask(potential[start:])
|
|
388
|
+
abnormal_mask[start:] = segment_mask
|
|
309
389
|
warnings = []
|
|
390
|
+
|
|
391
|
+
if not segment_mask.any() and structural_warnings:
|
|
392
|
+
structural_mask = structural_abnormal_mask(
|
|
393
|
+
lengths[start:], volume[start:]
|
|
394
|
+
)
|
|
395
|
+
relaxed_energy = potential_abnormal_mask(
|
|
396
|
+
potential[start:], corroborated=True
|
|
397
|
+
)
|
|
398
|
+
overlap = relaxed_energy & structural_mask
|
|
399
|
+
if overlap.any():
|
|
400
|
+
segment_mask = _keep_major_regions(
|
|
401
|
+
_bridge_mask(overlap, max(1, len(overlap) // 20))
|
|
402
|
+
)
|
|
403
|
+
warning_prefix = "potential-energy: structurally corroborated"
|
|
404
|
+
elif structural_mask.any():
|
|
405
|
+
segment_mask = structural_mask
|
|
406
|
+
warning_prefix = "structure: mapped fallback"
|
|
407
|
+
else:
|
|
408
|
+
segment_mask = np.ones(len(potential) - start, dtype=bool)
|
|
409
|
+
warning_prefix = "structure: full equilibrium fallback"
|
|
410
|
+
abnormal_mask[start:] = segment_mask
|
|
411
|
+
else:
|
|
412
|
+
warning_prefix = "potential-energy: localized"
|
|
413
|
+
|
|
414
|
+
intervals = mask_intervals(time, abnormal_mask)
|
|
310
415
|
if intervals:
|
|
311
416
|
warnings.append(
|
|
312
|
-
"
|
|
417
|
+
f"{warning_prefix} abnormal region(s) at "
|
|
313
418
|
+ _format_intervals(intervals)
|
|
314
419
|
)
|
|
315
420
|
else:
|
|
@@ -318,13 +423,14 @@ def load_thermo(thermo_path, run_path):
|
|
|
318
423
|
warnings.append(f"potential-energy: {potential_reason}")
|
|
319
424
|
abnormal_mask[start:] = True
|
|
320
425
|
|
|
321
|
-
warnings.extend(
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
426
|
+
warnings.extend(structural_warnings)
|
|
427
|
+
warnings.extend(other_warnings)
|
|
428
|
+
if warnings and not abnormal_mask.any():
|
|
429
|
+
abnormal_mask[start:] = True
|
|
430
|
+
warnings.insert(
|
|
431
|
+
0, "region mapping: full equilibrium segment used as fallback"
|
|
432
|
+
)
|
|
433
|
+
|
|
328
434
|
return ThermoData(
|
|
329
435
|
time=time,
|
|
330
436
|
temperature=temperature,
|
|
@@ -59,9 +59,11 @@ class MDAnalyzer:
|
|
|
59
59
|
if not path.exists():
|
|
60
60
|
return []
|
|
61
61
|
runs = []
|
|
62
|
-
dump_interval
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
dump_interval = None
|
|
63
|
+
dump_msd = None
|
|
64
|
+
time_step = 1.0
|
|
65
|
+
for raw in path.read_text(encoding="utf-8").splitlines():
|
|
66
|
+
parts = raw.split("#", 1)[0].split()
|
|
65
67
|
if not parts:
|
|
66
68
|
continue
|
|
67
69
|
key = parts[0]
|
|
@@ -75,11 +77,12 @@ class MDAnalyzer:
|
|
|
75
77
|
runs.append({
|
|
76
78
|
"steps": int(parts[1]),
|
|
77
79
|
"dump_interval": dump_interval,
|
|
78
|
-
"dump_msd": dump_msd
|
|
80
|
+
"dump_msd": dump_msd,
|
|
79
81
|
"time_step": time_step,
|
|
80
82
|
})
|
|
83
|
+
dump_interval = None
|
|
84
|
+
dump_msd = None
|
|
81
85
|
return runs
|
|
82
|
-
|
|
83
86
|
@staticmethod
|
|
84
87
|
def _detect_all_groups(filepath) -> bool:
|
|
85
88
|
"""True if a `compute_msd ... all_groups` directive is present."""
|
|
@@ -100,19 +103,26 @@ class MDAnalyzer:
|
|
|
100
103
|
|
|
101
104
|
@staticmethod
|
|
102
105
|
def _calculate_volume(data: np.ndarray) -> np.ndarray:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
np.cross(box[:, 1], box[:, 2])))
|
|
110
|
-
raise ValueError(f"Unsupported thermo.out format with {n_cols} columns")
|
|
106
|
+
if data.shape[1] != 18:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"thermo.out must use the official 18-column GPUMD format"
|
|
109
|
+
)
|
|
110
|
+
box = data[:, 9:18].reshape(-1, 3, 3)
|
|
111
|
+
return np.abs(np.linalg.det(box))
|
|
111
112
|
|
|
112
113
|
def load_thermo_data(self, filepath) -> Tuple[float, float]:
|
|
113
114
|
data = np.atleast_2d(np.loadtxt(Path(filepath)))
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
thermo_runs = [
|
|
116
|
+
run for run in self.runs_info
|
|
117
|
+
if run["dump_interval"] is not None
|
|
118
|
+
]
|
|
119
|
+
if thermo_runs:
|
|
120
|
+
run = thermo_runs[-1]
|
|
121
|
+
sl = self._last_run_slice(
|
|
122
|
+
len(data), run["dump_interval"]
|
|
123
|
+
)
|
|
124
|
+
else:
|
|
125
|
+
sl = slice(0, len(data))
|
|
116
126
|
self.temperature = float(np.mean(data[sl, 0]))
|
|
117
127
|
self.volume = float(np.mean(self._calculate_volume(data)[sl]))
|
|
118
128
|
return self.temperature, self.volume
|
|
@@ -143,8 +153,14 @@ class MDAnalyzer:
|
|
|
143
153
|
group's worth of columns it is mapped to the diffuse-ion group.
|
|
144
154
|
"""
|
|
145
155
|
data = np.loadtxt(Path(filepath))
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
msd_runs = [
|
|
157
|
+
run for run in self.runs_info
|
|
158
|
+
if run["dump_msd"] is not None
|
|
159
|
+
]
|
|
160
|
+
if msd_runs:
|
|
161
|
+
data = data[self._last_run_slice(
|
|
162
|
+
len(data), msd_runs[-1]["dump_msd"]
|
|
163
|
+
)]
|
|
148
164
|
times = data[:, 0] - data[0, 0]
|
|
149
165
|
|
|
150
166
|
num_groups = (data.shape[1] - 1) // 6
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Export, plot, and check GPUMD thermo data."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from ._gpumd import load_thermo, mask_intervals
|
|
12
|
+
except ImportError:
|
|
13
|
+
from _gpumd import load_thermo, mask_intervals
|
|
14
|
+
|
|
15
|
+
COMMAND = "gpumd-thermo"
|
|
16
|
+
HELP = "Export and plot GPUMD thermo.out data and warn about instability."
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def save_table(path, time, values, header):
|
|
20
|
+
values = np.asarray(values)
|
|
21
|
+
if values.ndim == 1:
|
|
22
|
+
values = values[:, None]
|
|
23
|
+
np.savetxt(path, np.column_stack([time, values]), fmt="%.10g", header=header)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def scaled_plot_values(values, quantity, unit):
|
|
27
|
+
values = np.asarray(values, dtype=float)
|
|
28
|
+
finite = np.abs(values[np.isfinite(values)])
|
|
29
|
+
use_thousands = finite.size > 0 and float(np.max(finite)) >= 1000.0
|
|
30
|
+
if use_thousands:
|
|
31
|
+
return values / 1000.0, f"{quantity} (×10³ {unit})"
|
|
32
|
+
return values, f"{quantity} ({unit})"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def mark_abnormal_regions(axis, intervals):
|
|
36
|
+
for begin, end in intervals:
|
|
37
|
+
axis.axvspan(
|
|
38
|
+
begin, end, color="#d9e8f5", alpha=0.85, linewidth=0, zorder=0
|
|
39
|
+
)
|
|
40
|
+
axis.text(
|
|
41
|
+
(begin + end) / 2.0,
|
|
42
|
+
0.97,
|
|
43
|
+
"abnormal region",
|
|
44
|
+
transform=axis.get_xaxis_transform(),
|
|
45
|
+
ha="center",
|
|
46
|
+
va="top",
|
|
47
|
+
fontsize=7,
|
|
48
|
+
color="#315a7d",
|
|
49
|
+
bbox={
|
|
50
|
+
"facecolor": "white",
|
|
51
|
+
"edgecolor": "none",
|
|
52
|
+
"alpha": 0.8,
|
|
53
|
+
"pad": 1.0,
|
|
54
|
+
},
|
|
55
|
+
clip_on=True,
|
|
56
|
+
zorder=4,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plot_thermo(path, thermo):
|
|
61
|
+
import matplotlib
|
|
62
|
+
matplotlib.use("Agg")
|
|
63
|
+
matplotlib.rcParams.update({
|
|
64
|
+
"font.family": "sans-serif",
|
|
65
|
+
"font.sans-serif": ["Arial", "Liberation Sans", "DejaVu Sans"],
|
|
66
|
+
})
|
|
67
|
+
import matplotlib.pyplot as plt
|
|
68
|
+
|
|
69
|
+
colors = ("#d22027", "#015699", "#fac00f")
|
|
70
|
+
legend_style = {
|
|
71
|
+
"loc": "best",
|
|
72
|
+
"frameon": True,
|
|
73
|
+
"facecolor": "white",
|
|
74
|
+
"edgecolor": "none",
|
|
75
|
+
"framealpha": 1.0,
|
|
76
|
+
}
|
|
77
|
+
intervals = mask_intervals(thermo.time, thermo.abnormal_mask)
|
|
78
|
+
potential, potential_label = scaled_plot_values(
|
|
79
|
+
thermo.potential, "Potential energy", "eV"
|
|
80
|
+
)
|
|
81
|
+
volume, volume_label = scaled_plot_values(
|
|
82
|
+
thermo.volume, "Volume", "ų"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
fig, axes = plt.subplots(2, 3, figsize=(12, 6), dpi=100)
|
|
86
|
+
|
|
87
|
+
axes[0, 0].plot(
|
|
88
|
+
thermo.time, thermo.temperature, color=colors[1], zorder=2
|
|
89
|
+
)
|
|
90
|
+
axes[0, 0].set_ylabel("Temperature (K)")
|
|
91
|
+
|
|
92
|
+
axes[0, 1].plot(
|
|
93
|
+
thermo.time, potential, color=colors[0], zorder=2
|
|
94
|
+
)
|
|
95
|
+
axes[0, 1].set_ylabel(potential_label)
|
|
96
|
+
mark_abnormal_regions(axes[0, 1], intervals)
|
|
97
|
+
|
|
98
|
+
for index, label in enumerate(("Pxx", "Pyy", "Pzz")):
|
|
99
|
+
axes[0, 2].plot(
|
|
100
|
+
thermo.time,
|
|
101
|
+
thermo.pressure[:, index],
|
|
102
|
+
label=label,
|
|
103
|
+
color=colors[index],
|
|
104
|
+
zorder=2,
|
|
105
|
+
)
|
|
106
|
+
axes[0, 2].set_ylabel("Pressure (GPa)")
|
|
107
|
+
axes[0, 2].legend(**legend_style).set_zorder(4)
|
|
108
|
+
|
|
109
|
+
for index, label in enumerate(("a", "b", "c")):
|
|
110
|
+
axes[1, 0].plot(
|
|
111
|
+
thermo.time,
|
|
112
|
+
thermo.lengths[:, index],
|
|
113
|
+
label=label,
|
|
114
|
+
color=colors[index],
|
|
115
|
+
zorder=2,
|
|
116
|
+
)
|
|
117
|
+
axes[1, 0].set_ylabel("Lattice parameters (Å)")
|
|
118
|
+
mark_abnormal_regions(axes[1, 0], intervals)
|
|
119
|
+
axes[1, 0].legend(**legend_style).set_zorder(4)
|
|
120
|
+
|
|
121
|
+
axes[1, 1].plot(
|
|
122
|
+
thermo.time, volume, color=colors[1], zorder=2
|
|
123
|
+
)
|
|
124
|
+
axes[1, 1].set_ylabel(volume_label)
|
|
125
|
+
mark_abnormal_regions(axes[1, 1], intervals)
|
|
126
|
+
|
|
127
|
+
for index, label in enumerate(("alpha", "beta", "gamma")):
|
|
128
|
+
axes[1, 2].plot(
|
|
129
|
+
thermo.time,
|
|
130
|
+
thermo.angles[:, index],
|
|
131
|
+
label=label,
|
|
132
|
+
color=colors[index],
|
|
133
|
+
zorder=2,
|
|
134
|
+
)
|
|
135
|
+
axes[1, 2].set_ylabel("Lattice angles (degree)")
|
|
136
|
+
axes[1, 2].legend(**legend_style).set_zorder(4)
|
|
137
|
+
|
|
138
|
+
for index, axis in enumerate(axes.flat):
|
|
139
|
+
axis.set_xlabel("Time (ps)")
|
|
140
|
+
axis.text(
|
|
141
|
+
-0.14,
|
|
142
|
+
1.03,
|
|
143
|
+
f"({'abcdef'[index]})",
|
|
144
|
+
transform=axis.transAxes,
|
|
145
|
+
fontsize=12,
|
|
146
|
+
va="top",
|
|
147
|
+
ha="right",
|
|
148
|
+
)
|
|
149
|
+
fig.tight_layout()
|
|
150
|
+
fig.savefig(path, dpi=300)
|
|
151
|
+
plt.close(fig)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def process_job(folder, thermo_name, run_name):
|
|
155
|
+
thermo = load_thermo(folder / thermo_name, folder / run_name)
|
|
156
|
+
save_table(
|
|
157
|
+
folder / "temperature.txt",
|
|
158
|
+
thermo.time,
|
|
159
|
+
thermo.temperature,
|
|
160
|
+
"time(ps) temperature(K)",
|
|
161
|
+
)
|
|
162
|
+
save_table(
|
|
163
|
+
folder / "potential-energy.txt",
|
|
164
|
+
thermo.time,
|
|
165
|
+
thermo.potential,
|
|
166
|
+
"time(ps) potential_energy(eV)",
|
|
167
|
+
)
|
|
168
|
+
save_table(
|
|
169
|
+
folder / "pressure.txt",
|
|
170
|
+
thermo.time,
|
|
171
|
+
thermo.pressure,
|
|
172
|
+
"time(ps) Pxx(GPa) Pyy(GPa) Pzz(GPa) Pyz(GPa) Pxz(GPa) Pxy(GPa)",
|
|
173
|
+
)
|
|
174
|
+
save_table(
|
|
175
|
+
folder / "lattice-parameters.txt",
|
|
176
|
+
thermo.time,
|
|
177
|
+
thermo.lengths,
|
|
178
|
+
"time(ps) a(Å) b(Å) c(Å)",
|
|
179
|
+
)
|
|
180
|
+
save_table(
|
|
181
|
+
folder / "volume.txt",
|
|
182
|
+
thermo.time,
|
|
183
|
+
thermo.volume,
|
|
184
|
+
"time(ps) volume(Å^3)",
|
|
185
|
+
)
|
|
186
|
+
save_table(
|
|
187
|
+
folder / "lattice-angles.txt",
|
|
188
|
+
thermo.time,
|
|
189
|
+
thermo.angles,
|
|
190
|
+
"time(ps) alpha(deg) beta(deg) gamma(deg)",
|
|
191
|
+
)
|
|
192
|
+
plot_thermo(folder / "thermo.png", thermo)
|
|
193
|
+
|
|
194
|
+
print(
|
|
195
|
+
f"{folder}: {len(thermo.time)} thermo record(s), "
|
|
196
|
+
f"{thermo.time[-1]:.6g} ps"
|
|
197
|
+
)
|
|
198
|
+
if thermo.warnings:
|
|
199
|
+
print("WARNING: possible abnormal fluctuation in the equilibrium segment:")
|
|
200
|
+
for warning in thermo.warnings:
|
|
201
|
+
print(f" {warning}")
|
|
202
|
+
else:
|
|
203
|
+
print("No sustained equilibrium drift or level shift detected.")
|
|
204
|
+
print("Wrote thermo text tables and thermo.png")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def discover_jobs(thermo_arg, run_arg):
|
|
208
|
+
thermo = Path(thermo_arg)
|
|
209
|
+
run_input = Path(run_arg)
|
|
210
|
+
if thermo.is_file() and run_input.is_file():
|
|
211
|
+
return [(thermo.parent.resolve(), thermo.name, run_input.name)]
|
|
212
|
+
|
|
213
|
+
root = Path(".").resolve()
|
|
214
|
+
jobs = sorted(
|
|
215
|
+
{
|
|
216
|
+
path.parent.resolve()
|
|
217
|
+
for path in root.rglob(thermo.name)
|
|
218
|
+
if (path.parent / run_input.name).is_file()
|
|
219
|
+
},
|
|
220
|
+
key=lambda path: (len(path.relative_to(root).parts), str(path)),
|
|
221
|
+
)
|
|
222
|
+
return [(folder, thermo.name, run_input.name) for folder in jobs]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"-t",
|
|
228
|
+
"-thermo",
|
|
229
|
+
dest="thermo",
|
|
230
|
+
default="thermo.out",
|
|
231
|
+
help="GPUMD thermo output (default: thermo.out).",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"-in",
|
|
235
|
+
dest="run_input",
|
|
236
|
+
default="run.in",
|
|
237
|
+
help="GPUMD run input (default: run.in).",
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def run(args) -> int:
|
|
242
|
+
jobs = discover_jobs(args.thermo, args.run_input)
|
|
243
|
+
if not jobs:
|
|
244
|
+
raise SystemExit(
|
|
245
|
+
"Error: no folder containing both thermo.out and run.in was found."
|
|
246
|
+
)
|
|
247
|
+
failures = 0
|
|
248
|
+
for folder, thermo_name, run_name in jobs:
|
|
249
|
+
try:
|
|
250
|
+
process_job(folder, thermo_name, run_name)
|
|
251
|
+
except Exception as exc:
|
|
252
|
+
failures += 1
|
|
253
|
+
print(f"{folder}: error ({exc})")
|
|
254
|
+
return 1 if failures == len(jobs) else 0
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
if __name__ == "__main__":
|
|
258
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
259
|
+
add_arguments(_parser)
|
|
260
|
+
raise SystemExit(run(_parser.parse_args()))
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Deterministically sparse an XYZ trajectory by a retained-frame ratio."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
from ._extxyz import read_blocks, write_blocks
|
|
10
|
+
except ImportError:
|
|
11
|
+
from _extxyz import read_blocks, write_blocks
|
|
12
|
+
|
|
13
|
+
COMMAND = "trj-sparse"
|
|
14
|
+
HELP = "Retain a deterministic fraction of frames from an XYZ trajectory."
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
18
|
+
parser.add_argument(
|
|
19
|
+
"-it",
|
|
20
|
+
"-input-trajectory",
|
|
21
|
+
dest="input_trajectory",
|
|
22
|
+
required=True,
|
|
23
|
+
help="Input XYZ trajectory.",
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument(
|
|
26
|
+
"-r",
|
|
27
|
+
"-ratio",
|
|
28
|
+
dest="ratio",
|
|
29
|
+
required=True,
|
|
30
|
+
type=float,
|
|
31
|
+
help="Fraction of frames to retain, in the interval (0, 1].",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-o",
|
|
35
|
+
"-output",
|
|
36
|
+
dest="output",
|
|
37
|
+
help="Output trajectory (default: sparse-<input filename>).",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def sparse_indices(frame_count, ratio):
|
|
42
|
+
if not 0.0 < ratio <= 1.0:
|
|
43
|
+
raise ValueError("-r must be greater than 0 and no greater than 1")
|
|
44
|
+
if frame_count <= 0:
|
|
45
|
+
return []
|
|
46
|
+
output_count = max(1, int(frame_count * ratio))
|
|
47
|
+
return [int(index / ratio) for index in range(output_count)]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def run(args) -> int:
|
|
51
|
+
input_path = Path(args.input_trajectory)
|
|
52
|
+
blocks, _ = read_blocks(input_path)
|
|
53
|
+
if not blocks:
|
|
54
|
+
raise SystemExit(f"Error: {input_path} contains no trajectory frames.")
|
|
55
|
+
try:
|
|
56
|
+
indices = sparse_indices(len(blocks), args.ratio)
|
|
57
|
+
except ValueError as exc:
|
|
58
|
+
raise SystemExit(f"Error: {exc}") from exc
|
|
59
|
+
|
|
60
|
+
output = (
|
|
61
|
+
Path(args.output)
|
|
62
|
+
if args.output
|
|
63
|
+
else input_path.with_name(f"sparse-{input_path.name}")
|
|
64
|
+
)
|
|
65
|
+
if output.resolve() == input_path.resolve():
|
|
66
|
+
raise SystemExit("Error: input and output trajectories must be different.")
|
|
67
|
+
|
|
68
|
+
write_blocks(output, blocks, indices)
|
|
69
|
+
print(
|
|
70
|
+
f"Wrote {len(indices)} of {len(blocks)} frame(s) "
|
|
71
|
+
f"({len(indices) / len(blocks):.6g}) to {output}"
|
|
72
|
+
)
|
|
73
|
+
return 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
if __name__ == "__main__":
|
|
77
|
+
_parser = argparse.ArgumentParser(description=__doc__)
|
|
78
|
+
add_arguments(_parser)
|
|
79
|
+
raise SystemExit(run(_parser.parse_args()))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymdkit
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.4
|
|
4
4
|
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
5
|
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -73,6 +73,7 @@ directory for job sub-folders automatically.
|
|
|
73
73
|
| `gpumd-trjcat` | Merge stable GPUMD trajectories and extract abnormal time regions |
|
|
74
74
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
75
75
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
76
|
+
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
76
77
|
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
77
78
|
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
78
79
|
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
@@ -137,6 +138,7 @@ pmk gpumd-thermo -t thermo.out -in run.in
|
|
|
137
138
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
138
139
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
139
140
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
|
+
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
140
142
|
|
|
141
143
|
pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
142
144
|
pmk rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
|
|
@@ -175,9 +177,13 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
175
177
|
|
|
176
178
|
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`.
|
|
177
179
|
|
|
178
|
-
`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. 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. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. 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.
|
|
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. 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 multiple lattice or volume measures independently show a transition, a more sensitive noise-relative potential-energy test is enabled; this avoids a fixed eV or percentage-of-total-energy threshold that would depend on system size. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. Thermo plots use Arial when available, opaque legends, correct Å units, and shaded abnormal regions 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.
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
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
|
+
|
|
184
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. 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
|
+
|
|
186
|
+
`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`.
|
|
181
187
|
|
|
182
188
|
Each command's full flag list is in `pmk <command> -help`.
|
|
183
189
|
|
|
@@ -36,6 +36,7 @@ src/pymdkit/commands/substitute.py
|
|
|
36
36
|
src/pymdkit/commands/supercell.py
|
|
37
37
|
src/pymdkit/commands/symmetrize.py
|
|
38
38
|
src/pymdkit/commands/trj_extract.py
|
|
39
|
+
src/pymdkit/commands/trj_sparse.py
|
|
39
40
|
src/pymdkit/commands/umap.py
|
|
40
41
|
src/pymdkit/commands/vasp2xyz.py
|
|
41
42
|
src/pymdkit/commands/vasp_fe.py
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"""Export, plot, and check GPUMD thermo data."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import argparse
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import numpy as np
|
|
9
|
-
|
|
10
|
-
try:
|
|
11
|
-
from ._gpumd import load_thermo
|
|
12
|
-
except ImportError:
|
|
13
|
-
from _gpumd import load_thermo
|
|
14
|
-
|
|
15
|
-
COMMAND = "gpumd-thermo"
|
|
16
|
-
HELP = "Export and plot GPUMD thermo.out data and warn about instability."
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def save_table(path, time, values, header):
|
|
20
|
-
values = np.asarray(values)
|
|
21
|
-
if values.ndim == 1:
|
|
22
|
-
values = values[:, None]
|
|
23
|
-
np.savetxt(path, np.column_stack([time, values]), fmt="%.10g", header=header)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def plot_thermo(path, thermo):
|
|
27
|
-
import matplotlib
|
|
28
|
-
matplotlib.use("Agg")
|
|
29
|
-
import matplotlib.pyplot as plt
|
|
30
|
-
|
|
31
|
-
colors = ("#d22027", "#015699", "#fac00f")
|
|
32
|
-
fig, axes = plt.subplots(2, 3, figsize=(12, 6), dpi=100)
|
|
33
|
-
|
|
34
|
-
axes[0, 0].plot(thermo.time, thermo.temperature, color=colors[1])
|
|
35
|
-
axes[0, 0].set_ylabel("Temperature (K)")
|
|
36
|
-
axes[0, 1].plot(thermo.time, thermo.potential / 1000.0, color=colors[0])
|
|
37
|
-
axes[0, 1].set_ylabel("Potential energy (x10^3 eV)")
|
|
38
|
-
for index, label in enumerate(("Px", "Py", "Pz")):
|
|
39
|
-
axes[0, 2].plot(thermo.time, thermo.pressure[:, index],
|
|
40
|
-
label=label, color=colors[index])
|
|
41
|
-
axes[0, 2].set_ylabel("Pressure (GPa)")
|
|
42
|
-
axes[0, 2].legend(frameon=False)
|
|
43
|
-
|
|
44
|
-
for index, label in enumerate(("a", "b", "c")):
|
|
45
|
-
axes[1, 0].plot(thermo.time, thermo.lengths[:, index],
|
|
46
|
-
label=label, color=colors[index])
|
|
47
|
-
axes[1, 0].set_ylabel("Lattice parameters (A)")
|
|
48
|
-
axes[1, 0].legend(frameon=False)
|
|
49
|
-
axes[1, 1].plot(thermo.time, thermo.volume / 1000.0, color=colors[1])
|
|
50
|
-
axes[1, 1].set_ylabel("Volume (x10^3 A^3)")
|
|
51
|
-
for index, label in enumerate(("alpha", "beta", "gamma")):
|
|
52
|
-
axes[1, 2].plot(thermo.time, thermo.angles[:, index],
|
|
53
|
-
label=label, color=colors[index])
|
|
54
|
-
axes[1, 2].set_ylabel("Lattice angles (degree)")
|
|
55
|
-
axes[1, 2].legend(frameon=False)
|
|
56
|
-
|
|
57
|
-
for index, axis in enumerate(axes.flat):
|
|
58
|
-
axis.set_xlabel("Time (ps)")
|
|
59
|
-
axis.text(-0.14, 1.03, f"({'abcdef'[index]})",
|
|
60
|
-
transform=axis.transAxes, fontsize=12, va="top", ha="right")
|
|
61
|
-
fig.tight_layout()
|
|
62
|
-
fig.savefig(path, dpi=300)
|
|
63
|
-
plt.close(fig)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def process_job(folder, thermo_name, run_name):
|
|
67
|
-
thermo = load_thermo(folder / thermo_name, folder / run_name)
|
|
68
|
-
save_table(folder / "temperature.txt", thermo.time, thermo.temperature,
|
|
69
|
-
"time(ps) temperature(K)")
|
|
70
|
-
save_table(folder / "potential-energy.txt", thermo.time, thermo.potential,
|
|
71
|
-
"time(ps) potential_energy(eV)")
|
|
72
|
-
save_table(folder / "pressure.txt", thermo.time, thermo.pressure,
|
|
73
|
-
"time(ps) pressure_x(GPa) pressure_y(GPa) pressure_z(GPa)")
|
|
74
|
-
save_table(folder / "lattice-parameters.txt", thermo.time, thermo.lengths,
|
|
75
|
-
"time(ps) a(A) b(A) c(A)")
|
|
76
|
-
save_table(folder / "volume.txt", thermo.time, thermo.volume,
|
|
77
|
-
"time(ps) volume(A^3)")
|
|
78
|
-
save_table(folder / "lattice-angles.txt", thermo.time, thermo.angles,
|
|
79
|
-
"time(ps) alpha(deg) beta(deg) gamma(deg)")
|
|
80
|
-
plot_thermo(folder / "thermo.png", thermo)
|
|
81
|
-
|
|
82
|
-
print(f"{folder}: {len(thermo.time)} thermo record(s), {thermo.time[-1]:.6g} ps")
|
|
83
|
-
if thermo.warnings:
|
|
84
|
-
print("WARNING: possible abnormal fluctuation in the equilibrium segment:")
|
|
85
|
-
for warning in thermo.warnings:
|
|
86
|
-
print(f" {warning}")
|
|
87
|
-
else:
|
|
88
|
-
print("No sustained equilibrium drift or level shift detected.")
|
|
89
|
-
print("Wrote thermo text tables and thermo.png")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def discover_jobs(thermo_arg, run_arg):
|
|
93
|
-
thermo = Path(thermo_arg)
|
|
94
|
-
run_input = Path(run_arg)
|
|
95
|
-
if thermo.is_file() and run_input.is_file():
|
|
96
|
-
return [(thermo.parent.resolve(), thermo.name, run_input.name)]
|
|
97
|
-
|
|
98
|
-
root = Path(".").resolve()
|
|
99
|
-
jobs = sorted(
|
|
100
|
-
{path.parent.resolve() for path in root.rglob(thermo.name)
|
|
101
|
-
if (path.parent / run_input.name).is_file()},
|
|
102
|
-
key=lambda path: (len(path.relative_to(root).parts), str(path)),
|
|
103
|
-
)
|
|
104
|
-
return [(folder, thermo.name, run_input.name) for folder in jobs]
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def add_arguments(parser: argparse.ArgumentParser) -> None:
|
|
108
|
-
parser.add_argument("-t", "-thermo", dest="thermo", default="thermo.out",
|
|
109
|
-
help="GPUMD thermo output (default: thermo.out).")
|
|
110
|
-
parser.add_argument("-in", dest="run_input", default="run.in",
|
|
111
|
-
help="GPUMD run input (default: run.in).")
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def run(args) -> int:
|
|
115
|
-
jobs = discover_jobs(args.thermo, args.run_input)
|
|
116
|
-
if not jobs:
|
|
117
|
-
raise SystemExit("Error: no folder containing both thermo.out and run.in was found.")
|
|
118
|
-
failures = 0
|
|
119
|
-
for folder, thermo_name, run_name in jobs:
|
|
120
|
-
try:
|
|
121
|
-
process_job(folder, thermo_name, run_name)
|
|
122
|
-
except Exception as exc:
|
|
123
|
-
failures += 1
|
|
124
|
-
print(f"{folder}: error ({exc})")
|
|
125
|
-
return 1 if failures == len(jobs) else 0
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if __name__ == "__main__":
|
|
129
|
-
_parser = argparse.ArgumentParser(description=__doc__)
|
|
130
|
-
add_arguments(_parser)
|
|
131
|
-
raise SystemExit(run(_parser.parse_args()))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|