Metadata-Version: 2.4
Name: pymdkit
Version: 1.2.5
Summary: A unified command-line toolkit for atomistic / MD structure workflows.
Author-email: Yueda Wang <ydwang0608@ustc.edu.cn>
License-Expression: GPL-3.0-or-later
Keywords: materials-science,molecular-dynamics,vasp,gpumd,ase,pymatgen
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Chemistry
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: numpy
Requires-Dist: ase
Requires-Dist: pymatgen
Requires-Dist: mp-api
Requires-Dist: pyxtal
Requires-Dist: dpdata
Dynamic: license-file

# pymdkit

A single command-line tool that bundles a collection of atomistic / molecular-dynamics
structure scripts behind one executable: `pymdkit`. Instead of copying individual
scripts into each working folder and running `python some_script.py`, you install
`pymdkit` once and call any tool from anywhere as `pymdkit <command> [options]`.

Every command exposes named `-flags` (no positional guessing), and each underlying
script is still runnable on its own.

## Install

Create a clean conda environment, activate it, then install `pymdkit` with pip:

```bash
conda create -n pymdkit python=3.10
conda activate pymdkit
pip install pymdkit
```

This installs the `pymdkit` command into the active conda environment, together
with its direct dependencies (numpy, ase, pymatgen, mp-api, pyxtal, dpdata). Some of these packages may install their own transitive dependencies.

Verify:

```bash
pymdkit -version
pymdkit -help                   # lists every command
pymdkit <command> -help         # shows that command's flags
```

## Commands

Commands that transform structures accept either a single file (`-i`/`-o`) or a
whole folder (`-if`/`-of`); commands that analyse VASP runs scan the current
directory for job sub-folders automatically.

| Command | What it does |
|---|---|
| `add-groups` | Tag atoms with a GPUMD group index by element order |
| `electrostatic-energy` | Compute CIF electrostatic energy with pymatgen EwaldSummation |
| `ehull` | Auto-detect VASP job folders and compute E_hull vs Materials Project |
| `final-energy` | List VASP job final energies from lowest to highest |
| `gather-contcar` | Collect CONTCARs from VASP job folders into one folder, renamed `<folder>.vasp` |
| `msd` | Diffusivity & conductivity from GPUMD MSD jobs (auto-scans `<structure>/<temp>/`) |
| `nep-rmse` | Compute NEP energy/force/stress RMSE with ASCII plots and optional candidate selection |
| `perturb` | Generate perturbed structures with dpdata |
| `vasp2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
| `submit-vasp` | Submit/resubmit VASP job folders while limiting active queue jobs |
| `rmsd` | Compute RMSD between two structure files, or all pairs in a folder |
| `stable-entry` | Download stable Materials Project structures for a chemical system |
| `stru2xyz` | Convert structure file(s) of any format to extxyz |
| `substitute` | Randomly substitute or remove selected atoms/sites from a structure |
| `supercell` | Build a supercell with cell lengths capped at a maximum (Angstrom); optional per-temperature GPUMD setup |
| `symmetrize` | Import space-group symmetry into a structure file (or folder) -> CIF |
| `vasp-relax` | Write VASP relaxation inputs for a structure (or folder); INCAR tags overridable |
| `vasp-static` | Write VASP static / single-point inputs for a structure (or folder) |

## Examples

```bash
pymdkit add-groups -i opted.cif -elements Li Y Cl -o model.xyz
pymdkit add-groups -if cifs/ -elements Li Y Cl -of cifs-grouped/   # whole folder
pymdkit add-groups -elements Li Y Cl                              # scan subfolders, tag each model.xyz in place
pymdkit stru2xyz -i opted.vasp -o opted.xyz                        # convert one file
pymdkit stru2xyz -if vasp-opted -of xyz-opted                      # convert a whole folder
pymdkit stru2xyz                                                  # scan subfolders, convert structures in place
pymdkit supercell -i opted.vasp -o sc.vasp -max-abc 20            # cell lengths <= 20 A
pymdkit supercell -if vasp-opted -max-abc 20 -individual          # per-structure ./<name>/<name>.<ext>
pymdkit supercell -if extxyz-opted -max-abc 24 -individual -temp 500 600 -md-if input-files -add-groups Li Y Cl
                                                                  # GPUMD: ./<name>/model.xyz (grouped) + ./<name>/<T>/ jobs
