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.
- {pymdkit-1.4.0/src/pymdkit.egg-info → pymdkit-1.4.2}/PKG-INFO +20 -3
- {pymdkit-1.4.0 → pymdkit-1.4.2}/README.md +21 -4
- {pymdkit-1.4.0 → pymdkit-1.4.2}/pyproject.toml +3 -3
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.4.2/src/pymdkit/commands/_extxyz.py +85 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_fileio.py +15 -8
- pymdkit-1.4.2/src/pymdkit/commands/_geometry.py +12 -0
- pymdkit-1.4.2/src/pymdkit/commands/_gpumd.py +348 -0
- pymdkit-1.4.2/src/pymdkit/commands/_reduction.py +165 -0
- pymdkit-1.4.2/src/pymdkit/commands/add_config_type.py +64 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_ehull.py +4 -8
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_rmsd.py +7 -17
- pymdkit-1.4.2/src/pymdkit/commands/gpumd_thermo.py +131 -0
- pymdkit-1.4.2/src/pymdkit/commands/gpumd_trjcat.py +198 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/pca.py +1 -1
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/substitute.py +7 -9
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/symmetrize.py +4 -14
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/trj_extract.py +3 -35
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/umap.py +1 -1
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp2xyz.py +9 -17
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_fe.py +3 -7
- {pymdkit-1.4.0 → pymdkit-1.4.2/src/pymdkit.egg-info}/PKG-INFO +20 -3
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/SOURCES.txt +4 -0
- pymdkit-1.4.0/src/pymdkit/commands/_extxyz.py +0 -36
- pymdkit-1.4.0/src/pymdkit/commands/_reduction.py +0 -99
- pymdkit-1.4.0/src/pymdkit/commands/gpumd_thermo.py +0 -250
- {pymdkit-1.4.0 → pymdkit-1.4.2}/LICENSE +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/setup.cfg +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/chemsys_entry.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/compute_msd_all_groups.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/convert.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/ewald.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/gather_contcar.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/gpumd_group.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/nep_rmse.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit/pymdkit_main.py +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/entry_points.txt +0 -0
- {pymdkit-1.4.0 → pymdkit-1.4.2}/src/pymdkit.egg-info/requires.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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]
|
|
@@ -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
|
-
|
|
58
|
-
|
|
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))
|