saddlemill 0.1.0__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 (45) hide show
  1. saddlemill-0.1.0/LICENSE +21 -0
  2. saddlemill-0.1.0/PKG-INFO +452 -0
  3. saddlemill-0.1.0/README.md +424 -0
  4. saddlemill-0.1.0/pyproject.toml +55 -0
  5. saddlemill-0.1.0/saddlemill/__main__.py +193 -0
  6. saddlemill-0.1.0/saddlemill/analyze_neb.py +1362 -0
  7. saddlemill-0.1.0/saddlemill/catsunami/autoframe.py +1688 -0
  8. saddlemill-0.1.0/saddlemill/catsunami/ocpneb.py +375 -0
  9. saddlemill-0.1.0/saddlemill/catsunami/reaction.py +99 -0
  10. saddlemill-0.1.0/saddlemill/config.py +1009 -0
  11. saddlemill-0.1.0/saddlemill/dimeropt.py +324 -0
  12. saddlemill-0.1.0/saddlemill/dimertools/kappa_dimer.py +201 -0
  13. saddlemill-0.1.0/saddlemill/dimertools/structure_edit.py +1067 -0
  14. saddlemill-0.1.0/saddlemill/geomopt.py +616 -0
  15. saddlemill-0.1.0/saddlemill/init_function.py +67 -0
  16. saddlemill-0.1.0/saddlemill/nebopt.py +647 -0
  17. saddlemill-0.1.0/saddlemill/nebtools/create_endpoints_for_MP_batteries.py +135 -0
  18. saddlemill-0.1.0/saddlemill/sellaopt.py +473 -0
  19. saddlemill-0.1.0/saddlemill/status.py +328 -0
  20. saddlemill-0.1.0/saddlemill/tools.py +1060 -0
  21. saddlemill-0.1.0/saddlemill/vasp_io.py +552 -0
  22. saddlemill-0.1.0/saddlemill.egg-info/PKG-INFO +452 -0
  23. saddlemill-0.1.0/saddlemill.egg-info/SOURCES.txt +43 -0
  24. saddlemill-0.1.0/saddlemill.egg-info/dependency_links.txt +1 -0
  25. saddlemill-0.1.0/saddlemill.egg-info/requires.txt +18 -0
  26. saddlemill-0.1.0/saddlemill.egg-info/top_level.txt +1 -0
  27. saddlemill-0.1.0/setup.cfg +4 -0
  28. saddlemill-0.1.0/tests/test_ase_vasp_incar.py +39 -0
  29. saddlemill-0.1.0/tests/test_config.py +829 -0
  30. saddlemill-0.1.0/tests/test_dimeropt_integration.py +242 -0
  31. saddlemill-0.1.0/tests/test_filter_neb_trajes.py +151 -0
  32. saddlemill-0.1.0/tests/test_geomopt_integration.py +321 -0
  33. saddlemill-0.1.0/tests/test_init_function.py +110 -0
  34. saddlemill-0.1.0/tests/test_kappa_changes.py +119 -0
  35. saddlemill-0.1.0/tests/test_main_integration.py +489 -0
  36. saddlemill-0.1.0/tests/test_nebopt_integration.py +274 -0
  37. saddlemill-0.1.0/tests/test_ocpneb.py +264 -0
  38. saddlemill-0.1.0/tests/test_sellaopt.py +848 -0
  39. saddlemill-0.1.0/tests/test_sp_resume.py +319 -0
  40. saddlemill-0.1.0/tests/test_sp_resume_modes.py +131 -0
  41. saddlemill-0.1.0/tests/test_spc_wrap_ordering.py +86 -0
  42. saddlemill-0.1.0/tests/test_status_csv_quoting.py +153 -0
  43. saddlemill-0.1.0/tests/test_structure_edit.py +676 -0
  44. saddlemill-0.1.0/tests/test_tools.py +773 -0
  45. saddlemill-0.1.0/tests/test_vasp_io.py +721 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ilgar Baghishov, Sung Hoon Jung and Graeme Henkelman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,452 @@
