pymdkit 1.4.0__tar.gz → 1.4.2__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 (48) hide show
  1. {pymdkit-1.4.0/src/pymdkit.egg-info → pymdkit-1.4.2}/PKG-INFO +20 -3
  2. {pymdkit-1.4.0 → pymdkit-1.4.2}/README.md +21 -4
  3. {pymdkit-1.4.0 → pymdkit-1.4.2}/pyproject.toml +3 -3
  4. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/__init__.py +1 -1
  5. pymdkit-1.4.2/src/pymdkit/commands/_extxyz.py +85 -0
  6. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_fileio.py +15 -8
  7. pymdkit-1.4.2/src/pymdkit/commands/_geometry.py +12 -0
  8. pymdkit-1.4.2/src/pymdkit/commands/_gpumd.py +348 -0
  9. pymdkit-1.4.2/src/pymdkit/commands/_reduction.py +165 -0
  10. pymdkit-1.4.2/src/pymdkit/commands/add_config_type.py +64 -0
  11. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_ehull.py +4 -8
  12. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_rmsd.py +7 -17
  13. pymdkit-1.4.2/src/pymdkit/commands/gpumd_thermo.py +131 -0
  14. pymdkit-1.4.2/src/pymdkit/commands/gpumd_trjcat.py +198 -0
  15. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/pca.py +1 -1
  16. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/substitute.py +7 -9
  17. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/symmetrize.py +4 -14
  18. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/trj_extract.py +3 -35
  19. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/umap.py +1 -1
  20. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp2xyz.py +9 -17
  21. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_fe.py +3 -7
  22. {pymdkit-1.4.0 → pymdkit-1.4.2/src/pymdkit.egg-info}/PKG-INFO +20 -3
  23. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/SOURCES.txt +4 -0
  24. pymdkit-1.4.0/src/pymdkit/commands/_extxyz.py +0 -36
  25. pymdkit-1.4.0/src/pymdkit/commands/_reduction.py +0 -99
  26. pymdkit-1.4.0/src/pymdkit/commands/gpumd_thermo.py +0 -250
  27. {pymdkit-1.4.0 → pymdkit-1.4.2}/LICENSE +0 -0
  28. {pymdkit-1.4.0 → pymdkit-1.4.2}/setup.cfg +0 -0
  29. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/__init__.py +0 -0
  30. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_vasp_output.py +0 -0
  31. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_vaspset.py +0 -0
  32. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
  33. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
  34. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/convert.py +0 -0
  35. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/ewald.py +0 -0
  36. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/gather_contcar.py +0 -0
  37. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/gpumd_group.py +0 -0
  38. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/nep_rmse.py +0 -0
  39. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/perturb.py +0 -0
  40. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/submit_vasp.py +0 -0
  41. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/supercell.py +0 -0
  42. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_relax.py +0 -0
  43. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_static.py +0 -0
  44. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/pymdkit_main.py +0 -0
  45. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
  46. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
  47. {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/requires.txt +0 -0
  48. {pymdkit-1.4.0 → pymdkit-1.4.2}/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.0
3
+ Version: 1.4.2
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
@@ -63,12 +63,14 @@ directory for job sub-folders automatically.
63
63
 
64
64
  | Command | What it does |
65
65
  |---|---|
66
+ | `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
66
67
  | `gpumd-group` | Tag atoms with a GPUMD group index by element order |
67
68
  | `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
68
69
  | `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
69
70
  | `vasp-fe` | List VASP job final energies from lowest to highest |
70
71
  | `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
71
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 |
72
74
  | `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
73
75
  | `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
74
76
  | `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
@@ -89,6 +91,8 @@ directory for job sub-folders automatically.
89
91
  ## Examples
90
92
 
91
93
  ```bash
94
+ pmk add-config-type -i single-structure.xyz -n P-3m1
95
+ pmk add-config-type -it trajectory.xyz -n P-3m1
92
96
  pmk gpumd-group -i opted.cif -elements Li Y Cl -o model.xyz
93
97
  pmk gpumd-group -if cifs/ -elements Li Y Cl -of cifs-grouped/ # whole folder
94
98
  pmk gpumd-group -elements Li Y Cl # scan subfolders, tag each model.xyz in place
@@ -125,11 +129,13 @@ pmk ewald -if Li3YCl6-all
125
129
  pmk nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
126
130
  pmk nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
127
131
  pmk perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
128
- pmk pca -i descriptor.out -o pca-descriptor.txt
132
+ pmk pca -i descriptor.out -o pca-descriptor.txt -it train.xyz # also split descriptors by Config_type
129
133
  pmk pca -i descriptor.out -o pca-descriptor.txt -fps 0.01 -it train.xyz
130
134
  pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
131
135
  pmk gpumd-thermo # current job, or recursively scan job folders
132
136
  pmk gpumd-thermo -t thermo.out -in run.in
137
+ pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
138
+ pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
133
139
  pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
134
140
 
135
141
  pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
@@ -161,11 +167,17 @@ multi-structure trajectory and writes one job sub-folder per frame
161
167
  folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
162
168
  `vasp2xyz`.
163
169
 
170
+ `add-config-type` updates its input atomically in place. With `-i`, the file must contain exactly one XYZ structure; use `-it` for a trajectory. Existing `Config_type` values are replaced, missing values are added, and all other extended-XYZ bytes remain unchanged.
171
+
164
172
  `nep-rmse -select`, PCA FPS, and UMAP FPS copy selected extended-XYZ frame blocks directly from the input trajectory. No parser rewrites energy, stress, forces, positions, precision, or extra metadata.
165
173
 
174
+ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every frame and writes one additional descriptor table per value, such as `pca-descriptor-P-3m1.txt` and `pca-descriptor-Pnma.txt`. With `-fps`, it also writes matching files such as `fps-0.01-pca-descriptor-P-3m1.txt` and `fps-0.01-train-P-3m1.xyz`. These files partition the single global FPS selection; FPS is not rerun independently for each `Config_type`. Frames without `Config_type` remain in the main outputs but do not receive a type-specific file.
175
+
166
176
  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`.
167
177
 
168
- `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. Its warning is a numerical reminder based on sustained drift or a level shift in the last `run` segment (or the last 80% when `run.in` has no `run` record); inspect the plot before deciding whether a trajectory is equilibrated.
178
+ `gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. Potential energy is the primary equilibrium signal: sustained potential-energy shifts, drifts, or localized departures define an abnormal job or time region. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
179
+
180
+ `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, only frames whose times fall in potential-energy abnormal regions are written to `abnormal.xyz` (or `-ao`); 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.
169
181
 
170
182
  Each command's full flag list is in `pmk <command> -help`.
171
183
 
@@ -208,14 +220,19 @@ pymdkit/
208
220
  |-- pymdkit_main.py # dispatcher: discovers and runs commands
209
221
  `-- commands/ # one module per command
210
222
  |-- _fileio.py # shared -i/-o/-if/-of helper (not a command)
223
+ |-- _gpumd.py # shared GPUMD run.in and thermo analysis
224
+ |-- _geometry.py # shared minimum-image geometry helper
211
225
  |-- _vaspset.py # shared VASP input-set helper (not a command)
212
226
  |-- gpumd_group.py
227
+ |-- gpumd_thermo.py
228
+ |-- gpumd_trjcat.py
213
229
  |-- compute_ehull.py
214
230
  |-- compute_rmsd.py
215
231
  |-- ewald.py
216
232
  |-- vasp_fe.py
217
233
  |-- perturb.py
218
234
  |-- nep_rmse.py
235
+ |-- add_config_type.py
219
236
  |-- chemsys_entry.py
220
237
  |-- submit_vasp.py
221
238
  |-- convert.py
@@ -37,12 +37,14 @@ directory for job sub-folders automatically.
37
37
 
38
38
  | Command | What it does |
39
39
  |---|---|
40
+ | `add-config-type` | Add Config_type to one XYZ structure or every trajectory frame |
40
41
  | `gpumd-group` | Tag atoms with a GPUMD group index by element order |
41
42
  | `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
42
43
  | `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
43
44
  | `vasp-fe` | List VASP job final energies from lowest to highest |
44
45
  | `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
45
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 |
46
48
  | `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
47
49
  | `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
48
50
  | `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
@@ -63,6 +65,8 @@ directory for job sub-folders automatically.
63
65
  ## Examples
64
66
 
65
67
  ```bash
68
+ pmk add-config-type -i single-structure.xyz -n P-3m1
69
+ pmk add-config-type -it trajectory.xyz -n P-3m1
66
70
  pmk gpumd-group -i opted.cif -elements Li Y Cl -o model.xyz
67
71
  pmk gpumd-group -if cifs/ -elements Li Y Cl -of cifs-grouped/ # whole folder
68
72
  pmk gpumd-group -elements Li Y Cl # scan subfolders, tag each model.xyz in place
@@ -99,11 +103,13 @@ pmk ewald -if Li3YCl6-all
99
103
  pmk nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
100
104
  pmk nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
101
105
  pmk perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
102
- pmk pca -i descriptor.out -o pca-descriptor.txt
106
+ pmk pca -i descriptor.out -o pca-descriptor.txt -it train.xyz # also split descriptors by Config_type
103
107
  pmk pca -i descriptor.out -o pca-descriptor.txt -fps 0.01 -it train.xyz
104
108
  pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
105
109
  pmk gpumd-thermo # current job, or recursively scan job folders
106
110
  pmk gpumd-thermo -t thermo.out -in run.in
111
+ pmk gpumd-trjcat # stable frames -> train.xyz; abnormal regions -> abnormal.xyz
112
+ pmk gpumd-trjcat -i traj.xyz -o train.xyz -ao abnormal.xyz
107
113
  pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
108
114
 
109
115
  pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
@@ -135,11 +141,17 @@ multi-structure trajectory and writes one job sub-folder per frame
135
141
  folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
136
142
  `vasp2xyz`.
137
143
 
144
+ `add-config-type` updates its input atomically in place. With `-i`, the file must contain exactly one XYZ structure; use `-it` for a trajectory. Existing `Config_type` values are replaced, missing values are added, and all other extended-XYZ bytes remain unchanged.
145
+
138
146
  `nep-rmse -select`, PCA FPS, and UMAP FPS copy selected extended-XYZ frame blocks directly from the input trajectory. No parser rewrites energy, stress, forces, positions, precision, or extra metadata.
139
147
 
148
+ When `pca` or `umap` receives `-it train.xyz`, it reads `Config_type` from every frame and writes one additional descriptor table per value, such as `pca-descriptor-P-3m1.txt` and `pca-descriptor-Pnma.txt`. With `-fps`, it also writes matching files such as `fps-0.01-pca-descriptor-P-3m1.txt` and `fps-0.01-train-P-3m1.xyz`. These files partition the single global FPS selection; FPS is not rerun independently for each `Config_type`. Frames without `Config_type` remain in the main outputs but do not receive a type-specific file.
149
+
140
150
  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`.
141
151
 
142
- `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. Its warning is a numerical reminder based on sustained drift or a level shift in the last `run` segment (or the last 80% when `run.in` has no `run` record); inspect the plot before deciding whether a trajectory is equilibrated.
152
+ `gpumd-thermo` writes `temperature.txt`, `potential-energy.txt`, `pressure.txt`, `lattice-parameters.txt`, `volume.txt`, `lattice-angles.txt`, and a headless `thermo.png` in every detected GPUMD job folder. Potential energy is the primary equilibrium signal: sustained potential-energy shifts, drifts, or localized departures define an abnormal job or time region. Lattice parameters, volume, temperature, pressure, and angles remain supporting diagnostics. The analysis uses the last `run` segment, or the last 80% when `run.in` has no `run` record; inspect the plot before making a final scientific judgment.
153
+
154
+ `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, only frames whose times fall in potential-energy abnormal regions are written to `abnormal.xyz` (or `-ao`); 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.
143
155
 
144
156
  Each command's full flag list is in `pmk <command> -help`.
145
157
 
@@ -181,15 +193,20 @@ pymdkit/
181
193
  `-- src/pymdkit/
182
194
  |-- pymdkit_main.py # dispatcher: discovers and runs commands
183
195
  `-- commands/ # one module per command
184
- |-- _fileio.py # shared -i/-o/-if/-of helper (not a command)
196
+ |-- _fileio.py # shared -i/-o/-if/-of helper (not a command)
197
+ |-- _gpumd.py # shared GPUMD run.in and thermo analysis
198
+ |-- _geometry.py # shared minimum-image geometry helper
185
199
  |-- _vaspset.py # shared VASP input-set helper (not a command)
186
- |-- gpumd_group.py
200
+ |-- gpumd_group.py
201
+ |-- gpumd_thermo.py
202
+ |-- gpumd_trjcat.py
187
203
  |-- compute_ehull.py
188
204
  |-- compute_rmsd.py
189
205
  |-- ewald.py
190
206
  |-- vasp_fe.py
191
207
  |-- perturb.py
192
208
  |-- nep_rmse.py
209
+ |-- add_config_type.py
193
210
  |-- chemsys_entry.py
194
211
  |-- submit_vasp.py
195
212
  |-- convert.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pymdkit"
7
- version = "1.4.0"
7
+ version = "1.4.2"
8
8
  description = "A unified command-line toolkit for atomistic / MD structure workflows."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -20,7 +20,7 @@ classifiers = [
20
20
  "Topic :: Scientific/Engineering :: Physics",
21
21
  ]
22
22
 
23
- # All dependencies are installed up front so a plain ppip install pymdkitp
23
+ # All dependencies are installed up front so a plain `pip install pymdkit`
24
24
  # gives a fully working CLI. Heavy libraries are still imported lazily inside
25
25
  # individual commands, so the dispatcher and pmk -help stay fast.
26
26
  dependencies = [
@@ -36,7 +36,7 @@ dependencies = [
36
36
  ]
37
37
 
38
38
  [project.scripts]
39
- # The distribution remains pymdkit; the concise public executable is ppmkp.
39
+ # The distribution remains pymdkit; the concise public executable is `pmk`.
40
40
  pmk = "pymdkit.pymdkit_main:main"
41
41
 
42
42
  [tool.setuptools.packages.find]
@@ -1,3 +1,3 @@
1
1
  """pymdkit -- a unified CLI for atomistic / MD structure workflows."""
2
2
 
3
- __version__ = "1.4.0"
3
+ __version__ = "1.4.2"
@@ -0,0 +1,85 @@
1
+ """Exact, byte-preserving helpers for extended-XYZ trajectories."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+
8
+ _CONFIG_TYPE_RE = re.compile(r'(?<!\S)Config_type=(?:"([^"]*)"|(\S+))')
9
+
10
+
11
+ def read_blocks(path):
12
+ """Return ``(blocks, atom_counts)`` without parsing or reformatting data."""
13
+ path = Path(path)
14
+ lines = path.read_bytes().splitlines(keepends=True)
15
+ blocks = []
16
+ atom_counts = []
17
+ index = 0
18
+ while index < len(lines):
19
+ if not lines[index].strip():
20
+ index += 1
21
+ continue
22
+ try:
23
+ count = int(lines[index].strip())
24
+ except ValueError as exc:
25
+ raise ValueError(
26
+ f"{path}: expected an atom count at line {index + 1}"
27
+ ) from exc
28
+ stop = index + count + 2
29
+ if stop > len(lines):
30
+ raise ValueError(f"{path}: incomplete frame beginning at line {index + 1}")
31
+ blocks.append(b"".join(lines[index:stop]))
32
+ atom_counts.append(count)
33
+ index = stop
34
+ return blocks, atom_counts
35
+
36
+
37
+ def write_blocks(path, blocks, indices):
38
+ """Write selected frames exactly as they appeared in the source file."""
39
+ Path(path).write_bytes(b"".join(blocks[int(index)] for index in indices))
40
+
41
+
42
+ def _comment_line(block):
43
+ lines = block.splitlines(keepends=True)
44
+ if len(lines) < 2:
45
+ raise ValueError("incomplete XYZ frame: missing comment line")
46
+ raw = lines[1]
47
+ if raw.endswith(b"\r\n"):
48
+ body, newline = raw[:-2], b"\r\n"
49
+ elif raw.endswith(b"\n") or raw.endswith(b"\r"):
50
+ body, newline = raw[:-1], raw[-1:]
51
+ else:
52
+ body, newline = raw, b""
53
+ return lines, body.decode("utf-8"), newline
54
+
55
+
56
+ def config_type_from_block(block):
57
+ """Return Config_type from one raw XYZ frame, if present."""
58
+ _, comment, _ = _comment_line(block)
59
+ match = _CONFIG_TYPE_RE.search(comment)
60
+ if not match:
61
+ return None
62
+ return match.group(1) if match.group(1) is not None else match.group(2)
63
+
64
+
65
+ def read_config_type(path):
66
+ """Read Config_type from the first frame of an XYZ file, if present."""
67
+ blocks, _ = read_blocks(path)
68
+ return config_type_from_block(blocks[0]) if blocks else None
69
+
70
+
71
+ def set_config_type(block, value):
72
+ """Set Config_type while preserving all other bytes in an XYZ frame."""
73
+ lines, comment, newline = _comment_line(block)
74
+ value = str(value)
75
+ if re.fullmatch(r'[^\s"=]+', value):
76
+ encoded_value = value
77
+ else:
78
+ encoded_value = '"' + value.replace('\\', '\\\\').replace('"', '\\"') + '"'
79
+ assignment = f"Config_type={encoded_value}"
80
+ if _CONFIG_TYPE_RE.search(comment):
81
+ comment = _CONFIG_TYPE_RE.sub(assignment, comment, count=1)
82
+ else:
83
+ comment += (" " if comment else "") + assignment
84
+ lines[1] = comment.encode("utf-8") + newline
85
+ return b"".join(lines)
@@ -13,8 +13,15 @@ from pathlib import Path
13
13
  # File extensions we treat as structures when scanning an input folder.
14
14
  STRUCT_EXTS = {
15
15
  ".cif", ".vasp", ".poscar", ".xyz", ".extxyz", ".pdb", ".xsf",
16
- ".cfg", ".gen", ".res", ".json", ".cssr", ".lmp", ".data",
16
+ ".cfg", ".gen", ".res", ".json", ".cssr", ".lmp", ".data", ".contcar",
17
17
  }
18
+ STRUCT_NAMES = {"POSCAR", "CONTCAR"}
19
+
20
+
21
+ def leading_index(name):
22
+ """Return a leading integer, placing nonnumeric names after numbered ones."""
23
+ match = re.match(r"^(\d+)", str(name))
24
+ return int(match.group(1)) if match else 10 ** 9
18
25
 
19
26
 
20
27
  def folder_sort_key(name):
@@ -54,8 +61,12 @@ def is_structure(path, exts=None):
54
61
  """True if *path* looks like a structure file (by extension / POSCAR name)."""
55
62
  exts = STRUCT_EXTS if exts is None else exts
56
63
  p = Path(path)
57
- return p.is_file() and (p.suffix.lower() in exts
58
- or p.name.upper().startswith("POSCAR"))
64
+ upper_name = p.name.upper()
65
+ return p.is_file() and (
66
+ p.suffix.lower() in exts
67
+ or upper_name in STRUCT_NAMES
68
+ or upper_name.startswith("POSCAR")
69
+ )
59
70
 
60
71
 
61
72
  def structure_files(folder):
@@ -63,11 +74,7 @@ def structure_files(folder):
63
74
  folder = Path(folder)
64
75
  if not folder.is_dir():
65
76
  raise SystemExit(f"Error: input folder not found: {folder}")
66
- files = sorted(
67
- p for p in folder.iterdir()
68
- if p.is_file() and (p.suffix.lower() in STRUCT_EXTS
69
- or p.name.upper().startswith("POSCAR"))
70
- )
77
+ files = sorted(p for p in folder.iterdir() if is_structure(p))
71
78
  if not files:
72
79
  raise SystemExit(f"Error: no structure files found in {folder}")
73
80
  return files
@@ -0,0 +1,12 @@
1
+ """Small geometry helpers shared by structure commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def fractional_distance(a, b):
7
+ """Return minimum-image Euclidean distance in fractional coordinates."""
8
+ import numpy as np
9
+
10
+ difference = np.asarray(a, dtype=float) - np.asarray(b, dtype=float)
11
+ difference -= np.round(difference)
12
+ return float(np.linalg.norm(difference))