pymdkit 1.4.5__tar.gz → 1.4.7__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.5 → pymdkit-1.4.7}/PKG-INFO +5 -4
- {pymdkit-1.4.5 → pymdkit-1.4.7}/README.md +5 -4
- {pymdkit-1.4.5 → pymdkit-1.4.7}/pyproject.toml +1 -1
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/__init__.py +1 -1
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_gpumd.py +29 -35
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_thermo.py +56 -50
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_trjcat.py +92 -46
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/PKG-INFO +5 -4
- {pymdkit-1.4.5 → pymdkit-1.4.7}/LICENSE +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/setup.cfg +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gather_contcar.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/SOURCES.txt +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.4.5 → pymdkit-1.4.7}/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.7
|
|
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
|
|
@@ -70,7 +70,7 @@ directory for job sub-folders automatically.
|
|
|
70
70
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
71
71
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
72
72
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
73
|
-
| `gpumd-trjcat` | Merge
|
|
73
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
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
76
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -137,6 +137,7 @@ pmk gpumd-thermo # current job, or recursively scan
|
|
|
137
137
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
138
138
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
139
139
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
140
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
140
141
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
142
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
142
143
|
|
|
@@ -177,11 +178,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
178
|
|
|
178
179
|
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
180
|
|
|
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
|
|
181
|
+
`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
182
|
|
|
182
183
|
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
|
|
|
184
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
185
|
+
`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
186
|
|
|
186
187
|
`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
188
|
|
|
@@ -44,7 +44,7 @@ directory for job sub-folders automatically.
|
|
|
44
44
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
45
45
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
46
46
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
47
|
-
| `gpumd-trjcat` | Merge
|
|
47
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
48
48
|
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
49
49
|
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
50
50
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -110,7 +110,8 @@ pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
|
|
|
110
110
|
pmk gpumd-thermo # current job, or recursively scan job folders
|
|
111
111
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
112
112
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
113
|
-
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
113
|
+
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
114
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
114
115
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
115
116
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
116
117
|
|
|
@@ -151,11 +152,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
151
152
|
|
|
152
153
|
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
154
|
|
|
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
|
|
155
|
+
`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
156
|
|
|
156
157
|
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
|
|
|
158
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
159
|
+
`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
160
|
|
|
160
161
|
`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
162
|
|
|
@@ -310,8 +310,6 @@ def structural_abnormal_mask(lengths, volume):
|
|
|
310
310
|
]
|
|
311
311
|
votes = np.sum(masks, axis=0)
|
|
312
312
|
candidate = votes >= 2
|
|
313
|
-
if not candidate.any():
|
|
314
|
-
candidate = votes >= 1
|
|
315
313
|
candidate = _bridge_mask(candidate, max(1, len(candidate) // 50))
|
|
316
314
|
return _keep_major_regions(_clean_mask(candidate))
|
|
317
315
|
|
|
@@ -363,7 +361,9 @@ def load_thermo(thermo_path, run_path):
|
|
|
363
361
|
thermo_path = Path(thermo_path)
|
|
364
362
|
data = np.loadtxt(thermo_path, ndmin=2)
|
|
365
363
|
if data.shape[1] != 18:
|
|
366
|
-
raise ValueError(
|
|
364
|
+
raise ValueError(
|
|
365
|
+
f"{thermo_path} must use the official 18-column GPUMD format"
|
|
366
|
+
)
|
|
367
367
|
|
|
368
368
|
time_step, dump_interval, runs = parse_run_input(run_path)
|
|
369
369
|
time = time_axis(len(data), time_step, dump_interval, runs)
|
|
@@ -373,59 +373,53 @@ def load_thermo(thermo_path, run_path):
|
|
|
373
373
|
lengths, angles, volume = cell_quantities(data)
|
|
374
374
|
start = equilibrium_start(len(data), runs)
|
|
375
375
|
|
|
376
|
-
|
|
376
|
+
cell_warnings = find_abnormal({
|
|
377
377
|
"lattice-parameter": lengths,
|
|
378
378
|
"volume": volume,
|
|
379
|
+
}, start)
|
|
380
|
+
angle_warnings = find_abnormal({
|
|
379
381
|
"lattice-angle": angles,
|
|
380
382
|
}, start)
|
|
381
|
-
|
|
383
|
+
diagnostic_warnings = find_abnormal({
|
|
382
384
|
"temperature": temperature,
|
|
383
385
|
"pressure": pressure,
|
|
384
386
|
}, start)
|
|
385
387
|
|
|
386
|
-
|
|
388
|
+
segment_length = len(data) - start
|
|
387
389
|
segment_mask = potential_abnormal_mask(potential[start:])
|
|
388
|
-
|
|
389
|
-
warnings = []
|
|
390
|
+
warning_prefix = "potential-energy: localized"
|
|
390
391
|
|
|
391
|
-
|
|
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
|
+
if not segment_mask.any() and len(cell_warnings) >= 2:
|
|
392
395
|
structural_mask = structural_abnormal_mask(
|
|
393
396
|
lengths[start:], volume[start:]
|
|
394
397
|
)
|
|
395
|
-
|
|
396
|
-
|
|
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))
|
|
398
|
+
if structural_mask.any():
|
|
399
|
+
relaxed_energy = potential_abnormal_mask(
|
|
400
|
+
potential[start:], corroborated=True
|
|
402
401
|
)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
else:
|
|
412
|
-
warning_prefix = "potential-energy: localized"
|
|
402
|
+
overlap = relaxed_energy & structural_mask
|
|
403
|
+
if overlap.any():
|
|
404
|
+
corroborated = _keep_major_regions(
|
|
405
|
+
_bridge_mask(overlap, max(1, segment_length // 20))
|
|
406
|
+
)
|
|
407
|
+
if corroborated.any():
|
|
408
|
+
segment_mask = corroborated
|
|
409
|
+
warning_prefix = "potential-energy: structurally corroborated"
|
|
413
410
|
|
|
411
|
+
abnormal_mask = np.zeros(len(data), dtype=bool)
|
|
412
|
+
abnormal_mask[start:] = segment_mask
|
|
414
413
|
intervals = mask_intervals(time, abnormal_mask)
|
|
414
|
+
warnings = []
|
|
415
415
|
if intervals:
|
|
416
416
|
warnings.append(
|
|
417
417
|
f"{warning_prefix} abnormal region(s) at "
|
|
418
418
|
+ _format_intervals(intervals)
|
|
419
419
|
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
warnings.append(f"potential-energy: {potential_reason}")
|
|
424
|
-
abnormal_mask[start:] = True
|
|
425
|
-
|
|
426
|
-
warnings.extend(structural_warnings)
|
|
427
|
-
if abnormal_mask.any():
|
|
428
|
-
warnings.extend(other_warnings)
|
|
420
|
+
warnings.extend(cell_warnings)
|
|
421
|
+
warnings.extend(angle_warnings)
|
|
422
|
+
warnings.extend(diagnostic_warnings)
|
|
429
423
|
|
|
430
424
|
return ThermoData(
|
|
431
425
|
time=time,
|
|
@@ -15,33 +15,19 @@ except ImportError:
|
|
|
15
15
|
COMMAND = "gpumd-thermo"
|
|
16
16
|
HELP = "Export and plot GPUMD thermo.out data and warn about instability."
|
|
17
17
|
|
|
18
|
-
|
|
19
|
-
"#686789",
|
|
20
|
-
"#B77F70",
|
|
21
|
-
"#E5E2B9",
|
|
22
|
-
"#BEB1A8",
|
|
23
|
-
"#A79A89",
|
|
24
|
-
"#8A95A9",
|
|
25
|
-
"#ECCED0",
|
|
26
|
-
"#7D7465",
|
|
27
|
-
"#E8D3C0",
|
|
28
|
-
"#7A8A71",
|
|
29
|
-
"#789798",
|
|
30
|
-
"#B57C82",
|
|
31
|
-
"#9FABB9",
|
|
32
|
-
"#B0B1B6",
|
|
33
|
-
"#99857E",
|
|
34
|
-
"#88878D",
|
|
35
|
-
"#91A0A5",
|
|
36
|
-
"#9AA690",
|
|
37
|
-
]
|
|
38
|
-
|
|
18
|
+
DARK2_NAME = "Dark2"
|
|
39
19
|
|
|
40
20
|
def save_table(path, time, values, header):
|
|
41
21
|
values = np.asarray(values)
|
|
42
22
|
if values.ndim == 1:
|
|
43
23
|
values = values[:, None]
|
|
44
|
-
np.savetxt(
|
|
24
|
+
np.savetxt(
|
|
25
|
+
path,
|
|
26
|
+
np.column_stack([time, values]),
|
|
27
|
+
fmt="%.10g",
|
|
28
|
+
header=header,
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
45
31
|
|
|
46
32
|
|
|
47
33
|
def scaled_plot_values(values, quantity, unit):
|
|
@@ -53,31 +39,18 @@ def scaled_plot_values(values, quantity, unit):
|
|
|
53
39
|
return values, f"{quantity} ({unit})"
|
|
54
40
|
|
|
55
41
|
|
|
56
|
-
def mark_abnormal_regions(axis, intervals):
|
|
57
|
-
for begin, end in intervals:
|
|
42
|
+
def mark_abnormal_regions(axis, intervals, color):
|
|
43
|
+
for index, (begin, end) in enumerate(intervals):
|
|
58
44
|
axis.axvspan(
|
|
59
|
-
begin,
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
0
|
|
64
|
-
"abnormal region",
|
|
65
|
-
|
|
66
|
-
ha="center",
|
|
67
|
-
va="top",
|
|
68
|
-
fontsize=7,
|
|
69
|
-
color=MORANDI_COLORS[0],
|
|
70
|
-
bbox={
|
|
71
|
-
"facecolor": MORANDI_COLORS[2],
|
|
72
|
-
"edgecolor": "none",
|
|
73
|
-
"alpha": 0.8,
|
|
74
|
-
"pad": 1.0,
|
|
75
|
-
},
|
|
76
|
-
clip_on=True,
|
|
77
|
-
zorder=4,
|
|
45
|
+
begin,
|
|
46
|
+
end,
|
|
47
|
+
color=color,
|
|
48
|
+
alpha=0.25,
|
|
49
|
+
linewidth=0,
|
|
50
|
+
label="abnormal region" if index == 0 else "_nolegend_",
|
|
51
|
+
zorder=0,
|
|
78
52
|
)
|
|
79
53
|
|
|
80
|
-
|
|
81
54
|
def set_upper_headroom(axis, values):
|
|
82
55
|
values = np.asarray(values, dtype=float)
|
|
83
56
|
finite = values[np.isfinite(values)]
|
|
@@ -94,14 +67,18 @@ def plot_thermo(path, thermo):
|
|
|
94
67
|
import matplotlib
|
|
95
68
|
matplotlib.use("Agg")
|
|
96
69
|
from cycler import cycler
|
|
70
|
+
colors = [
|
|
71
|
+
matplotlib.colors.to_hex(color)
|
|
72
|
+
for color in matplotlib.colormaps[DARK2_NAME].colors
|
|
73
|
+
]
|
|
97
74
|
matplotlib.rcParams.update({
|
|
98
75
|
"font.family": "sans-serif",
|
|
99
76
|
"font.sans-serif": ["Arial", "Liberation Sans", "DejaVu Sans"],
|
|
100
|
-
"axes.prop_cycle": cycler(color=
|
|
77
|
+
"axes.prop_cycle": cycler(color=colors),
|
|
78
|
+
"image.cmap": DARK2_NAME,
|
|
101
79
|
})
|
|
102
80
|
import matplotlib.pyplot as plt
|
|
103
81
|
|
|
104
|
-
colors = MORANDI_COLORS
|
|
105
82
|
legend_style = {
|
|
106
83
|
"loc": "upper right",
|
|
107
84
|
"frameon": False,
|
|
@@ -125,7 +102,9 @@ def plot_thermo(path, thermo):
|
|
|
125
102
|
thermo.time, potential, color=colors[0], zorder=2
|
|
126
103
|
)
|
|
127
104
|
axes[0, 1].set_ylabel(potential_label)
|
|
128
|
-
mark_abnormal_regions(axes[0, 1], intervals)
|
|
105
|
+
mark_abnormal_regions(axes[0, 1], intervals, colors[7])
|
|
106
|
+
if intervals:
|
|
107
|
+
axes[0, 1].legend(**legend_style).set_zorder(4)
|
|
129
108
|
|
|
130
109
|
for index, label in enumerate(("Pxx", "Pyy", "Pzz", "Pyz", "Pxz", "Pxy")):
|
|
131
110
|
axes[0, 2].plot(
|
|
@@ -148,7 +127,7 @@ def plot_thermo(path, thermo):
|
|
|
148
127
|
zorder=2,
|
|
149
128
|
)
|
|
150
129
|
axes[1, 0].set_ylabel("Lattice parameters (Å)")
|
|
151
|
-
mark_abnormal_regions(axes[1, 0], intervals)
|
|
130
|
+
mark_abnormal_regions(axes[1, 0], intervals, colors[7])
|
|
152
131
|
set_upper_headroom(axes[1, 0], thermo.lengths)
|
|
153
132
|
axes[1, 0].legend(**legend_style).set_zorder(4)
|
|
154
133
|
|
|
@@ -156,7 +135,9 @@ def plot_thermo(path, thermo):
|
|
|
156
135
|
thermo.time, volume, color=colors[1], zorder=2
|
|
157
136
|
)
|
|
158
137
|
axes[1, 1].set_ylabel(volume_label)
|
|
159
|
-
mark_abnormal_regions(axes[1, 1], intervals)
|
|
138
|
+
mark_abnormal_regions(axes[1, 1], intervals, colors[7])
|
|
139
|
+
if intervals:
|
|
140
|
+
axes[1, 1].legend(**legend_style).set_zorder(4)
|
|
160
141
|
|
|
161
142
|
for index, label in enumerate(("alpha", "beta", "gamma")):
|
|
162
143
|
axes[1, 2].plot(
|
|
@@ -185,6 +166,25 @@ def plot_thermo(path, thermo):
|
|
|
185
166
|
plt.close(fig)
|
|
186
167
|
|
|
187
168
|
|
|
169
|
+
def write_abnormal_time(path, intervals):
|
|
170
|
+
path = Path(path)
|
|
171
|
+
if not intervals:
|
|
172
|
+
path.unlink(missing_ok=True)
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
text = "".join(
|
|
176
|
+
f"{begin:.2f} ps to {end:.2f} ps\n"
|
|
177
|
+
for begin, end in intervals
|
|
178
|
+
)
|
|
179
|
+
temporary = path.with_name(f".{path.name}.pmk-tmp")
|
|
180
|
+
try:
|
|
181
|
+
temporary.write_text(text, encoding="utf-8")
|
|
182
|
+
temporary.replace(path)
|
|
183
|
+
except Exception:
|
|
184
|
+
temporary.unlink(missing_ok=True)
|
|
185
|
+
raise
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
188
|
def process_job(folder, thermo_name, run_name):
|
|
189
189
|
thermo = load_thermo(folder / thermo_name, folder / run_name)
|
|
190
190
|
save_table(
|
|
@@ -224,6 +224,10 @@ def process_job(folder, thermo_name, run_name):
|
|
|
224
224
|
"time(ps) alpha(deg) beta(deg) gamma(deg)",
|
|
225
225
|
)
|
|
226
226
|
plot_thermo(folder / "thermo.png", thermo)
|
|
227
|
+
intervals = mask_intervals(thermo.time, thermo.abnormal_mask)
|
|
228
|
+
wrote_abnormal_time = write_abnormal_time(
|
|
229
|
+
folder / "abnormal-time.txt", intervals
|
|
230
|
+
)
|
|
227
231
|
|
|
228
232
|
print(
|
|
229
233
|
f"{folder}: {len(thermo.time)} thermo record(s), "
|
|
@@ -236,6 +240,8 @@ def process_job(folder, thermo_name, run_name):
|
|
|
236
240
|
else:
|
|
237
241
|
print("No sustained equilibrium drift or level shift detected.")
|
|
238
242
|
print("Wrote thermo text tables and thermo.png")
|
|
243
|
+
if wrote_abnormal_time:
|
|
244
|
+
print("Wrote abnormal-time.txt")
|
|
239
245
|
|
|
240
246
|
|
|
241
247
|
def discover_jobs(thermo_arg, run_arg):
|
|
@@ -291,4 +297,4 @@ def run(args) -> int:
|
|
|
291
297
|
if __name__ == "__main__":
|
|
292
298
|
_parser = argparse.ArgumentParser(description=__doc__)
|
|
293
299
|
add_arguments(_parser)
|
|
294
|
-
raise SystemExit(run(_parser.parse_args()))
|
|
300
|
+
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.7
|
|
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
|
|
@@ -70,7 +70,7 @@ directory for job sub-folders automatically.
|
|
|
70
70
|
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
71
71
|
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
72
72
|
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
73
|
-
| `gpumd-trjcat` | Merge
|
|
73
|
+
| `gpumd-trjcat` | Merge normal or all GPUMD trajectories and extract the first abnormal region |
|
|
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
76
|
| `trj-sparse` | Deterministically retain a requested fraction of XYZ trajectory frames |
|
|
@@ -137,6 +137,7 @@ pmk gpumd-thermo # current job, or recursively scan
|
|
|
137
137
|
pmk gpumd-thermo -t thermo.out -in run.in
|
|
138
138
|
pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
|
|
139
139
|
pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
|
|
140
|
+
pmk gpumd-trjcat -all # merge every readable trajectory into train.xyz
|
|
140
141
|
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
141
142
|
pmk trj-sparse -it example.xyz -r 0.5 -o sparse-example.xyz
|
|
142
143
|
|
|
@@ -177,11 +178,11 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
178
|
|
|
178
179
|
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
180
|
|
|
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
|
|
181
|
+
`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
182
|
|
|
182
183
|
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
|
|
|
184
|
-
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`.
|
|
185
|
+
`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
186
|
|
|
186
187
|
`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
188
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|