pymdkit vasp-relax  -i opted.vasp                                  # relax inputs in current dir
pymdkit vasp-relax  -if optimal_occupancy                          # one ./<name>/ job folder per structure
pymdkit vasp-static -if cifs/ -custom-setting my_incar.txt         # static inputs, custom INCAR
pymdkit vasp-static -it traj.xyz                                    # one ./frame_N/ job per trajectory frame
pymdkit msd                                         # scans <structure>/<temp>/ -> per-job msd/ + msd_summary.txt
pymdkit msd -diffuse_ion Li -ion_charge 1         # choose the mobile ion for conductivity
pymdkit stable-entry -s Li La Ta Cl                 # MP stable entries -> Li-La-Ta-Cl-stable-entries/
pymdkit ehull -mp-api-key $MP_API_KEY              # scans ./ for VASP jobs -> ehull.txt
pymdkit ehull -local Li-La-Ta-Cl-stable-entries-opted
pymdkit final-energy                                # scans ./ for VASP jobs -> final-energy.txt with convergence status
pymdkit gather-contcar -of vasp-opted               # CONTCARs -> vasp-opted/<folder>.vasp
pymdkit gather-contcar -of vasp-opted -ehull 0.028  # only structures with E_hull < 0.028 eV/atom
pymdkit vasp2xyz                                    # scans ./ for VASP output folders -> scf-converged.xyz
pymdkit vasp2xyz -position-only                    # write positions only, without energy/forces/stress
pymdkit submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30
nohup pymdkit submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
pymdkit substitute -i Li3YCl6.cif -se Li -sn 3 -we Na -wn 3 -on 100
pymdkit substitute -i Li3YCl6.cif -se Li -sn 3 -we none -on 100
pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li1 Li2 -sn 20 67 -we none -on 100
pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta -d 1.01 1.02
pymdkit substitute -i Li96Ta6La11Cl72.cif -se Li2 -we none -ref La Ta
pymdkit electrostatic-energy -i Li3YCl6.cif
pymdkit electrostatic-energy -if Li3YCl6-all
pymdkit nep-rmse                                    # writes energy/force/stress train txt files, rmse_value.txt, and terminal plots
pymdkit nep-rmse -select-candidate -xyz train.xyz   # interactive candidate selection; writes candidate.xyz and accurate.xyz
pymdkit perturb -i example.xyz -atom 0.2 -lattice 0.03 -n 100 -o example-perturb-atom-0.2-lattice-0.03.xyz

