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.
Files changed (46) hide show
  1. {pymdkit-1.4.5 → pymdkit-1.4.7}/PKG-INFO +5 -4
  2. {pymdkit-1.4.5 → pymdkit-1.4.7}/README.md +5 -4
  3. {pymdkit-1.4.5 → pymdkit-1.4.7}/pyproject.toml +1 -1
  4. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/__init__.py +1 -1
  5. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_gpumd.py +29 -35
  6. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_thermo.py +56 -50
  7. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_trjcat.py +92 -46
  8. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/PKG-INFO +5 -4
  9. {pymdkit-1.4.5 → pymdkit-1.4.7}/LICENSE +0 -0
  10. {pymdkit-1.4.5 → pymdkit-1.4.7}/setup.cfg +0 -0
  11. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/__init__.py +0 -0
  12. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_extxyz.py +0 -0
  13. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_fileio.py +0 -0
  14. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_geometry.py +0 -0
  15. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_reduction.py +0 -0
  16. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_vasp_output.py +0 -0
  17. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/_vaspset.py +0 -0
  18. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/add_config_type.py +0 -0
  19. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/chemsys_entry.py +0 -0
  20. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_ehull.py +0 -0
  21. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
  22. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/compute_rmsd.py +0 -0
  23. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/convert.py +0 -0
  24. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/ewald.py +0 -0
  25. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gather_contcar.py +0 -0
  26. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/gpumd_group.py +0 -0
  27. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/nep_rmse.py +0 -0
  28. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/pca.py +0 -0
  29. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/perturb.py +0 -0
  30. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/submit_vasp.py +0 -0
  31. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/substitute.py +0 -0
  32. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/supercell.py +0 -0
  33. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/symmetrize.py +0 -0
  34. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/trj_extract.py +0 -0
  35. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/trj_sparse.py +0 -0
  36. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/umap.py +0 -0
  37. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp2xyz.py +0 -0
  38. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_fe.py +0 -0
  39. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_relax.py +0 -0
  40. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/commands/vasp_static.py +0 -0
  41. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit/pymdkit_main.py +0 -0
  42. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/SOURCES.txt +0 -0
  43. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  44. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/entry_points.txt +0 -0
  45. {pymdkit-1.4.5 → pymdkit-1.4.7}/src/pymdkit.egg-info/requires.txt +0 -0
  46. {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.5
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 stable GPUMD trajectories and extract abnormal time regions |
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 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 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. Thermo plots use Arial when available and the Morandi color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and shade 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.
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`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. Missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
185
+ `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 stable GPUMD trajectories and extract abnormal time regions |
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 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 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. Thermo plots use Arial when available and the Morandi color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and shade 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.
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`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. Missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.4.5"
7
+ version = "1.4.7"
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.5"
3
+ __version__ = "1.4.7"
@@ -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,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
- 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
+ # 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
- 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
+ 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
- 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
- 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
- MORANDI_COLORS = [
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(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
+ )
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, end, color=MORANDI_COLORS[12], alpha=0.35, linewidth=0, zorder=0
60
- )
61
- axis.text(
62
- (begin + end) / 2.0,
63
- 0.97,
64
- "abnormal region",
65
- transform=axis.get_xaxis_transform(),
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=MORANDI_COLORS),
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 stable GPUMD trajectories and extract abnormal trajectory regions."""
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 stable GPUMD trajectories and extract abnormal regions."
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
- return job, (thermo, interval_ps), None
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 any(
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
- stable_jobs = []
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 reason:
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
- stable_jobs.append(job)
159
- print(f"{relative_folder}: stable - {job.frame_count} frame(s){config}")
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
- skipped += 1
166
- print(
167
- f"{relative_folder}: abnormal but no potential-energy region "
168
- f"mapped to trajectory frames - {details}"
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
- print(
175
- f"{relative_folder}: abnormal - extracted {len(indices)} of "
176
- f"{job.frame_count} frame(s){config} - {details}"
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, stable_jobs)
180
- write_merged(abnormal_output, abnormal_jobs)
181
- stable_frames = sum(job.frame_count for job in stable_jobs)
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
- print(
184
- f"Wrote {stable_frames} frame(s) from {len(stable_jobs)} stable job(s) "
185
- f"to {args.output}"
186
- )
187
- print(
188
- f"Wrote {abnormal_frames} frame(s) from {len(abnormal_jobs)} abnormal "
189
- f"job(s) to {args.abnormal_output}"
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.5
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 stable GPUMD trajectories and extract abnormal time regions |
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 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 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. Thermo plots use Arial when available and the Morandi color cycle. They plot all six pressure components, use frameless upper-right legends with added vertical headroom, preserve correct Å units, and shade 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.
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`. Stable trajectories are merged in deterministic folder order into `train.xyz` (or `-o`). For an abnormal job, frames from the structurally corroborated potential-energy region are written to `abnormal.xyz` (or `-ao`); if no energy region can be resolved, the dominant structural transition is used as a fallback so an abnormal job cannot produce an empty abnormal output. Frame times are obtained from `time_step` and `dump_xyz` in that job's `run.in`. Missing, unreadable, or unmappable jobs are skipped. When `<job>/model.xyz` contains `Config_type`, that value is applied to every exported frame while all other extended-XYZ bytes remain unchanged.
185
+ `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