pymdkit 1.3.0__tar.gz → 1.4.1__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.1/PKG-INFO +278 -0
- pymdkit-1.4.1/README.md +252 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/pyproject.toml +7 -5
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/__init__.py +1 -1
- pymdkit-1.4.1/src/pymdkit/commands/_extxyz.py +85 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/_fileio.py +15 -8
- pymdkit-1.4.1/src/pymdkit/commands/_geometry.py +12 -0
- pymdkit-1.4.1/src/pymdkit/commands/_gpumd.py +172 -0
- pymdkit-1.4.1/src/pymdkit/commands/_reduction.py +165 -0
- pymdkit-1.3.0/src/pymdkit/commands/stable_entry.py → pymdkit-1.4.1/src/pymdkit/commands/chemsys_entry.py +2 -2
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/compute_ehull.py +4 -8
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/compute_msd_all_groups.py +2 -2
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/compute_rmsd.py +10 -20
- pymdkit-1.4.1/src/pymdkit/commands/convert.py +160 -0
- pymdkit-1.3.0/src/pymdkit/commands/electrostatic_energy.py → pymdkit-1.4.1/src/pymdkit/commands/ewald.py +3 -3
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/gather_contcar.py +1 -1
- pymdkit-1.3.0/src/pymdkit/commands/add_groups.py → pymdkit-1.4.1/src/pymdkit/commands/gpumd_group.py +1 -1
- pymdkit-1.4.1/src/pymdkit/commands/gpumd_thermo.py +131 -0
- pymdkit-1.4.1/src/pymdkit/commands/gpumd_trjcat.py +134 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/nep_rmse.py +28 -33
- pymdkit-1.4.1/src/pymdkit/commands/pca.py +39 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/substitute.py +11 -13
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/symmetrize.py +6 -16
- pymdkit-1.4.1/src/pymdkit/commands/trj_extract.py +99 -0
- pymdkit-1.4.1/src/pymdkit/commands/umap.py +38 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/vasp2xyz.py +9 -17
- pymdkit-1.3.0/src/pymdkit/commands/final_energy.py → pymdkit-1.4.1/src/pymdkit/commands/vasp_fe.py +4 -8
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/pymdkit_main.py +6 -6
- pymdkit-1.4.1/src/pymdkit.egg-info/PKG-INFO +278 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit.egg-info/SOURCES.txt +14 -5
- pymdkit-1.4.1/src/pymdkit.egg-info/entry_points.txt +2 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit.egg-info/requires.txt +2 -0
- pymdkit-1.3.0/PKG-INFO +0 -249
- pymdkit-1.3.0/README.md +0 -225
- pymdkit-1.3.0/src/pymdkit/commands/stru2xyz.py +0 -100
- pymdkit-1.3.0/src/pymdkit.egg-info/PKG-INFO +0 -249
- pymdkit-1.3.0/src/pymdkit.egg-info/entry_points.txt +0 -2
- {pymdkit-1.3.0 → pymdkit-1.4.1}/LICENSE +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/setup.cfg +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/__init__.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/_vasp_output.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/_vaspset.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/perturb.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/submit_vasp.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/supercell.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/vasp_relax.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit/commands/vasp_static.py +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit.egg-info/dependency_links.txt +0 -0
- {pymdkit-1.3.0 → pymdkit-1.4.1}/src/pymdkit.egg-info/top_level.txt +0 -0
pymdkit-1.4.1/PKG-INFO
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pymdkit
|
|
3
|
+
Version: 1.4.1
|
|
4
|
+
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
|
|
5
|
+
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
|
|
6
|
+
License-Expression: GPL-3.0-or-later
|
|
7
|
+
Keywords: materials-science,molecular-dynamics,vasp,gpumd,ase,pymatgen
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: numpy
|
|
17
|
+
Requires-Dist: ase
|
|
18
|
+
Requires-Dist: pymatgen
|
|
19
|
+
Requires-Dist: mp-api
|
|
20
|
+
Requires-Dist: py4vasp-core
|
|
21
|
+
Requires-Dist: pyxtal
|
|
22
|
+
Requires-Dist: dpdata
|
|
23
|
+
Requires-Dist: matplotlib
|
|
24
|
+
Requires-Dist: umap-learn
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# pymdkit
|
|
28
|
+
|
|
29
|
+
A single command-line tool that bundles a collection of atomistic / molecular-dynamics
|
|
30
|
+
structure scripts behind one executable: `pmk`. Instead of copying individual
|
|
31
|
+
scripts into each working folder and running `python some_script.py`, you install
|
|
32
|
+
`pymdkit` once and call any tool from anywhere as `pmk <command> [options]`.
|
|
33
|
+
|
|
34
|
+
Every command exposes named `-flags` (no positional guessing), and each underlying
|
|
35
|
+
script is still runnable on its own.
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
Create a clean conda environment, activate it, then install `pymdkit` with pip:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
conda create -n pymdkit python=3.10
|
|
43
|
+
conda activate pymdkit
|
|
44
|
+
pip install pymdkit
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
This installs the `pmk` command into the active conda environment, together
|
|
48
|
+
with its direct dependencies, including ASE, pymatgen, py4vasp-core, dpdata, matplotlib, and UMAP. Some of these packages may install their own transitive dependencies.
|
|
49
|
+
|
|
50
|
+
Verify:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pmk -version
|
|
54
|
+
pmk -help # lists every command
|
|
55
|
+
pmk <command> -help # shows that command's flags
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
Commands that transform structures accept either a single file (`-i`/`-o`) or a
|
|
61
|
+
whole folder (`-if`/`-of`); commands that analyse VASP runs scan the current
|
|
62
|
+
directory for job sub-folders automatically.
|
|
63
|
+
|
|
64
|
+
| Command | What it does |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
67
|
+
| `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
|
|
68
|
+
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
|
|
69
|
+
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
70
|
+
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
71
|
+
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
72
|
+
| `gpumd-trjcat` | Merge trajectories from GPUMD jobs with stable equilibrium thermo data |
|
|
73
|
+
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
74
|
+
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
75
|
+
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
76
|
+
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
77
|
+
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
78
|
+
| `perturb` | Generate perturbed structures with dpdata |
|
|
79
|
+
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
80
|
+
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
81
|
+
| `rmsd` | Compute RMSD between two structure files, or all pairs in a folder |
|
|
82
|
+
| `chemsys-entry` | Download stable Materials Project structures for a chemical system |
|
|
83
|
+
| `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
|
|
84
|
+
| `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
|
|
85
|
+
| `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
|
|
86
|
+
| `symmetrize` | Import space-group symmetry into a structure file (or folder) -> CIF |
|
|
87
|
+
| `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
|
|
88
|
+
| `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |
|
|
89
|
+
|
|
90
|
+
## Examples
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
pmk gpumd-group -i opted.cif -elements Li Y Cl -o model.xyz
|
|
94
|
+
pmk gpumd-group -if cifs/ -elements Li Y Cl -of cifs-grouped/ # whole folder
|
|
95
|
+
pmk gpumd-group -elements Li Y Cl # scan subfolders, tag each model.xyz in place
|
|
96
|
+
pmk convert -i opted.vasp -o opted.xyz # convert one structure
|
|
97
|
+
pmk convert -if vasp-opted -of cif-opted -oe cif # folder conversion target format
|
|
98
|
+
pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
|
|
99
|
+
pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
|
|
100
|
+
pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
|
|
101
|
+
pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -add-groups Li Y Cl
|
|
102
|
+
# GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
|
|
103
|
+
pmk vasp-relax -i opted.vasp # relax inputs in current dir
|
|
104
|
+
pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
|
|
105
|
+
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
106
|
+
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
107
|
+
pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
|
|
108
|
+
pmk msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion for conductivity
|
|
109
|
+
pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
|
|
110
|
+
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
111
|
+
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
112
|
+
pmk vasp-fe # scans ./ for VASP jobs -> final-energy.txt with convergence status
|
|
113
|
+
pmk gather-contcar -of vasp-opted # CONTCARs -> vasp-opted/<folder>.vasp
|
|
114
|
+
pmk gather-contcar -of vasp-opted -ehull 0.028 # only structures with E_hull < 0.028 eV/atom
|
|
115
|
+
pmk vasp2xyz # scans ./ for VASP output folders -> scf-converged.xyz
|
|
116
|
+
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
117
|
+
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
118
|
+
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
119
|
+
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
120
|
+
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
121
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
122
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta -d 1.01 1.02
|
|
123
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
|
|
124
|
+
pmk ewald -i Li3YCl6.cif
|
|
125
|
+
pmk ewald -if Li3YCl6-all
|
|
126
|
+
pmk nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
|
|
127
|
+
pmk nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
|
|
128
|
+
pmk perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
|
|
129
|
+
pmk pca -i descriptor.out -o pca-descriptor.txt -it train.xyz # also split descriptors by Config_type
|
|
130
|
+
pmk pca -i descriptor.out -o pca-descriptor.txt -fps 0.01 -it train.xyz
|
|
131
|
+
pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
|
|
132
|
+
pmk gpumd-thermo # current job, or recursively scan job folders
|
|
133
|
+
pmk gpumd-thermo -t thermo.out -in run.in
|
|
134
|
+
pmk gpumd-trjcat # stable ./<job>/traj.xyz files -> train.xyz
|
|
135
|
+
pmk gpumd-trjcat -i traj.xyz -o train.xyz
|
|
136
|
+
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
137
|
+
|
|
138
|
+
pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
139
|
+
pmk rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
|
|
140
|
+
pmk symmetrize -i opted.cif -symprec 0.01 -add_oxidation yes -o opted-symm.cif
|
|
141
|
+
pmk symmetrize -if my_cifs/ -symprec 0.01 -add_oxidation no -of my_cifs-symm
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
|
|
145
|
+
jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
|
|
146
|
+
`-if` creates one `./<name>/` folder per structure, and `-it` creates one
|
|
147
|
+
`./frame_N/` folder per trajectory frame - all directly in the current path.
|
|
148
|
+
|
|
149
|
+
They start from sensible default INCAR settings; override them by passing a
|
|
150
|
+
settings file with `-custom-setting FILE`. The file may be a Python-dict block
|
|
151
|
+
or `KEY = VALUE` lines (a `None`/blank value clears a tag):
|
|
152
|
+
|
|
153
|
+
```text
|
|
154
|
+
custom_settings = {
|
|
155
|
+
"ENCUT": "600.0",
|
|
156
|
+
"ISIF": "3",
|
|
157
|
+
"MAGMOM": None
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
`vasp-static -it traj.xyz` (also available on `vasp-relax`) reads a
|
|
162
|
+
multi-structure trajectory and writes one job sub-folder per frame
|
|
163
|
+
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
|
|
164
|
+
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
|
|
165
|
+
`vasp2xyz`.
|
|
166
|
+
|
|
167
|
+
`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.
|
|
168
|
+
|
|
169
|
+
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.
|
|
170
|
+
|
|
171
|
+
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`.
|
|
172
|
+
|
|
173
|
+
`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.
|
|
174
|
+
|
|
175
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. Jobs with missing, unreadable, or abnormal thermo data are skipped. Stable trajectories are merged in deterministic folder order. When `<job>/model.xyz` contains `Config_type`, that value is applied to every frame from the job while all other extended-XYZ bytes remain unchanged.
|
|
176
|
+
|
|
177
|
+
Each command's full flag list is in `pmk <command> -help`.
|
|
178
|
+
|
|
179
|
+
For long VASP batch submission, run `submit-vasp` with `nohup` and `&` if you
|
|
180
|
+
want it to keep sleeping, checking the queue, and submitting new jobs after you
|
|
181
|
+
exit the terminal:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
The command itself controls the loop: it submits until the active queue reaches
|
|
188
|
+
`-max-job-num`, sleeps when the queue is full, checks again, and continues until
|
|
189
|
+
all needed jobs are submitted. `nohup ... &` is what makes that loop continue in
|
|
190
|
+
the background after logout.
|
|
191
|
+
|
|
192
|
+
`substitute -ref` removes selected sites near reference sites and writes one
|
|
193
|
+
`<input-stem>_substitute.cif` in the current path. If `-d` is omitted, each
|
|
194
|
+
cutoff is `0.7 * (selected covalent radius + reference covalent radius)` using
|
|
195
|
+
the covalent radii from Cordero et al., Dalton Trans., 2008, 2832-2838.
|
|
196
|
+
|
|
197
|
+
VASP-output readers (`vasp2xyz`, `ehull`, `vasp-fe`, and other future VASP-output
|
|
198
|
+
commands) use the global priority `vaspout.h5 > vasprun.xml > OUTCAR`.
|
|
199
|
+
|
|
200
|
+
`ehull` auto-detects every sub-folder of the current path that contains a
|
|
201
|
+
supported VASP output, groups them by chemical system (elements ordered by
|
|
202
|
+
electronegativity, e.g. `Li-Y-Cl`), and builds/reuses one `mp_cache_<system>.json`
|
|
203
|
+
per system - so a pure Li-Y-Cl batch yields a single `mp_cache_Li-Y-Cl.json`, while
|
|
204
|
+
a mixed Li-Y-Cl + La-O batch yields both `mp_cache_Li-Y-Cl.json` and
|
|
205
|
+
`mp_cache_La-O.json`. (Formation energy is reported alongside E_hull in
|
|
206
|
+
`ehull.txt`.)
|
|
207
|
+
|
|
208
|
+
## Layout
|
|
209
|
+
|
|
210
|
+
```
|
|
211
|
+
pymdkit/
|
|
212
|
+
|-- pyproject.toml # package metadata + the `pmk` entry point
|
|
213
|
+
|-- README.md
|
|
214
|
+
`-- src/pymdkit/
|
|
215
|
+
|-- pymdkit_main.py # dispatcher: discovers and runs commands
|
|
216
|
+
`-- commands/ # one module per command
|
|
217
|
+
|-- _fileio.py # shared -i/-o/-if/-of helper (not a command)
|
|
218
|
+
|-- _gpumd.py # shared GPUMD run.in and thermo analysis
|
|
219
|
+
|-- _geometry.py # shared minimum-image geometry helper
|
|
220
|
+
|-- _vaspset.py # shared VASP input-set helper (not a command)
|
|
221
|
+
|-- gpumd_group.py
|
|
222
|
+
|-- gpumd_thermo.py
|
|
223
|
+
|-- gpumd_trjcat.py
|
|
224
|
+
|-- compute_ehull.py
|
|
225
|
+
|-- compute_rmsd.py
|
|
226
|
+
|-- ewald.py
|
|
227
|
+
|-- vasp_fe.py
|
|
228
|
+
|-- perturb.py
|
|
229
|
+
|-- nep_rmse.py
|
|
230
|
+
|-- chemsys_entry.py
|
|
231
|
+
|-- submit_vasp.py
|
|
232
|
+
|-- convert.py
|
|
233
|
+
|-- substitute.py
|
|
234
|
+
|-- supercell.py
|
|
235
|
+
|-- vasp2xyz.py
|
|
236
|
+
|-- vasp_relax.py
|
|
237
|
+
|-- vasp_static.py
|
|
238
|
+
|-- ...
|
|
239
|
+
`-- symmetrize.py
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Modules whose name starts with `_` are shared helpers and are skipped by the
|
|
243
|
+
dispatcher, so they never appear as commands.
|
|
244
|
+
|
|
245
|
+
## Adding a new tool later
|
|
246
|
+
|
|
247
|
+
Drop a module in `src/pymdkit/commands/` that defines four things:
|
|
248
|
+
|
|
249
|
+
```python
|
|
250
|
+
COMMAND = "my-tool" # the subcommand name you'll type
|
|
251
|
+
HELP = "One-line description."
|
|
252
|
+
|
|
253
|
+
def add_arguments(parser): # register flags
|
|
254
|
+
parser.add_argument("-input", required=True)
|
|
255
|
+
|
|
256
|
+
def run(args): # do the work; return an exit code (0 = ok)
|
|
257
|
+
...
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
if __name__ == "__main__": # keeps the script runnable on its own
|
|
261
|
+
import argparse
|
|
262
|
+
_p = argparse.ArgumentParser(description=__doc__)
|
|
263
|
+
add_arguments(_p)
|
|
264
|
+
raise SystemExit(run(_p.parse_args()))
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
It will appear in `pmk -help` automatically - no central registration needed.
|
|
268
|
+
Put heavy imports (pymatgen, ase, ...) inside `run()` where practical; the dispatcher
|
|
269
|
+
reads each command's name and help without importing it, so `pmk -help` stays
|
|
270
|
+
fast and a missing optional dependency only affects the one command that needs it.
|
|
271
|
+
|
|
272
|
+
## Running a script standalone
|
|
273
|
+
|
|
274
|
+
Every command module still works directly, which is handy for debugging:
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
|
|
278
|
+
```
|
pymdkit-1.4.1/README.md
ADDED
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# pymdkit
|
|
2
|
+
|
|
3
|
+
A single command-line tool that bundles a collection of atomistic / molecular-dynamics
|
|
4
|
+
structure scripts behind one executable: `pmk`. Instead of copying individual
|
|
5
|
+
scripts into each working folder and running `python some_script.py`, you install
|
|
6
|
+
`pymdkit` once and call any tool from anywhere as `pmk <command> [options]`.
|
|
7
|
+
|
|
8
|
+
Every command exposes named `-flags` (no positional guessing), and each underlying
|
|
9
|
+
script is still runnable on its own.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
Create a clean conda environment, activate it, then install `pymdkit` with pip:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
conda create -n pymdkit python=3.10
|
|
17
|
+
conda activate pymdkit
|
|
18
|
+
pip install pymdkit
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This installs the `pmk` command into the active conda environment, together
|
|
22
|
+
with its direct dependencies, including ASE, pymatgen, py4vasp-core, dpdata, matplotlib, and UMAP. Some of these packages may install their own transitive dependencies.
|
|
23
|
+
|
|
24
|
+
Verify:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pmk -version
|
|
28
|
+
pmk -help # lists every command
|
|
29
|
+
pmk <command> -help # shows that command's flags
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
Commands that transform structures accept either a single file (`-i`/`-o`) or a
|
|
35
|
+
whole folder (`-if`/`-of`); commands that analyse VASP runs scan the current
|
|
36
|
+
directory for job sub-folders automatically.
|
|
37
|
+
|
|
38
|
+
| Command | What it does |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `gpumd-group` | Tag atoms with a GPUMD group index by element order |
|
|
41
|
+
| `ewald` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
|
|
42
|
+
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
|
|
43
|
+
| `vasp-fe` | List VASP job final energies from lowest to highest |
|
|
44
|
+
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
|
|
45
|
+
| `gpumd-thermo` | Export and plot GPUMD thermo data and detect sustained equilibrium changes |
|
|
46
|
+
| `gpumd-trjcat` | Merge trajectories from GPUMD jobs with stable equilibrium thermo data |
|
|
47
|
+
| `pca` | Reduce descriptor data to two dimensions; optional FPS sampling |
|
|
48
|
+
| `trj-extract` | Extract a GPUMD XYZ trajectory by time range |
|
|
49
|
+
| `umap` | Reduce descriptor data to two dimensions with UMAP; optional FPS sampling |
|
|
50
|
+
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
|
|
51
|
+
| `nep-rmse` | Compute NEP energy/force/stress RMSE with terminal plots and optional candidate selection |
|
|
52
|
+
| `perturb` | Generate perturbed structures with dpdata |
|
|
53
|
+
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
|
|
54
|
+
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
|
|
55
|
+
| `rmsd` | Compute RMSD between two structure files, or all pairs in a folder |
|
|
56
|
+
| `chemsys-entry` | Download stable Materials Project structures for a chemical system |
|
|
57
|
+
| `convert` | Convert structures, trajectories, or folders between ASE/pymatgen formats |
|
|
58
|
+
| `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
|
|
59
|
+
| `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
|
|
60
|
+
| `symmetrize` | Import space-group symmetry into a structure file (or folder) -> CIF |
|
|
61
|
+
| `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
|
|
62
|
+
| `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |
|
|
63
|
+
|
|
64
|
+
## Examples
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pmk gpumd-group -i opted.cif -elements Li Y Cl -o model.xyz
|
|
68
|
+
pmk gpumd-group -if cifs/ -elements Li Y Cl -of cifs-grouped/ # whole folder
|
|
69
|
+
pmk gpumd-group -elements Li Y Cl # scan subfolders, tag each model.xyz in place
|
|
70
|
+
pmk convert -i opted.vasp -o opted.xyz # convert one structure
|
|
71
|
+
pmk convert -if vasp-opted -of cif-opted -oe cif # folder conversion target format
|
|
72
|
+
pmk convert -it train.xyz -ot train.extxyz # convert a complete trajectory
|
|
73
|
+
pmk supercell -i opted.vasp -o sc.vasp -max-abc 20 # cell lengths <= 20 A
|
|
74
|
+
pmk supercell -if vasp-opted -max-abc 20 -individual # per-structure ./<name>/<name>.<ext>
|
|
75
|
+
pmk supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -add-groups Li Y Cl
|
|
76
|
+
# GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
|
|
77
|
+
pmk vasp-relax -i opted.vasp # relax inputs in current dir
|
|
78
|
+
pmk vasp-relax -if optimal_occupancy # one ./<name>/ job folder per structure
|
|
79
|
+
pmk vasp-static -if cifs/ -custom-setting my_incar.txt # static inputs, custom INCAR
|
|
80
|
+
pmk vasp-static -it traj.xyz # one ./frame_N/ job per trajectory frame
|
|
81
|
+
pmk msd # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
|
|
82
|
+
pmk msd -diffuse_ion Li -ion_charge 1 # choose the mobile ion for conductivity
|
|
83
|
+
pmk chemsys-entry -s Li La Ta Cl # MP stable entries -> Li-La-Ta-Cl-stable-entries/
|
|
84
|
+
pmk ehull -mp-api-key $MP_API_KEY # scans ./ for VASP jobs -> ehull.txt
|
|
85
|
+
pmk ehull -local Li-La-Ta-Cl-stable-entries-opted
|
|
86
|
+
pmk vasp-fe # scans ./ for VASP jobs -> final-energy.txt with convergence status
|
|
87
|
+
pmk gather-contcar -of vasp-opted # CONTCARs -> vasp-opted/<folder>.vasp
|
|
88
|
+
pmk gather-contcar -of vasp-opted -ehull 0.028 # only structures with E_hull < 0.028 eV/atom
|
|
89
|
+
pmk vasp2xyz # scans ./ for VASP output folders -> scf-converged.xyz
|
|
90
|
+
pmk vasp2xyz -position-only # write positions only, without energy/forces/stress
|
|
91
|
+
pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
|
|
92
|
+
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
93
|
+
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
|
|
94
|
+
pmk substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
|
|
95
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
|
|
96
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta -d 1.01 1.02
|
|
97
|
+
pmk substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
|
|
98
|
+
pmk ewald -i Li3YCl6.cif
|
|
99
|
+
pmk ewald -if Li3YCl6-all
|
|
100
|
+
pmk nep-rmse # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
|
|
101
|
+
pmk nep-rmse -select -xyz train.xyz # interactive candidate selection; writes candidate.xyz and accurate.xyz
|
|
102
|
+
pmk perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz
|
|
103
|
+
pmk pca -i descriptor.out -o pca-descriptor.txt -it train.xyz # also split descriptors by Config_type
|
|
104
|
+
pmk pca -i descriptor.out -o pca-descriptor.txt -fps 0.01 -it train.xyz
|
|
105
|
+
pmk umap -i descriptor.out -o umap-descriptor.txt -fps 0.01 -it train.xyz
|
|
106
|
+
pmk gpumd-thermo # current job, or recursively scan job folders
|
|
107
|
+
pmk gpumd-thermo -t thermo.out -in run.in
|
|
108
|
+
pmk gpumd-trjcat # stable ./<job>/traj.xyz files -> train.xyz
|
|
109
|
+
pmk gpumd-trjcat -i traj.xyz -o train.xyz
|
|
110
|
+
pmk trj-extract -it traj.xyz -in run.in -b 100 -e 200 -o traj-100ps-200ps.xyz
|
|
111
|
+
|
|
112
|
+
pmk rmsd a.cif b.cif # RMSD of two files -> rmsd.txt
|
|
113
|
+
pmk rmsd vasp-opted/ # all pairs in a folder -> rmsd.txt
|
|
114
|
+
pmk symmetrize -i opted.cif -symprec 0.01 -add_oxidation yes -o opted-symm.cif
|
|
115
|
+
pmk symmetrize -if my_cifs/ -symprec 0.01 -add_oxidation no -of my_cifs-symm
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
|
|
119
|
+
jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
|
|
120
|
+
`-if` creates one `./<name>/` folder per structure, and `-it` creates one
|
|
121
|
+
`./frame_N/` folder per trajectory frame - all directly in the current path.
|
|
122
|
+
|
|
123
|
+
They start from sensible default INCAR settings; override them by passing a
|
|
124
|
+
settings file with `-custom-setting FILE`. The file may be a Python-dict block
|
|
125
|
+
or `KEY = VALUE` lines (a `None`/blank value clears a tag):
|
|
126
|
+
|
|
127
|
+
```text
|
|
128
|
+
custom_settings = {
|
|
129
|
+
"ENCUT": "600.0",
|
|
130
|
+
"ISIF": "3",
|
|
131
|
+
"MAGMOM": None
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
`vasp-static -it traj.xyz` (also available on `vasp-relax`) reads a
|
|
136
|
+
multi-structure trajectory and writes one job sub-folder per frame
|
|
137
|
+
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
|
|
138
|
+
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
|
|
139
|
+
`vasp2xyz`.
|
|
140
|
+
|
|
141
|
+
`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.
|
|
142
|
+
|
|
143
|
+
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.
|
|
144
|
+
|
|
145
|
+
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`.
|
|
146
|
+
|
|
147
|
+
`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.
|
|
148
|
+
|
|
149
|
+
`gpumd-trjcat` recursively scans job subfolders for `traj.xyz`, `thermo.out`, and `run.in`. Jobs with missing, unreadable, or abnormal thermo data are skipped. Stable trajectories are merged in deterministic folder order. When `<job>/model.xyz` contains `Config_type`, that value is applied to every frame from the job while all other extended-XYZ bytes remain unchanged.
|
|
150
|
+
|
|
151
|
+
Each command's full flag list is in `pmk <command> -help`.
|
|
152
|
+
|
|
153
|
+
For long VASP batch submission, run `submit-vasp` with `nohup` and `&` if you
|
|
154
|
+
want it to keep sleeping, checking the queue, and submitting new jobs after you
|
|
155
|
+
exit the terminal:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
nohup pmk submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The command itself controls the loop: it submits until the active queue reaches
|
|
162
|
+
`-max-job-num`, sleeps when the queue is full, checks again, and continues until
|
|
163
|
+
all needed jobs are submitted. `nohup ... &` is what makes that loop continue in
|
|
164
|
+
the background after logout.
|
|
165
|
+
|
|
166
|
+
`substitute -ref` removes selected sites near reference sites and writes one
|
|
167
|
+
`<input-stem>_substitute.cif` in the current path. If `-d` is omitted, each
|
|
168
|
+
cutoff is `0.7 * (selected covalent radius + reference covalent radius)` using
|
|
169
|
+
the covalent radii from Cordero et al., Dalton Trans., 2008, 2832-2838.
|
|
170
|
+
|
|
171
|
+
VASP-output readers (`vasp2xyz`, `ehull`, `vasp-fe`, and other future VASP-output
|
|
172
|
+
commands) use the global priority `vaspout.h5 > vasprun.xml > OUTCAR`.
|
|
173
|
+
|
|
174
|
+
`ehull` auto-detects every sub-folder of the current path that contains a
|
|
175
|
+
supported VASP output, groups them by chemical system (elements ordered by
|
|
176
|
+
electronegativity, e.g. `Li-Y-Cl`), and builds/reuses one `mp_cache_<system>.json`
|
|
177
|
+
per system - so a pure Li-Y-Cl batch yields a single `mp_cache_Li-Y-Cl.json`, while
|
|
178
|
+
a mixed Li-Y-Cl + La-O batch yields both `mp_cache_Li-Y-Cl.json` and
|
|
179
|
+
`mp_cache_La-O.json`. (Formation energy is reported alongside E_hull in
|
|
180
|
+
`ehull.txt`.)
|
|
181
|
+
|
|
182
|
+
## Layout
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
pymdkit/
|
|
186
|
+
|-- pyproject.toml # package metadata + the `pmk` entry point
|
|
187
|
+
|-- README.md
|
|
188
|
+
`-- src/pymdkit/
|
|
189
|
+
|-- pymdkit_main.py # dispatcher: discovers and runs commands
|
|
190
|
+
`-- commands/ # one module per command
|
|
191
|
+
|-- _fileio.py # shared -i/-o/-if/-of helper (not a command)
|
|
192
|
+
|-- _gpumd.py # shared GPUMD run.in and thermo analysis
|
|
193
|
+
|-- _geometry.py # shared minimum-image geometry helper
|
|
194
|
+
|-- _vaspset.py # shared VASP input-set helper (not a command)
|
|
195
|
+
|-- gpumd_group.py
|
|
196
|
+
|-- gpumd_thermo.py
|
|
197
|
+
|-- gpumd_trjcat.py
|
|
198
|
+
|-- compute_ehull.py
|
|
199
|
+
|-- compute_rmsd.py
|
|
200
|
+
|-- ewald.py
|
|
201
|
+
|-- vasp_fe.py
|
|
202
|
+
|-- perturb.py
|
|
203
|
+
|-- nep_rmse.py
|
|
204
|
+
|-- chemsys_entry.py
|
|
205
|
+
|-- submit_vasp.py
|
|
206
|
+
|-- convert.py
|
|
207
|
+
|-- substitute.py
|
|
208
|
+
|-- supercell.py
|
|
209
|
+
|-- vasp2xyz.py
|
|
210
|
+
|-- vasp_relax.py
|
|
211
|
+
|-- vasp_static.py
|
|
212
|
+
|-- ...
|
|
213
|
+
`-- symmetrize.py
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Modules whose name starts with `_` are shared helpers and are skipped by the
|
|
217
|
+
dispatcher, so they never appear as commands.
|
|
218
|
+
|
|
219
|
+
## Adding a new tool later
|
|
220
|
+
|
|
221
|
+
Drop a module in `src/pymdkit/commands/` that defines four things:
|
|
222
|
+
|
|
223
|
+
```python
|
|
224
|
+
COMMAND = "my-tool" # the subcommand name you'll type
|
|
225
|
+
HELP = "One-line description."
|
|
226
|
+
|
|
227
|
+
def add_arguments(parser): # register flags
|
|
228
|
+
parser.add_argument("-input", required=True)
|
|
229
|
+
|
|
230
|
+
def run(args): # do the work; return an exit code (0 = ok)
|
|
231
|
+
...
|
|
232
|
+
return 0
|
|
233
|
+
|
|
234
|
+
if __name__ == "__main__": # keeps the script runnable on its own
|
|
235
|
+
import argparse
|
|
236
|
+
_p = argparse.ArgumentParser(description=__doc__)
|
|
237
|
+
add_arguments(_p)
|
|
238
|
+
raise SystemExit(run(_p.parse_args()))
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
It will appear in `pmk -help` automatically - no central registration needed.
|
|
242
|
+
Put heavy imports (pymatgen, ase, ...) inside `run()` where practical; the dispatcher
|
|
243
|
+
reads each command's name and help without importing it, so `pmk -help` stays
|
|
244
|
+
fast and a missing optional dependency only affects the one command that needs it.
|
|
245
|
+
|
|
246
|
+
## Running a script standalone
|
|
247
|
+
|
|
248
|
+
Every command module still works directly, which is handy for debugging:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
|
|
252
|
+
```
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pymdkit"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.4.1"
|
|
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,9 +20,9 @@ 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 ppip install pymdkitp
|
|
24
24
|
# gives a fully working CLI. Heavy libraries are still imported lazily inside
|
|
25
|
-
# individual commands, so the dispatcher and
|
|
25
|
+
# individual commands, so the dispatcher and pmk -help stay fast.
|
|
26
26
|
dependencies = [
|
|
27
27
|
"numpy",
|
|
28
28
|
"ase",
|
|
@@ -31,11 +31,13 @@ dependencies = [
|
|
|
31
31
|
"py4vasp-core",
|
|
32
32
|
"pyxtal",
|
|
33
33
|
"dpdata",
|
|
34
|
+
"matplotlib",
|
|
35
|
+
"umap-learn",
|
|
34
36
|
]
|
|
35
37
|
|
|
36
38
|
[project.scripts]
|
|
37
|
-
#
|
|
38
|
-
|
|
39
|
+
# The distribution remains pymdkit; the concise public executable is ppmkp.
|
|
40
|
+
pmk = "pymdkit.pymdkit_main:main"
|
|
39
41
|
|
|
40
42
|
[tool.setuptools.packages.find]
|
|
41
43
|
where = ["src"]
|
|
@@ -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)
|