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.
Files changed (46) hide show
  1. {pymdkit-1.4.4 → pymdkit-1.4.6}/PKG-INFO +2 -2
  2. {pymdkit-1.4.4 → pymdkit-1.4.6}/README.md +1 -1
  3. {pymdkit-1.4.4 → pymdkit-1.4.6}/pyproject.toml +1 -1
  4. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/__init__.py +1 -1
  5. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_gpumd.py +30 -39
  6. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_thermo.py +76 -36
  7. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/PKG-INFO +2 -2
  8. {pymdkit-1.4.4 → pymdkit-1.4.6}/LICENSE +0 -0
  9. {pymdkit-1.4.4 → pymdkit-1.4.6}/setup.cfg +0 -0
  10. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/__init__.py +0 -0
  11. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_extxyz.py +0 -0
  12. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_fileio.py +0 -0
  13. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_geometry.py +0 -0
  14. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_reduction.py +0 -0
  15. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_vasp_output.py +0 -0
  16. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/_vaspset.py +0 -0
  17. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/add_config_type.py +0 -0
  18. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/chemsys_entry.py +0 -0
  19. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_ehull.py +0 -0
  20. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
  21. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/compute_rmsd.py +0 -0
  22. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/convert.py +0 -0
  23. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/ewald.py +0 -0
  24. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gather_contcar.py +0 -0
  25. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_group.py +0 -0
  26. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/gpumd_trjcat.py +0 -0
  27. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/nep_rmse.py +0 -0
  28. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/pca.py +0 -0
  29. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/perturb.py +0 -0
  30. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/submit_vasp.py +0 -0
  31. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/substitute.py +0 -0
  32. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/supercell.py +0 -0
  33. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/symmetrize.py +0 -0
  34. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/trj_extract.py +0 -0
  35. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/trj_sparse.py +0 -0
  36. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/umap.py +0 -0
  37. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp2xyz.py +0 -0
  38. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_fe.py +0 -0
  39. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_relax.py +0 -0
  40. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/commands/vasp_static.py +0 -0
  41. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit/pymdkit_main.py +0 -0
  42. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  43. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  44. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/entry_points.txt +0 -0
  45. {pymdkit-1.4.4 → pymdkit-1.4.6}/src/pymdkit.egg-info/requires.txt +0 -0
  46. {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.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 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.
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 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.
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.4.4"
7
+ version = "1.4.6"
8
8
  description = "A unified command-line toolkit for atomistic / MD structure workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """pymdkit -- a unified CLI for atomistic / MD structure workflows."""
2
2
 
3
- __version__ = "1.4.4"
3
+ __version__ = "1.4.6"
@@ -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(f"{thermo_path} must use the official 18-column GPUMD format")
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
- structural_warnings = find_abnormal({
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
- other_warnings = find_abnormal({
383
+ diagnostic_warnings = find_abnormal({
382
384
  "temperature": temperature,
383
385
  "pressure": pressure,
384
386
  }, start)
385
387
 
386
- abnormal_mask = np.zeros(len(data), dtype=bool)
388
+ segment_length = len(data) - start
387
389
  segment_mask = potential_abnormal_mask(potential[start:])
388
- abnormal_mask[start:] = segment_mask
389
- warnings = []
390
+ warning_prefix = "potential-energy: localized"
390
391
 
391
- if not segment_mask.any() and structural_warnings:
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
- 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))
398
+ if structural_mask.any():
399
+ relaxed_energy = potential_abnormal_mask(
400
+ potential[start:], corroborated=True
402
401
  )
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"
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
- else:
421
- potential_reason = abnormal_reason(potential[start:])
422
- if potential_reason:
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(path, np.column_stack([time, values]), fmt="%.10g", header=header)
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, 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,
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": "best",
72
- "frameon": True,
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].legend(**legend_style).set_zorder(4)
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.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 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.
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