pymdkit 1.4.4__tar.gz → 1.4.6__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.4 → pymdkit-1.4.6}/PKG-INFO +2 -2
- {pymdkit-1.4.4 → pymdkit-1.4.6}/README.md +1 -1
- {pymdkit-1.4.4 → pymdkit-1.4.6}/pyproject.toml +1 -1
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/__init__.py +1 -1
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_gpumd.py +30 -39
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_thermo.py +76 -36
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/PKG-INFO +2 -2
- {pymdkit-1.4.4 → pymdkit-1.4.6}/LICENSE +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/setup.cfg +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_extxyz.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_fileio.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_geometry.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_reduction.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/add_config_type.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_ehull.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_rmsd.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gather_contcar.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/pca.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/substitute.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/symmetrize.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/trj_extract.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/trj_sparse.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/umap.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp2xyz.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_fe.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/SOURCES.txt +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/requires.txt +0 -0
- {pymdkit-1.4.4 → pymdkit-1.4.6}/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.6
|
|
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
|
|
@@ -177,7 +177,7 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
177
|
|
|
178
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`.
|
|
179
179
|
|
|
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
|
|
180
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two independent lattice-parameter or volume measures show the same mapped 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 and volume can corroborate an energy transition. 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
181
|
|
|
182
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
183
|
|
|
@@ -151,7 +151,7 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
151
151
|
|
|
152
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`.
|
|
153
153
|
|
|
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
|
|
154
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two independent lattice-parameter or volume measures show the same mapped 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 and volume can corroborate an energy transition. 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
155
|
|
|
156
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
157
|
|
|
@@ -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,63 +373,54 @@ 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
|
+
# Structural evidence can lower the energy threshold only when at least
|
|
393
|
+
# two independent cell measures agree on a mapped transition.
|
|
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
|
-
|
|
412
|
-
warning_prefix = "potential-energy: localized"
|
|
402
|
+
overlap = relaxed_energy & structural_mask
|
|
403
|
+
if overlap.any():
|
|
404
|
+
segment_mask = _keep_major_regions(
|
|
405
|
+
_bridge_mask(overlap, max(1, segment_length // 20))
|
|
406
|
+
)
|
|
407
|
+
warning_prefix = "potential-energy: structurally corroborated"
|
|
408
|
+
else:
|
|
409
|
+
segment_mask = structural_mask
|
|
410
|
+
warning_prefix = "structure: coherently mapped"
|
|
413
411
|
|
|
412
|
+
abnormal_mask = np.zeros(len(data), dtype=bool)
|
|
413
|
+
abnormal_mask[start:] = segment_mask
|
|
414
414
|
intervals = mask_intervals(time, abnormal_mask)
|
|
415
|
+
warnings = []
|
|
415
416
|
if intervals:
|
|
416
417
|
warnings.append(
|
|
417
418
|
f"{warning_prefix} abnormal region(s) at "
|
|
418
419
|
+ _format_intervals(intervals)
|
|
419
420
|
)
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
warnings.append(f"potential-energy: {potential_reason}")
|
|
424
|
-
abnormal_mask[start:] = True
|
|
425
|
-
|
|
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
|
-
)
|
|
421
|
+
warnings.extend(cell_warnings)
|
|
422
|
+
warnings.extend(angle_warnings)
|
|
423
|
+
warnings.extend(diagnostic_warnings)
|
|
433
424
|
|
|
434
425
|
return ThermoData(
|
|
435
426
|
time=time,
|
|
@@ -13,14 +13,21 @@ except ImportError:
|
|
|
13
13
|
from _gpumd import load_thermo, mask_intervals
|
|
14
14
|
|
|
15
15
|
COMMAND = "gpumd-thermo"
|
|
16
|
-
HELP = "Export and plot GPUMD thermo.out data and warn about instability."
|
|
17
|
-
|
|
16
|
+
HELP = "Export and plot GPUMD thermo.out data and warn about instability."
|
|
17
|
+
|
|
18
|
+
DARK2_NAME = "Dark2"
|
|
18
19
|
|
|
19
20
|
def save_table(path, time, values, header):
|
|
20
21
|
values = np.asarray(values)
|
|
21
22
|
if values.ndim == 1:
|
|
22
23
|
values = values[:, None]
|
|
23
|
-
np.savetxt(
|
|
24
|
+
np.savetxt(
|
|
25
|
+
path,
|
|
26
|
+
np.column_stack([time, values]),
|
|
27
|
+
fmt="%.10g",
|
|
28
|
+
header=header,
|
|
29
|
+
encoding="utf-8",
|
|
30
|
+
)
|
|
24
31
|
|
|
25
32
|
|
|
26
33
|
def scaled_plot_values(values, quantity, unit):
|
|
@@ -32,47 +39,49 @@ def scaled_plot_values(values, quantity, unit):
|
|
|
32
39
|
return values, f"{quantity} ({unit})"
|
|
33
40
|
|
|
34
41
|
|
|
35
|
-
def mark_abnormal_regions(axis, intervals):
|
|
36
|
-
for begin, end in intervals:
|
|
42
|
+
def mark_abnormal_regions(axis, intervals, color):
|
|
43
|
+
for index, (begin, end) in enumerate(intervals):
|
|
37
44
|
axis.axvspan(
|
|
38
|
-
begin,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
0
|
|
43
|
-
"abnormal region",
|
|
44
|
-
|
|
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,
|
|
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,
|
|
57
52
|
)
|
|
58
53
|
|
|
54
|
+
def set_upper_headroom(axis, values):
|
|
55
|
+
values = np.asarray(values, dtype=float)
|
|
56
|
+
finite = values[np.isfinite(values)]
|
|
57
|
+
if not finite.size:
|
|
58
|
+
return
|
|
59
|
+
minimum = float(np.min(finite))
|
|
60
|
+
maximum = float(np.max(finite))
|
|
61
|
+
span = maximum - minimum
|
|
62
|
+
if span == 0:
|
|
63
|
+
span = max(abs(maximum) * 0.05, 1.0)
|
|
64
|
+
axis.set_ylim(minimum - 0.05 * span, maximum + 0.4 * span)
|
|
59
65
|
|
|
60
66
|
def plot_thermo(path, thermo):
|
|
61
67
|
import matplotlib
|
|
62
68
|
matplotlib.use("Agg")
|
|
69
|
+
from cycler import cycler
|
|
70
|
+
colors = [
|
|
71
|
+
matplotlib.colors.to_hex(color)
|
|
72
|
+
for color in matplotlib.colormaps[DARK2_NAME].colors
|
|
73
|
+
]
|
|
63
74
|
matplotlib.rcParams.update({
|
|
64
75
|
"font.family": "sans-serif",
|
|
65
76
|
"font.sans-serif": ["Arial", "Liberation Sans", "DejaVu Sans"],
|
|
77
|
+
"axes.prop_cycle": cycler(color=colors),
|
|
78
|
+
"image.cmap": DARK2_NAME,
|
|
66
79
|
})
|
|
67
80
|
import matplotlib.pyplot as plt
|
|
68
81
|
|
|
69
|
-
colors = ("#d22027", "#015699", "#fac00f")
|
|
70
82
|
legend_style = {
|
|
71
|
-
"loc": "
|
|
72
|
-
"frameon":
|
|
73
|
-
"facecolor": "white",
|
|
74
|
-
"edgecolor": "none",
|
|
75
|
-
"framealpha": 1.0,
|
|
83
|
+
"loc": "upper right",
|
|
84
|
+
"frameon": False,
|
|
76
85
|
}
|
|
77
86
|
intervals = mask_intervals(thermo.time, thermo.abnormal_mask)
|
|
78
87
|
potential, potential_label = scaled_plot_values(
|
|
@@ -93,9 +102,11 @@ def plot_thermo(path, thermo):
|
|
|
93
102
|
thermo.time, potential, color=colors[0], zorder=2
|
|
94
103
|
)
|
|
95
104
|
axes[0, 1].set_ylabel(potential_label)
|
|
96
|
-
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)
|
|
97
108
|
|
|
98
|
-
for index, label in enumerate(("Pxx", "Pyy", "Pzz")):
|
|
109
|
+
for index, label in enumerate(("Pxx", "Pyy", "Pzz", "Pyz", "Pxz", "Pxy")):
|
|
99
110
|
axes[0, 2].plot(
|
|
100
111
|
thermo.time,
|
|
101
112
|
thermo.pressure[:, index],
|
|
@@ -104,7 +115,8 @@ def plot_thermo(path, thermo):
|
|
|
104
115
|
zorder=2,
|
|
105
116
|
)
|
|
106
117
|
axes[0, 2].set_ylabel("Pressure (GPa)")
|
|
107
|
-
axes[0, 2].
|
|
118
|
+
set_upper_headroom(axes[0, 2], thermo.pressure)
|
|
119
|
+
axes[0, 2].legend(**legend_style, ncol=2).set_zorder(4)
|
|
108
120
|
|
|
109
121
|
for index, label in enumerate(("a", "b", "c")):
|
|
110
122
|
axes[1, 0].plot(
|
|
@@ -115,14 +127,17 @@ def plot_thermo(path, thermo):
|
|
|
115
127
|
zorder=2,
|
|
116
128
|
)
|
|
117
129
|
axes[1, 0].set_ylabel("Lattice parameters (Å)")
|
|
118
|
-
mark_abnormal_regions(axes[1, 0], intervals)
|
|
130
|
+
mark_abnormal_regions(axes[1, 0], intervals, colors[7])
|
|
131
|
+
set_upper_headroom(axes[1, 0], thermo.lengths)
|
|
119
132
|
axes[1, 0].legend(**legend_style).set_zorder(4)
|
|
120
133
|
|
|
121
134
|
axes[1, 1].plot(
|
|
122
135
|
thermo.time, volume, color=colors[1], zorder=2
|
|
123
136
|
)
|
|
124
137
|
axes[1, 1].set_ylabel(volume_label)
|
|
125
|
-
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)
|
|
126
141
|
|
|
127
142
|
for index, label in enumerate(("alpha", "beta", "gamma")):
|
|
128
143
|
axes[1, 2].plot(
|
|
@@ -151,6 +166,25 @@ def plot_thermo(path, thermo):
|
|
|
151
166
|
plt.close(fig)
|
|
152
167
|
|
|
153
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
|
+
|
|
154
188
|
def process_job(folder, thermo_name, run_name):
|
|
155
189
|
thermo = load_thermo(folder / thermo_name, folder / run_name)
|
|
156
190
|
save_table(
|
|
@@ -190,6 +224,10 @@ def process_job(folder, thermo_name, run_name):
|
|
|
190
224
|
"time(ps) alpha(deg) beta(deg) gamma(deg)",
|
|
191
225
|
)
|
|
192
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
|
+
)
|
|
193
231
|
|
|
194
232
|
print(
|
|
195
233
|
f"{folder}: {len(thermo.time)} thermo record(s), "
|
|
@@ -202,6 +240,8 @@ def process_job(folder, thermo_name, run_name):
|
|
|
202
240
|
else:
|
|
203
241
|
print("No sustained equilibrium drift or level shift detected.")
|
|
204
242
|
print("Wrote thermo text tables and thermo.png")
|
|
243
|
+
if wrote_abnormal_time:
|
|
244
|
+
print("Wrote abnormal-time.txt")
|
|
205
245
|
|
|
206
246
|
|
|
207
247
|
def discover_jobs(thermo_arg, run_arg):
|
|
@@ -257,4 +297,4 @@ def run(args) -> int:
|
|
|
257
297
|
if __name__ == "__main__":
|
|
258
298
|
_parser = argparse.ArgumentParser(description=__doc__)
|
|
259
299
|
add_arguments(_parser)
|
|
260
|
-
raise SystemExit(run(_parser.parse_args()))
|
|
300
|
+
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.6
|
|
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
|
|
@@ -177,7 +177,7 @@ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every
|
|
|
177
177
|
|
|
178
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`.
|
|
179
179
|
|
|
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
|
|
180
|
+
`gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. For an abnormal run it also writes `abnormal-time.txt`, with one `BEGIN ps to END ps` interval per line; a normal rerun removes a stale file. Potential energy is the primary equilibrium signal. Detection requires a persistent change with a meaningful magnitude relative to the robust natural energy fluctuations, so ordinary correlated thermal noise and isolated spikes are not classified as abnormal. When at least two independent lattice-parameter or volume measures show the same mapped 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 and volume can corroborate an energy transition. 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
181
|
|
|
182
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
183
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|