Metadata-Version: 2.4
Name: pymdkit
Version: 1.1.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: scipy
Requires-Dist: ase
Requires-Dist: pymatgen
Requires-Dist: mp_api
Requires-Dist: pyxtal
Requires-Dist: gemmi
Requires-Dist: tqdm
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 dependencies (numpy, scipy, ase, pymatgen, pyxtal, mp_api, gemmi,
tqdm).

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 |
| `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>/`) |
| `outcar2xyz` | Collect SCF-converged VASP job folders (any name) into one extxyz file |
| `rmsd` | Compute RMSD between two structure files, or all pairs in a folder |
| `select-candidate` | Split a NEP training set into candidate/accurate sets by energy error |
| `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 ehull --mp-api-key $MP_API_KEY              # scans ./ for VASP jobs -> ehull.txt
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 outcar2xyz                                  # scans ./ for OUTCAR folders -> scf-converged.xyz
pymdkit outcar2xyz --position-only                  # write positions only, without energy/forces/stress
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 select-candidate                            # RMSE bands: <low all accurate, >high all candidate, else worst 50%
pymdkit select-candidate -r 0.8                     # in the middle band, take worst 80% as candidate.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
`outcar2xyz`.

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

`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.

`ehull` auto-detects every sub-folder of the current path that contains a
`vasprun.xml`, 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
        |-- outcar2xyz.py
        |-- stru2xyz.py
        |-- substitute.py
        |-- supercell.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
```