pymdkit rmsd a.cif b.cif                            # RMSD of two files -> rmsd.txt
pymdkit rmsd vasp-opted/                            # all pairs in a folder -> rmsd.txt
pymdkit symmetrize -i opted.cif -symprec 0.01 -add_oxidation yes -o opted-symm.cif
pymdkit symmetrize -if my_cifs/ -symprec 0.01 -add_oxidation no -of my_cifs-symm
```

VASP input commands (`vasp-relax`, `vasp-static`) always produce **individual**
jobs (one structure per folder): `-i` writes into the current dir (or `-o`),
`-if` creates one `./<name>/` folder per structure, and `-it` creates one
`./frame_N/` folder per trajectory frame - all directly in the current path.

They start from sensible default INCAR settings; override them by passing a
settings file with `-custom-setting FILE`. The file may be a Python-dict block
or `KEY = VALUE` lines (a `None`/blank value clears a tag):

```text
custom_settings = {
    "ENCUT": "600.0",
    "ISIF": "3",
    "MAGMOM": None
}
```

`vasp-static -it traj.xyz` (also available on `vasp-relax`) reads a
multi-structure trajectory and writes one job sub-folder per frame
(`frame_1/`, `frame_2/`, ..., prefix configurable via `-frame-prefix`). Each
folder also keeps a `frame_N.xyz`, so `Config_type` survives for a later
`vasp2xyz`.

Each command's full flag list is in `pymdkit <command> -help`.

For long VASP batch submission, run `submit-vasp` with `nohup` and `&` if you
want it to keep sleeping, checking the queue, and submitting new jobs after you
exit the terminal:

```bash
nohup pymdkit submit-vasp -subscript sub_vasp -queue slurm -max-job-num 30 > submit-vasp.log 2>&1 &
```

The command itself controls the loop: it submits until the active queue reaches
`-max-job-num`, sleeps when the queue is full, checks again, and continues until
all needed jobs are submitted. `nohup ... &` is what makes that loop continue in
the background after logout.

`substitute -ref` removes selected sites near reference sites and writes one
`<input-stem>_substitute.cif` in the current path. If `-d` is omitted, each
cutoff is `0.7 * (selected covalent radius + reference covalent radius)` using
the covalent radii from Cordero et al., Dalton Trans., 2008, 2832-2838.

VASP-output readers (`vasp2xyz`, `ehull`, and other future VASP-output
commands) use the global priority `vaspout.h5 > vasprun.xml > OUTCAR`.

`ehull` auto-detects every sub-folder of the current path that contains a
supported VASP output, groups them by chemical system (elements ordered by
electronegativity, e.g. `Li-Y-Cl`), and builds/reuses one `mp_cache_<system>.json`
per system - so a pure Li-Y-Cl batch yields a single `mp_cache_Li-Y-Cl.json`, while
a mixed Li-Y-Cl + La-O batch yields both `mp_cache_Li-Y-Cl.json` and
`mp_cache_La-O.json`. (Formation energy is reported alongside E_hull in
`ehull.txt`.)

## Layout

```
pymdkit/
|-- pyproject.toml              # package metadata + the `pymdkit` entry point
|-- README.md
`-- src/pymdkit/
    |-- pymdkit_main.py         # dispatcher: discovers and runs commands
    `-- commands/               # one module per command
        |-- _fileio.py          # shared -i/-o/-if/-of helper (not a command)
        |-- _vaspset.py         # shared VASP input-set helper (not a command)
        |-- add_groups.py
        |-- compute_ehull.py
        |-- compute_rmsd.py
        |-- electrostatic_energy.py
        |-- final_energy.py
        |-- perturb.py
        |-- nep_rmse.py
        |-- stable_entry.py
        |-- submit_vasp.py
        |-- stru2xyz.py
        |-- substitute.py
        |-- supercell.py
        |-- vasp2xyz.py
        |-- vasp_relax.py
        |-- vasp_static.py
        |-- ...
        `-- symmetrize.py
```

Modules whose name starts with `_` are shared helpers and are skipped by the
dispatcher, so they never appear as commands.

## Adding a new tool later

Drop a module in `src/pymdkit/commands/` that defines four things:

```python
COMMAND = "my-tool"                 # the subcommand name you'll type
HELP = "One-line description."

def add_arguments(parser):          # register flags
    parser.add_argument("-input", required=True)

def run(args):                      # do the work; return an exit code (0 = ok)
    ...
    return 0

if __name__ == "__main__":          # keeps the script runnable on its own
    import argparse
    _p = argparse.ArgumentParser(description=__doc__)
    add_arguments(_p)
    raise SystemExit(run(_p.parse_args()))
```

It will appear in `pymdkit -help` automatically - no central registration needed.
Put heavy imports (pymatgen, ase, ...) inside `run()` where practical; the dispatcher
reads each command's name and help without importing it, so `pymdkit -help` stays
fast and a missing optional dependency only affects the one command that needs it.

## Running a script standalone

Every command module still works directly, which is handy for debugging:

```bash
python src/pymdkit/commands/supercell.py -i in.cif -max-abc 20 -o sc.vasp
```