1
+ Metadata-Version: 2.4
2
+ Name: saddlemill
3
+ Version: 0.1.0
4
+ Summary: High-throughput transition state search using neural network potentials or DFT
5
+ Author: Ilgar Baghishov, Sung Hoon Jung, Graeme Henkelman
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/SciLM-ai/SaddleMill
8
+ Project-URL: Repository, https://github.com/SciLM-ai/SaddleMill
9
+ Project-URL: Dataset, https://huggingface.co/datasets/SciLM/MaterialsSaddles
10
+ Requires-Python: >=3.10
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Requires-Dist: ase>=3.26.0
14
+ Requires-Dist: fairchem-core>=2.19.0
15
+ Requires-Dist: numpy
16
+ Requires-Dist: scipy
17
+ Requires-Dist: matplotlib
18
+ Provides-Extra: test
19
+ Requires-Dist: pytest>=7.0; extra == "test"
20
+ Requires-Dist: pytest-timeout; extra == "test"
21
+ Provides-Extra: gpu
22
+ Requires-Dist: torch; extra == "gpu"
23
+ Provides-Extra: distributed
24
+ Requires-Dist: executorlib; extra == "distributed"
25
+ Provides-Extra: sella
26
+ Requires-Dist: sella>=2.5; extra == "sella"
27
+ Dynamic: license-file
28
+
29
+ # SaddleMill
30
+
31
+ High-throughput transition-state searches (Dimer, Sella, NEB) and minimizations with machine-learned potentials
32
+ or DFT, from one GPU to many nodes. It computed the
33
+ [MaterialsSaddles](https://huggingface.co/datasets/SciLM/MaterialsSaddles) dataset.
34
+
35
+ ## Install with pip (one GPU, no Flux)
36
+
37
+ ```bash
38
+ pip install "saddlemill[sella]" # [sella] adds Sella for method = Sella; the other methods do not need it
39
+ python -c "import torch; print(torch.cuda.is_available())" # should print True
40
+ ```
41
+
42
+ **Linux on ARM (aarch64), e.g. NVIDIA GH200:** PyPI has only CPU builds of torch for aarch64, so install the CUDA
43
+ build first: `pip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cu129` (`torch==2.8.0` from
44
+ the same index with fairchem-core 2.19–2.21).
45
+
46
+ This runs the jobs one after another in a single process: set `executorlib = False` in `config.ini` and run
47
+ `python -u -m saddlemill` (see [Running SaddleMill](#running-saddlemill)). UMA models are gated: accept the license
48
+ at [facebook/UMA](https://huggingface.co/facebook/UMA) and run `hf auth login`.
49
+
50
+ Running many jobs in parallel over GPUs and nodes needs executorlib and the Flux scheduler. `pip install
51
+ "saddlemill[distributed]"` adds executorlib, but Flux is not on PyPI (the PyPI package named `flux-core` is an
52
+ unrelated project): install it from conda-forge as in the HPC steps below.
53
+
54
+ ## Installation on HPC (multi-node, with Flux)
55
+
56
+ ### 1. Base Environment Setup
57
+
58
+ #### *3 A100 per node HPC*
59
+
60
+ Load necessary modules and create the base Conda environment.
61
+
62
+ ```bash
63
+ module unload impi python3
64
+ module load cuda/12.8
65
+
66
+ # Enter idev to get a GPU node
67
+ idev -A YOUR_ALLOCATION -p gpu-a100-dev -t 02:00:00
68
+
69
+ # Create base environment
70
+ conda create --prefix $WORK/conda_envs/executorlib -c conda-forge python=3.12 flux-core flux-sched openmpi=5.0.5 "libhwloc=*=cuda*" executorlib
71
+ conda activate executorlib
72
+
73
+ find $CONDA_PREFIX -name "sched-fluxion-*.so" -path "*feedstock_root*" -exec cp {} $CONDA_PREFIX/lib/flux/modules/ \;
74
+
75
+ ```
76
+
77
+ #### *GH200 HPC*
78
+
79
+ Load necessary modules and create the base Conda environment.
80
+
81
+ ```bash
82
+ # Create base environment
83
+ conda create -n executorlib -c conda-forge python=3.12 flux-core flux-sched "openmpi=5.0.5" executorlib
84
+ conda activate executorlib
85
+
86
+ # Install hardware locality
87
+ conda install "libhwloc=*=cuda*" -c conda-forge
88
+ export LD_LIBRARY_PATH=/opt/apps/cuda/12.4/targets/sbsa-linux/lib/:$LD_LIBRARY_PATH
89
+
90
+ find $CONDA_PREFIX -name "sched-fluxion-*.so" -path "*feedstock_root*" -exec cp {} $CONDA_PREFIX/lib/flux/modules/ \;
91
+ ```
92
+
93
+ **(Optional) MPI Support:**
94
+ If `mpi4py` is required, use the following (experimental):
95
+
96
+ ```bash
97
+ MPICC=$(which mpicc) pip install --no-binary=mpi4py mpi4py
98
+
99
+ ```
100
+
101
+ #### *4 A100 per node HPC*
102
+
103
+ Necessary modules (should be loaded by default):
104
+ * `PrgEnv-gnu/8.5.0` (Compiler Suite)
105
+ * `cudatoolkit/12.4` (System CUDA headers)
106
+ * `craype-accel-nvidia80` (Links MPI to A100 GPUs)
107
+ * `cray-mpich/8.1.30` (High-Speed Network Library)
108
+ * `python/3.11` (System Python)
109
+ * `conda/Miniforge3-24.7.1-0` (Access to mamba)
110
+
111
+ Other modules (should be loaded by default):
112
+ * `craype-x86-milan`
113
+ * `libfabric/1.22.0`
114
+ * `craype-network-ofi`
115
+ * `xpmem/2.9.7...`
116
+ * `cray-dsmml/0.3.0 `
117
+ * `cray-libsci/24.07.0`
118
+ * `craype/2.7.32`
119
+ * `gcc-native/13.2`
120
+ * `perftools-base/24.07.0`
121
+ * `cpe/24.07`
122
+ * `gpu/1.0`
123
+ * `sqs/2.0`
124
+ * `darshan/default`
125
+
126
+ Create base environment:
127
+
128
+ ```bash
129
+ # set mamba cache at scratch
130
+ mamba config --set pkgs_dirs $SCRATCH/.cache/conda
131
+
132
+ # Enter idev to get a GPU node
133
+ salloc --nodes 1 --qos interactive --time 04:00:00 --constraint gpu --gpus 4 --account YOUR_ALLOCATION
134
+
135
+ # Create base environment
136
+ mamba create -p $SCRATCH/envs/executorlib -c conda-forge python=3.12 \
137
+ flux-core flux-sched executorlib "libhwloc=*=cuda*" # warnings about cuda, ucx, nccl, etc. are ok
138
+ conda activate executorlib
139
+
140
+ ```
141
+
142
+ **(Optional) MPI Support:**
143
+ If `mpi4py` is required, use the following to compile it from source with Cray wrappers (experimental):
144
+
145
+ ```bash
146
+ MPICC="cc -shared" pip install --force-reinstall --no-cache-dir --no-binary=mpi4py mpi4py
147
+
148
+ ```
149
+
150
+ #### *In-house CPU cluster*
151
+
152
+ CPU-only (no GPU/CUDA), for VASP runs — no `torch`/`fairchem-core`. Create the env as `saddlemill` directly (no later clone):
153
+
154
+ ```bash
155
+ conda create -n saddlemill -c conda-forge python=3.12 flux-core flux-sched "openmpi=5.0.5" executorlib
156
+ conda activate saddlemill
157
+
158
+ find $CONDA_PREFIX -name "sched-fluxion-*.so" -path "*feedstock_root*" -exec cp {} $CONDA_PREFIX/lib/flux/modules/ \;
159
+
160
+ ```
161
+
162
+ ### 2. Verify Flux Resources
163
+
164
+ Check if flux detects all resources correctly:
165
+
166
+ ```bash
167
+ # For the GH200 HPC and the 4 A100 per node HPC
168
+ srun -n 2 flux start flux resource list
169
+ # For the 3 A100 per node HPC
170
+ srun -n 2 --mpi=pmi2 flux start flux resource list
171
+ # For the in-house CPU cluster (grab nodes first, e.g. salloc --nodes 2 --qos debug)
172
+ srun -n 2 --mpi=pmi2 flux start flux resource list
173
+
174
+ ```
175
+
176
+ ### 3. Application Specifics
177
+
178
+ Clone the environment and install specific machine learning libraries.
179
+
180
+ ```bash
181
+ conda create --prefix $WORK/conda_libraries/saddlemill --clone executorlib
182
+
183
+ pip config set global.cache-dir "/path/to/your/cache/directory" # like $SCRATCH/.cache/pip
184
+
185
+ pip install "fairchem-core>=2.19.0" "ase>=3.26.0" scipy==1.16
186
+ # If you want Vasp inpute files to be created like in omat/oc20:
187
+ pip install fairchem-data-omat
188
+ # If you will need some of the catsunami functionality or create Vasp input files for oc20
189
+ pip install fairchem-data-oc
190
+ # If you will need VaspInteractive
191
+ pip install git+https://github.com/tiangroup-uofa/vasp-interactive.git
192
+ # If you will need method = Sella (P-RFO saddle search) — see the note below
193
+ pip install sella
194
+
195
+ # This part below is only necessary for the GH200 HPC (not for the A100-based HPCs)
196
+ pip install "torch==2.9.0+cu128" --index-url https://download.pytorch.org/whl/cu128
197
+
198
+ ```
199
+
200
+ **Sella (`method = Sella`):** the P-RFO saddle optimiser is an *optional* dependency — SaddleMill only imports it inside `sellaopt.py`, so every other method works without it. Install it into the application env (the one holding `fairchem-core`/`ase`):
201
+
202
+ ```bash
203
+ conda activate /path/to/conda_envs/tsearch # your SaddleMill application env
204
+ pip freeze > /tmp/env_before.txt # snapshot, so any change is reversible
205
+ pip install --constraint <(grep -E "^[A-Za-z0-9_.-]+==[0-9]" /tmp/env_before.txt) sella
206
+ ```
207
+
208
+ The `--constraint` file pins every package already installed, so pip physically cannot upgrade `ase`, `numpy`, `scipy` or `torch` out from under `fairchem-core`. On Python 3.12 / x86-64, Sella 2.5.0 installs from a prebuilt `manylinux` wheel and adds only `sella`, `jax`, `jaxlib` and `ml_dtypes` — no compilation and no version changes. Verify with:
209
+
210
+ ```bash
211
+ python -c "import ase, torch, fairchem.core, sella; print(ase.__version__, torch.__version__)"
212
+ ```
213
+
214
+ If pip ever falls back to building Sella from source (older Python, non-x86, no wheel), the build needs a GNU compiler — NVIDIA's `nvc` rejects Sella's `-fno-strict-overflow`:
215
+
216
+ ```bash
217
+ CC=gcc CXX=g++ pip install --no-binary sella sella
218
+ ```
219
+
220
+ **VTST + VASP (manual ASE patch required for now):** ASE ≤ 3.28 writes the integer INCAR tag `DROTMAX` as a float (`10.000000`), which VASP's VTST dimer silently ignores (falls back to `RotMax 4`). Until fixed upstream, patch the installed ASE — in `ase/calculators/vasp/create_input.py`, move `'drotmax'` from `float_keys` to `int_keys`. The `ase` version pin will be bumped to the fixed release once available. (Guarded by `tests/test_ase_vasp_incar.py`.)
221
+
222
+ **In-house CPU cluster (VASP-only):** the `saddlemill` env is already the app env (no `--clone`); VASP needs no `fairchem-core`/`torch`. Install only:
223
+
224
+ ```bash
225
+ pip install "ase>=3.26.0" scipy==1.16 fairchem-data-omat fairchem-data-oc
226
+ ```
227
+
228
+ Add `fairchem-core>=2.19.0` only for the FAIRChem calculator or `.aselmdb` I/O.
229
+
230
+ ## Configuration
231
+
232
+ ### Hugging Face Login
233
+
234
+ The CLI login may encounter issues. Use the Python interface instead:
235
+
236
+ ```bash
237
+ python -c "import huggingface_hub; huggingface_hub.login()"
238
+
239
+ ```
240
+
241
+ Alternatively, export the token directly:
242
+
243
+ ```bash
244
+ export HF_TOKEN="***"
245
+
246
+ ```
247
+
248
+ ### Environment Variables
249
+
250
+ Add these to your `.bashrc` or run before execution to manage cache and paths:
251
+
252
+ ```bash
253
+ # Move cache to scratch to save home directory space
254
+ export FAIRCHEM_CACHE_DIR="$SCRATCH/.cache/fairchem"
255
+ export PYTHONPATH=<saddlemill_path>:$PYTHONPATH
256
+
257
+ ```
258
+
259
+ ## Usage
260
+
261
+ ### Interactive Node
262
+
263
+ Request an interactive node:
264
+
265
+ ```bash
266
+ idev -p gh-dev -N 1 -m 120 -A YOUR_ALLOCATION
267
+ # or for the 3 A100 per node HPC:
268
+ idev -p gpu-a100-dev -N 1 -m 120 -A YOUR_ALLOCATION
269
+ # or for the in-house CPU cluster:
270
+ salloc --nodes 2 --qos debug
271
+
272
+ ```
273
+
274
+ ### Runtime Requirements
275
+
276
+ Ensure you run these commands (or add to `.bashrc`) every time you log in or start a job:
277
+
278
+ ```bash
279
+ # On the GH200 HPC:
280
+ export PYTHONPATH=<saddlemill_path>:$PYTHONPATH
281
+ export FAIRCHEM_CACHE_DIR="$SCRATCH/.cache/fairchem"
282
+ export LD_LIBRARY_PATH=/opt/apps/cuda/12.4/targets/sbsa-linux/lib/:$LD_LIBRARY_PATH
283
+
284
+
285
+ # On the 3 A100 per node HPC:
286
+ export PYTHONPATH=<saddlemill_path>:$PYTHONPATH
287
+ export FAIRCHEM_CACHE_DIR="$SCRATCH/.cache/fairchem"
288
+ module unload impi python3
289
+ module load cuda/12.8
290
+
291
+
292
+ # On the 4 A100 per node HPC
293
+ export PYTHONPATH=<saddlemill_path>:$PYTHONPATH
294
+ export FAIRCHEM_CACHE_DIR="$SCRATCH/.cache/fairchem"
295
+ # --- START: FIX LIBRARY PATHS ---
296
+ export PY_SITE_PKGS=$(python -c "import site; print(site.getsitepackages()[0])")
297
+ export NVIDIA_DIR="${PY_SITE_PKGS}/nvidia"
298
+ # Prepend ALL Nvidia libraries to the load path
299
+ for lib in cuda_runtime nvjitlink cusparse cublas cufft cudnn curand cusolver nccl; do
300
+ export LD_LIBRARY_PATH="${NVIDIA_DIR}/${lib}/lib:${LD_LIBRARY_PATH}"
301
+ done
302
+ # --- END: FIX LIBRARY PATHS ---
303
+
304
+
305
+ # On the in-house CPU cluster (VASP-only; no CUDA/FAIRChem):
306
+ export PYTHONPATH=<saddlemill_path>:$PYTHONPATH
307
+ export VASP_PP_PATH=/path/to/vasp/potcars # directory holding potpaw_PBE.<version>/
308
+
309
+ ```
310
+
311
+ ### Running SaddleMill
312
+
313
+ Create a `config.ini` in your working directory (see `CLAUDE.md` for full reference), place your input `.traj` files in `dir_path` (subdirectories are scanned recursively), then launch:
314
+
315
+ ```bash
316
+ # Distributed (multi-node, multi-GPU)
317
+ srun -N $SLURM_NNODES -n $SLURM_NNODES --gpus-per-node=4 flux start python -u -m saddlemill
318
+
319
+ # Distributed on the in-house CPU cluster (no --gpus-per-node; --mpi=pmi2 + clear inherited
320
+ # PMI vars so the inner Flux/MPI doesn't clash with the outer srun):
321
+ srun -N $SLURM_NNODES -n $SLURM_NNODES --mpi=pmi2 flux start \
322
+ env -u PMI2_FD -u PMI_FD -u PMI2_RANK -u PMI_RANK -u PMI2_SIZE -u PMI_SIZE -u PMI2_SPROUTE \
323
+ python -u -m saddlemill
324
+
325
+ # Serial (single GPU, useful for debugging)
326
+ # Set executorlib = False in config.ini, then:
327
+ python -u -m saddlemill
328
+ ```
329
+
330
+ ### Resume and Continuation
331
+
332
+ SaddleMill automatically handles resume if a job times out or is interrupted:
333
+
334
+ - **Resume unfinished jobs**: Just resubmit with the same `config.ini`. By default (`run_jobs = remaining`), only jobs that never ran are picked up. Already-completed jobs are skipped.
335
+
336
+ - **Redo specific categories**: Set `run_jobs` to target specific outcomes. Selection is per-line: a job with mixed results (e.g., 3 converged + 3 not_converged Dimer attempts) is selectable by both categories. Only matching entries are redone; the rest stay.
337
+ ```ini
338
+ run_jobs = not_converged # Redo unconverged attempts/sub-bands/sides
339
+ run_jobs = converged # Redo converged entries (e.g., refine with VASP)
340
+ run_jobs = errored # Retry errored entries
341
+ run_jobs = all # Redo everything
342
+ ```
343
+
344
+ - **Continue from previous result** (`continue_from_result = True`, default): When re-running, SaddleMill extracts the last result from output trajs and continues optimization from there. For Dimer, each matching attempt continues with its eigenmode. For NEB, matching sub-bands continue from their extracted images. For DoubleMinimization, unconverged sides continue from their last state. Errored entries fall back to fresh generation. Set `continue_from_result = False` to generate fresh replacements (same reaction types / same sub-band endpoints).
345
+
346
+ - **Archiving**: Old files are fully backed up as `previous_{N}.zip`. Only matching entries are removed from active CSVs and output trajectories (per-attempt for Dimer, per-sub-band for NEB, per-side for DoubleMinimization).
347
+
348
+ - **Fresh start**: Delete `traj_files_ordered.json` and the output directories to start completely from scratch.
349
+
350
+ ### Dimer: `initial_guess` Reaction Type
351
+
352
+ The `initial_guess` reaction type is for running the dimer method on a **pre-prepared TS guess** from an external source (another code, a database, or a different SaddleMill method like NEB). It starts the dimer from the input geometry as-is with no displacement and no supercell expansion (even if `supercell = True`). If the input structure carries a mode, it seeds the dimer instead of a random guess: the exact Hessian mode `atoms.info['eigenmodes3'][0]` (from a SinglePoint `compute_eigenmodes` pass) is used first, else a saddle search's `atoms.info['approx_eigenmode']`.
353
+
354
+ ```ini
355
+ [ourDimer]
356
+ dataset_type = bulk # or oc
357
+ reaction_types = initial_guess
358
+ ```
359
+
360
+ This is different from `continue_from_result`, which is an automatic mechanism for continuing a previous SaddleMill run. Use `initial_guess` when bringing a TS from outside SaddleMill; `continue_from_result` handles the "pick up where I left off" case internally.
361
+
362
+ ### VASP input generation (`input_generator`)
363
+
364
+ By default the `[Vasp]` section *is* your INCAR — every tag you list there is written to the calculator. When you want a standard input recipe (e.g. OMat24 or OC20) instead of hand-writing every tag, set `input_generator` in the `[ourVasp]` section. (`[ourVasp]` holds SaddleMill's VASP-input knobs; `[Vasp]` stays a pure ASE pass-through.) It works for **all methods** (NEB, Dimer, Minimization, DoubleMinimization, SinglePoint).
365
+
366
+ ```ini
367
+ [ourVasp]
368
+ input_generator = omat24_static # omat24_static | omat24_relax | cheap_omat
369
+ # | oc20 | cheap_oc20 | oc22 | cheap_oc22
370
+ # ...or 'my_pkg.my_module:my_func'
371
+ # ...or '/path/to/my_inputs.py:my_func'
372
+
373
+ [Vasp]
374
+ encut = 600 # any [Vasp] tag overrides the generator
375
+ pp = PBE
376
+ xc = PBE
377
+ ```
378
+
379
+ Per-tag precedence is **`[Vasp]` key → `input_generator` output → ASE/VASP default**: a tag you set in `[Vasp]` always wins; a tag the generator supplies is used when `[Vasp]` is silent; a tag in neither falls back to ASE's default. The generator contributes only electronic/accuracy settings — ionic-driver tags (`IBRION`/`NSW`/`POTIM`/`EDIFFG`) are stripped, since SaddleMill drives the geometry through ASE.
380
+
381
+ A custom generator is any function `generator(atoms) -> dict` returning ASE-`Vasp` kwargs (lowercased INCAR tags plus `kpts`/`gamma`/`setups`/`magmom`); point `input_generator` at it as `module:func` or `file.py:func`. The `omat24_*`/`cheap_omat` built-ins need `fairchem-data-omat` and `oc20`/`cheap_oc20` need `fairchem-data-oc` installed (see the HPC installation above); `oc22`/`cheap_oc22` are self-contained (the exact Meta OC22 settings are frozen inside SaddleMill, since the upstream `OC22_dataset` code is deprecated and today's pymatgen has drifted from what Meta ran).
382
+
383
+ The `oc22` built-in reproduces the OC22 dataset's level of theory exactly (arXiv:2206.08917): PBE + Materials Project Hubbard U, spin-polarized with MP-default initial moments (magmoms already on the atoms win), ENCUT 500, Γ-centered `ceil(30/a) × ceil(30/b) × 1` meshes, 2022-era MPRelaxSet POTCAR setups, and dipole correction only when the structure has tag=2 adsorbate atoms (Meta applied it to adsorbate+slabs only). Meta used the PBE 5.4 POTCAR library — add `pp_version = 54` in `[Vasp]` if your `$VASP_PP_PATH` has versioned `potpaw_PBE.54` folders. The `cheap_oc20`/`cheap_oc22` variants follow the `cheap_omat` recipe: the in-plane k-point multiplier is halved (40→20 / 30→15) and POTCARs are lightened (`minimal` base, soft `_s` O/C/N, f-in-core lanthanides kept) so ENCUT can drop to ~300 via `[Vasp]` — spin, U and the functional are untouched, so the cheap pass stays on the same PES; reconverge with the exact preset afterwards.
384
+
385
+ ### Extra input files (`extra_input_files`), e.g. VTST MODECAR
386
+
387
+ `input_generator` only sets INCAR/k-points/POTCAR. To drop **extra files** into the VASP working directory — most usefully a VTST `MODECAR` (initial dimer mode) — use `extra_input_files`:
388
+
389
+ ```ini
390
+ [ourVasp]
391
+ extra_input_files = modecar # built-in | 'module:func' | 'file.py:func' | space-separated list
392
+ ```
393
+
394
+ The built-in `modecar` builds the file from the frame's lowest mode, reordered to POSCAR order: `atoms.info['eigenmodes3'][0]` (exact, from a SinglePoint `compute_eigenmodes` pass) if present, else `atoms.info['approx_eigenmode']` (the mode NEB/Dimer/Sella stamp on their output frames). A DoubleMinimization TS frame carries the mode it displaced along, as `eigenmodes3` when that mode was an exact input mode left unrefined and as `approx_eigenmode` otherwise. A custom writer is any `writer(calc, atoms, directory) -> None` (it receives the calculator, so it can use `calc.sort`).
395
+
396
+ Symmetrically, **`extra_outputs`** parses files back *out* of the VASP directory after the run and merges the result into the output frame's `.info`. A parser is any `parser(calc, atoms, directory) -> dict`; the built-in `vtst_dimer` returns `approx_eigenmode` (from `NEWMODECAR`, mapped back to atoms order via `calc.resort`) and `approx_curvature` (from `DIMCAR`).
397
+
398
+ Together these enable a **VASP-internal VTST dimer driven by SaddleMill as a pure launcher**:
399
+
400
+ ```ini
401
+ [ourVasp]
402
+ input_generator = omat24_static # base INCAR recipe
403
+ extra_input_files = modecar # write MODECAR from each frame's lowest mode
404
+ extra_outputs = vtst_dimer # read NEWMODECAR/DIMCAR -> approx_eigenmode/approx_curvature onto output
405
+
406
+ [Vasp] # VTST dimer driver tags (plain pass-through)
407
+ ichain = 2
408
+ ibrion = 3
409
+ potim = 0
410
+ iopt = 3 # QuickMin: force-driven step, no fixed jump
411
+ maxmove = 0.1
412
+ ddr = 0.005
413
+ nsw = 300
414
+ ediffg = -0.03
415
+ ```
416
+
417
+ with `method = SinglePoint`, `Calculator = Vasp`. SaddleMill runs one VASP call per structure, VASP runs the whole dimer internally, and the converged saddle (geometry + E/F) **plus** the refined `approx_eigenmode`/`approx_curvature` are written to the output traj. Use plain `Vasp` (not `VaspInteractive`, which forces `ibrion = -1`). The input frames must carry `atoms.info['approx_eigenmode']` (e.g. NEB-CI / Dimer / Sella outputs) or `atoms.info['eigenmodes3']` (a SinglePoint `compute_eigenmodes` pass), and a DoubleMinimization TS frame carries one of the two (see `modecar` above).
418
+
419
+ ## Known limitations
420
+
421
+ These edge cases fall outside the usual Dimer → DoubleMinimization workflow and are not fixed yet.
422
+
423
+ - **Fresh Dimer/Sella attempts reuse a stored mode.** When an input frame already carries a mode (`eigenmodes3` or `approx_eigenmode`, as on a Dimer, Sella, NEB-CI or SinglePoint `compute_eigenmodes` output), every fresh displacement attempt is seeded with that mode, although it was computed for the input geometry and the attempt starts from a displaced one. On bulk inputs the supercell or vacancy step changes the atom count, so the attempt fails with a shape error such as `operands could not be broadcast together with shapes (31,3) (4,3)`. `initial_guess` attempts and continuations are meant to reuse the mode and work as intended.
424
+ - **SinglePoint `.traj` output hides the input's mode.** Mode readers check the top level of `.info` and one `orig_info` level down, and a SinglePoint output frame stores only its own results at the top level. A DoubleMinimization run on the SinglePoint output of a Dimer run therefore stops with "Input structure missing 'eigenmodes3' or 'approx_eigenmode' in info." unless that SinglePoint pass wrote its own mode (`compute_eigenmodes = True`). Dimer and Sella outputs already carry energies and forces, so DoubleMinimization can read them directly.
425
+ - **`orig_info` history grows without limit.** Each job wraps the whole previous `.info` in a new `orig_info` level and none is ever removed, so a Dimer → DoubleMinimization cycle adds 2 levels. The usual 2 to 4 levels are harmless, but a long chain such as an AKMC loop would grow the stored frames at every step, and ASE's traj/aselmdb writers fail with a `RecursionError` between 950 and 1000 levels (about 475 Dimer → DoubleMinimization cycles, with Python's default recursion limit of 1000). Trimming old levels would also have to keep the provenance levels that `scripts/` join on.
426
+ - **Dimer runs are not bit-reproducible.** ASE's `MinModeAtoms` seeds its own random generator from the clock when no `random_seed` is given, and SaddleMill passes none, so two Dimer runs on the same input can differ even with fixed `random` and NumPy seeds.
427
+
428
+ ## Testing
429
+
430
+ Install test dependencies:
431
+
432
+ ```bash
433
+ pip install pytest pytest-timeout
434
+ ```
435
+
436
+ Run CPU-only unit tests (no GPU needed, works on login nodes):
437
+
438
+ ```bash
439
+ pytest -m "not gpu and not flux" -v
440
+ ```
441
+
442
+ Run all tests (requires a GPU node):
443
+
444
+ ```bash
445
+ pytest -v --timeout=600
446
+ ```
447
+
448
+ Run only GPU integration tests:
449
+
450
+ ```bash
451
+ pytest -m gpu -v
452
+ ```