saddleflow 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.
- saddleflow-0.1.0/.claude/scheduled_tasks.lock +1 -0
- saddleflow-0.1.0/.github/workflows/publish.yml +36 -0
- saddleflow-0.1.0/.gitignore +75 -0
- saddleflow-0.1.0/CLAUDE.md +206 -0
- saddleflow-0.1.0/LICENSE +21 -0
- saddleflow-0.1.0/PKG-INFO +233 -0
- saddleflow-0.1.0/README.md +194 -0
- saddleflow-0.1.0/examples/LematBulk/README.md +50 -0
- saddleflow-0.1.0/examples/LematBulk/run.sh +170 -0
- saddleflow-0.1.0/examples/LiC/README.md +114 -0
- saddleflow-0.1.0/examples/LiC/plot_throws.py +262 -0
- saddleflow-0.1.0/examples/LiC/random_throw.py +223 -0
- saddleflow-0.1.0/examples/LiC/test_set.traj +0 -0
- saddleflow-0.1.0/examples/LiC/train.py +174 -0
- saddleflow-0.1.0/examples/LiC/train_set.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/README.md +139 -0
- saddleflow-0.1.0/examples/LiC_simpler/make_small_cell.py +145 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_debug_zips/structure_rank_None_data.zip +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_status_csvs/status_rank_None.csv +1 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_trajes/collected_ts_rank_None.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/README.md +75 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/config.ini +47 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/data/LiC_simpler_RP.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/make_input.py +41 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/run.sh +37 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/saddlemill.log +137 -0
- saddleflow-0.1.0/examples/LiC_simpler/neb/traj_files_ordered.json +1 -0
- saddleflow-0.1.0/examples/LiC_simpler/one_saddle.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/small_n5_one_saddle.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/small_n5_six_saddles.traj +0 -0
- saddleflow-0.1.0/examples/LiC_simpler/train.py +210 -0
- saddleflow-0.1.0/examples/LiC_simpler/viz_checkpoints.py +299 -0
- saddleflow-0.1.0/examples/MP20Bat/README.md +152 -0
- saddleflow-0.1.0/examples/MP20Bat/analysis_fmax_parity.py +165 -0
- saddleflow-0.1.0/examples/MP20Bat/compare_K10_K50.py +511 -0
- saddleflow-0.1.0/examples/MP20Bat/data_prep.py +263 -0
- saddleflow-0.1.0/examples/MP20Bat/eval_full_testset_K10.py +586 -0
- saddleflow-0.1.0/examples/MP20Bat/replot_parity.py +162 -0
- saddleflow-0.1.0/examples/MP20Bat/run.sh +201 -0
- saddleflow-0.1.0/examples/MP20Bat/sample_and_distance_eval.py +730 -0
- saddleflow-0.1.0/examples/MP20Bat/train.py +532 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/README.md +258 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/analysis_fmax_parity.py +165 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/build_pairs.py +72 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/check_pretrained.py +56 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/compare_K10_K50.py +511 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/data_prep.py +297 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_multistart.py +82 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_predictions.py +196 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws.py +103 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws2.py +117 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws3.py +125 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws_batched.py +141 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/eval_full_testset_K10.py +645 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/eval_mp20bat_testset.sh +92 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/export_pretrained.py +76 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/replot_parity.py +162 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/run.sh +300 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/sample_and_distance_eval.py +730 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/sella_eval.py +139 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/sm_collect.py +72 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/train.py +1008 -0
- saddleflow-0.1.0/examples/MaterialsSaddles/verify_hyperparams.py +183 -0
- saddleflow-0.1.0/examples/quickstart/README.md +9 -0
- saddleflow-0.1.0/examples/quickstart/quickstart.py +35 -0
- saddleflow-0.1.0/examples/quickstart/reactant.traj +0 -0
- saddleflow-0.1.0/examples/scaling_bench/README.md +113 -0
- saddleflow-0.1.0/examples/scaling_bench/analyze.py +312 -0
- saddleflow-0.1.0/examples/scaling_bench/bench_launch.sh +166 -0
- saddleflow-0.1.0/examples/scaling_bench/prestage.sh +45 -0
- saddleflow-0.1.0/examples/scaling_bench/sweep.sh +62 -0
- saddleflow-0.1.0/pyproject.toml +53 -0
- saddleflow-0.1.0/saddleflow/__init__.py +14 -0
- saddleflow-0.1.0/saddleflow/cli.py +59 -0
- saddleflow-0.1.0/saddleflow/data/__init__.py +31 -0
- saddleflow-0.1.0/saddleflow/data/convert_to_db.py +124 -0
- saddleflow-0.1.0/saddleflow/data/core.py +200 -0
- saddleflow-0.1.0/saddleflow/data/db_dataset.py +80 -0
- saddleflow-0.1.0/saddleflow/data/materials_saddles_dataset.py +430 -0
- saddleflow-0.1.0/saddleflow/data/traj_dataset.py +123 -0
- saddleflow-0.1.0/saddleflow/data/transforms.py +100 -0
- saddleflow-0.1.0/saddleflow/flow/__init__.py +17 -0
- saddleflow-0.1.0/saddleflow/flow/matching.py +1096 -0
- saddleflow-0.1.0/saddleflow/flow/sampler.py +416 -0
- saddleflow-0.1.0/saddleflow/models/__init__.py +11 -0
- saddleflow-0.1.0/saddleflow/models/eigenmode_head.py +56 -0
- saddleflow-0.1.0/saddleflow/models/global_attn.py +96 -0
- saddleflow-0.1.0/saddleflow/models/time_film.py +118 -0
- saddleflow-0.1.0/saddleflow/models/time_filmed_backbone.py +300 -0
- saddleflow-0.1.0/saddleflow/models/velocity_head.py +483 -0
- saddleflow-0.1.0/saddleflow/pretrained.py +210 -0
- saddleflow-0.1.0/saddleflow/search.py +146 -0
- saddleflow-0.1.0/saddleflow/utils/__init__.py +34 -0
- saddleflow-0.1.0/saddleflow/utils/backbone.py +138 -0
- saddleflow-0.1.0/saddleflow/utils/checkpointing.py +69 -0
- saddleflow-0.1.0/saddleflow/utils/eval.py +416 -0
- saddleflow-0.1.0/saddleflow/utils/forces.py +167 -0
- saddleflow-0.1.0/saddleflow/utils/training.py +686 -0
- saddleflow-0.1.0/tests/test_autocast_precision.py +70 -0
- saddleflow-0.1.0/tests/test_pretrained.py +119 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"sessionId":"94b4102a-2392-429c-bad0-9e0b075ba740","pid":2648357,"procStart":"49722193","acquiredAt":1778081663965}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Build the package and upload it to PyPI when a GitHub release is published. Uses PyPI trusted publishing
|
|
2
|
+
# (the `pypi` environment of this repository is registered as a publisher on PyPI), so no token is stored.
|
|
3
|
+
name: Publish to PyPI
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
release:
|
|
7
|
+
types: [published]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
- uses: actions/setup-python@v5
|
|
15
|
+
with:
|
|
16
|
+
python-version: "3.12"
|
|
17
|
+
- run: python -m pip install build twine
|
|
18
|
+
- run: python -m build
|
|
19
|
+
- run: python -m twine check dist/*
|
|
20
|
+
- uses: actions/upload-artifact@v4
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/download-artifact@v4
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.pyc
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.pytest_cache/
|
|
6
|
+
.venv/
|
|
7
|
+
.eggs/
|
|
8
|
+
build/
|
|
9
|
+
dist/
|
|
10
|
+
|
|
11
|
+
# PyTorch / checkpoints
|
|
12
|
+
*.pt
|
|
13
|
+
*.pth
|
|
14
|
+
*.ckpt
|
|
15
|
+
*.safetensors
|
|
16
|
+
|
|
17
|
+
# Training outputs
|
|
18
|
+
checkpoints/
|
|
19
|
+
results/
|
|
20
|
+
logs/
|
|
21
|
+
wandb/
|
|
22
|
+
lightning_logs/
|
|
23
|
+
|
|
24
|
+
# Example-run output directories. Nothing under runs/ is tracked, so a user's
|
|
25
|
+
# first training leaves a clean `git status`. To commit a reference figure
|
|
26
|
+
# anyway, force it: `git add -f examples/<case>/runs/<run>/viz/foo.png`.
|
|
27
|
+
**/runs/
|
|
28
|
+
|
|
29
|
+
# Belt-and-braces for checkpoints written outside a runs/ directory: keep light
|
|
30
|
+
# files (*.json, *.png, *.npz) but never per-checkpoint heavy tensors (model
|
|
31
|
+
# weights, optimizer state, EMA shadow, RNG state, LR-scheduler state) or
|
|
32
|
+
# intermediate epoch checkpoints.
|
|
33
|
+
**/checkpoint_epoch_*/
|
|
34
|
+
**/checkpoint_final/model.safetensors
|
|
35
|
+
**/checkpoint_final/optimizer.bin
|
|
36
|
+
**/checkpoint_final/scheduler.bin
|
|
37
|
+
**/checkpoint_final/ema.pt
|
|
38
|
+
**/checkpoint_final/random_states_*.pkl
|
|
39
|
+
|
|
40
|
+
# Data directories (datasets not tracked in git) anywhere in the tree,
|
|
41
|
+
# with an explicit exception for saddleflow/data/ (the source package).
|
|
42
|
+
data/
|
|
43
|
+
!saddleflow/data/
|
|
44
|
+
!saddleflow/data/**
|
|
45
|
+
# ...and for the recorded SaddleMill NEB input of the LiC_simpler example (10 KB).
|
|
46
|
+
!examples/LiC_simpler/neb/data/
|
|
47
|
+
!examples/LiC_simpler/neb/data/**
|
|
48
|
+
# Re-exclude cache/build artifacts that the negation above would otherwise un-ignore.
|
|
49
|
+
saddleflow/data/**/__pycache__/
|
|
50
|
+
saddleflow/data/**/*.pyc
|
|
51
|
+
*.lmdb
|
|
52
|
+
*.h5
|
|
53
|
+
*.hdf5
|
|
54
|
+
|
|
55
|
+
# SLURM
|
|
56
|
+
slurm*.log
|
|
57
|
+
slurm-*.out
|
|
58
|
+
*.err
|
|
59
|
+
|
|
60
|
+
# OS / editors
|
|
61
|
+
.DS_Store
|
|
62
|
+
.idea/
|
|
63
|
+
.vscode/
|
|
64
|
+
|
|
65
|
+
# Reference codes — kept locally for design reference; will be deleted before public release
|
|
66
|
+
other_codes/
|
|
67
|
+
|
|
68
|
+
# Private experiments tree (its own git repo; not part of the public package)
|
|
69
|
+
private/
|
|
70
|
+
|
|
71
|
+
# NeurIPS rebuttal working directory (uploaded to OpenReview, not part of the package)
|
|
72
|
+
rebuttal/
|
|
73
|
+
|
|
74
|
+
# Personal / machine-specific Claude notes (research log, cluster paths); never commit
|
|
75
|
+
CLAUDE.local.md
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Guide for anyone (person or coding agent) working on SaddleFlow. Machine- or person-specific notes (cluster paths,
|
|
4
|
+
job logs, unpublished experiments) belong in an untracked `CLAUDE.local.md` next to this file; it is git-ignored and
|
|
5
|
+
must never be committed.
|
|
6
|
+
|
|
7
|
+
## What SaddleFlow is
|
|
8
|
+
|
|
9
|
+
SaddleFlow generates transition-state (first-order saddle) structures for periodic materials by **flow matching**.
|
|
10
|
+
The velocity field is Meta FAIR's pretrained **UMA** backbone (`uma-s-1p2` or `uma-m-1p1`) with an equivariant
|
|
11
|
+
**time-FiLM** at selected backbone blocks (all of them in the paper model), an optional global-attention layer
|
|
12
|
+
(`GlobalAttn`, off in every released recipe), and a small SO(3)-equivariant `VelocityHead` that outputs a per-atom
|
|
13
|
+
velocity. Package import name `saddleflow`; written
|
|
14
|
+
**SaddleFlow** in prose.
|
|
15
|
+
|
|
16
|
+
Two ways to use it:
|
|
17
|
+
|
|
18
|
+
- **Unconditional, single-ended (Dimer-like).** Only the reactant is known. Start from the reactant plus Gaussian noise
|
|
19
|
+
and flow to a nearby first-order saddle; the head never sees the product (`--delta-endpoint-channels 0`). This is
|
|
20
|
+
the model in the MaterialsSaddles paper (NeurIPS 2026, Evaluations & Datasets), trained in `examples/MaterialsSaddles`.
|
|
21
|
+
- **Conditional, double-ended (NEB-like).** Both endpoints (R, P) are known. Start at their minimum-image midpoint and
|
|
22
|
+
give the head the per-atom displacement to each endpoint at every step (`--delta-endpoint-channels 32`). Recipe in
|
|
23
|
+
`examples/MP20Bat`.
|
|
24
|
+
|
|
25
|
+
## Repository layout
|
|
26
|
+
|
|
27
|
+
- `saddleflow/data/` — datasets (`TrajTripletDataset` for `.traj`, `AseDbSaddleDataset`, `MaterialsSaddlesDataset`
|
|
28
|
+
for the released ASE-LMDB shards), coordinate transforms (`wrap_positions`, `mic_displacement`).
|
|
29
|
+
- `saddleflow/flow/` — `FlowMatchingConfig`, `FlowMatchingLoss`, start-distribution sampling, sampler.
|
|
30
|
+
- `saddleflow/models/` — `TimeFiLMBackbone`, `VelocityHead`, `GlobalAttn`.
|
|
31
|
+
- `saddleflow/utils/` — backbone loading, the `accelerate` training loop (`training.py`), EMA, evaluation helpers.
|
|
32
|
+
- `examples/` — `MaterialsSaddles` (paper model, data download, evaluation), `MP20Bat` (conditional recipe),
|
|
33
|
+
`LiC` and `LiC_simpler` (small unconditioned examples with their own READMEs), `LematBulk`, `scaling_bench`.
|
|
34
|
+
- `tests/` — `pytest tests`.
|
|
35
|
+
|
|
36
|
+
## The paper model (unconditional, all four MaterialsSaddles subsets)
|
|
37
|
+
|
|
38
|
+
**Data.** `python examples/MaterialsSaddles/data_prep.py` downloads MaterialsSaddles from Hugging Face
|
|
39
|
+
(`SciLM/MaterialsSaddles`) into `$MATERIALSSADDLES_ROOT` (default `$SCRATCH/MaterialsSaddles_v2`). The release is
|
|
40
|
+
deduplicated and split 90/5/5 by element set into `<subset>/{train,val,test}/` directories; no chemical system
|
|
41
|
+
appears in two splits.
|
|
42
|
+
|
|
43
|
+
**Training command** (one epoch over all subsets with oc20 ×2 and oc22, mp20bat ×8 = 67.7 M records; 64 GH200 GPUs,
|
|
44
|
+
8 records per GPU, global batch 512, ~132 k steps). Launch one process per GPU with the SPMD pattern of
|
|
45
|
+
`examples/MaterialsSaddles/run.sh`:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
python examples/MaterialsSaddles/train.py --all-subsets --output-dir <out> \
|
|
49
|
+
--subset-repeat oc22=8,mp20bat=8,oc20=2 --task-name-map oc22=oc20 \
|
|
50
|
+
--backbone uma-m-1p1 --mixed-precision no --grad-clip-norm 1.0 --batch-size 8 --num-epochs 1 \
|
|
51
|
+
--learning-rate 1e-3 --uma-lr 3e-3 --warmup-steps $((250 * WORLD_SIZE)) --ema-decay 0.9996 \
|
|
52
|
+
--loss-type huber --huber-delta 0.05 \
|
|
53
|
+
--delta-endpoint-channels 0 --path-start-prob 1.0 --path-noise-sigma 0.3 --path-u-power 2.0 \
|
|
54
|
+
--save-every-steps 2000 --val-every-steps 2000 --max-val-records 4000 \
|
|
55
|
+
--unfreeze-uma-all --early-time-film-blocks 0,1,2,3,4,5,6,7,8,9 --head-depth 3 --attn-layers 0 \
|
|
56
|
+
--com-symmetric-loss --xt-target-correction --xt-perturb-sigma 0.05 \
|
|
57
|
+
--no-inject-force --no-dimer-residual --no-frozen-force-backbone --no-endpoint-features --eigenmode-aux-weight 0
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Why each choice.**
|
|
61
|
+
- **Path starts** (`--path-start-prob 1`). Each sample starts on the line from an endpoint E (reactant or product) to its
|
|
62
|
+
saddle S plus noise: `x0 = E + u (S − E) + ε`, `ε ~ N(0, σ²)` on mobile atoms. Training only from `S + ε`
|
|
63
|
+
(transition-state denoising) fails for this task: in 3N dimensions an isotropic draw almost never points along the
|
|
64
|
+
reaction, so the model learns to undo noise and stays at the minimum.
|
|
65
|
+
- **`u ~ U(0,1)²`** (`--path-u-power 2`) biases starts toward the endpoint, where inference begins.
|
|
66
|
+
- **σ = 0.3 Å matches the inference noise.** Never deploy wider than the training σ (the model degrades sharply);
|
|
67
|
+
narrower is safe. Ship the training σ with the weights.
|
|
68
|
+
- **x_t perturbation (0.05 Å)** is not redundant with the start noise: ε enters `x_t` scaled by `(1 − t)` and fades near
|
|
69
|
+
the saddle, where the model's own endpoints land off the line.
|
|
70
|
+
- **Convergent target** (`--xt-target-correction`, `t_floor = 0.1`): `v = (S − x_t)/(1 − t)` for `t ≤ 0.9`, with S and
|
|
71
|
+
x_t unwrapped to the start (not a minimum-image difference); `S − x0` above (perturbation off there).
|
|
72
|
+
- **Huber δ = 0.05** approximates the median of a multi-modal target where MSE averages the modes.
|
|
73
|
+
- **Backbone LR 3e-3 on UMA-M** (1e-2, the UMA-S optimum, diverges on UMA-M); head/FiLM LR 1e-3.
|
|
74
|
+
- **`--task-name-map oc22=oc20`**: `uma-m-1p1` has no `oc22` mixture-of-experts entry and raises `KeyError('oc22')`.
|
|
75
|
+
|
|
76
|
+
**Inference: ×5 K4.** Spend 20 network evaluations as five passes of four Euler steps, each pass restarting at `t = 0`
|
|
77
|
+
from the previous endpoint, rather than one pass of 20 (×1 K20). A single pass stops short of the saddle; because the
|
|
78
|
+
training starts span the whole endpoint–saddle line, the same model also knows how to refine its own endpoint.
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
SF_NAPPLY=5 python examples/MaterialsSaddles/dump_throws_batched.py \
|
|
82
|
+
--data-glob "$MATERIALSSADDLES_ROOT/<subset>/*/<subset>_*.aselmdb" --subset <subset> --split test \
|
|
83
|
+
--ckpt <ckpt> --use-ema --task-name-map oc22=oc20 --sides 0 --sigma 0.3 --K 4 --seed 0 --tag ev --outdir <dir>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Then converge every endpoint with Sella (see Evaluation) and collect with `sm_collect.py`.
|
|
87
|
+
|
|
88
|
+
## The conditional recipe (`examples/MP20Bat`)
|
|
89
|
+
|
|
90
|
+
UMA-S-1.2 with all four blocks unfrozen (LR 1e-4) and time-FiLM at every block; `VelocityHead(depth=3,
|
|
91
|
+
delta_endpoint_channels=32)` fed `(Δ_R, Δ_P) = MIC(R − x_t), MIC(P − x_t)` stacked as `(N, 2, 3)`; `x0 = (R + P)/2`
|
|
92
|
+
(minimum-image midpoint); x_t perturbation 0.05 Å with the convergent target; CoM-symmetric loss. `run.sh` holds the
|
|
93
|
+
exact hyperparameters. Later work found Huber loss and a much higher backbone LR (3e-3 to 1e-2) clearly better for
|
|
94
|
+
this recipe. Score conditional models against the dataset label (they are asked for that specific saddle).
|
|
95
|
+
|
|
96
|
+
## Architecture
|
|
97
|
+
|
|
98
|
+
**Backbone (UMA).** Loaded by `saddleflow.utils.load_uma_backbone`; only `.backbone` is used, the energy/force heads are
|
|
99
|
+
discarded. UMA-S-1.2: 4 blocks, lmax 2, 128 channels, embedding `(N, 9, 128)`. UMA-M-1.1: 10 blocks, lmax 4,
|
|
100
|
+
128 channels, embedding `(N, 25, 128)`, 1.4 B parameters. With a 6 Å cutoff, even the 4 UMA-S blocks reach ~24 Å,
|
|
101
|
+
beyond most released cells; this is why `GlobalAttn` stays off. Mixture-of-experts routing comes from each record's
|
|
102
|
+
`task_name` (`omat`, `oc20`, `oc22`, ...) via `csd_embedding(charge, spin, task)`; `charge`/`spin` default to 0.
|
|
103
|
+
|
|
104
|
+
**Time-FiLM.** A zero-initialised equivariant FiLM is hooked before each selected block (`TimeFiLMBackbone`) and in the
|
|
105
|
+
head: an additive bias on `l = 0` channels and a `(1 + γ)` gate on `l ≥ 1` channels, both functions of `t` only, so
|
|
106
|
+
equivariance is exact. The bias alone would never reach `l ≥ 1` outputs (`SO3_Linear` keeps the `l` paths separate);
|
|
107
|
+
the gate is what lets time act on the vector channels. At init the wrapped model equals UMA bit for bit.
|
|
108
|
+
|
|
109
|
+
**VelocityHead.** Built from UMA's `SO3_Linear` (not e3nn's `o3.Linear`; the two tensor layouts are not
|
|
110
|
+
interchangeable) with `depth − 1` blocks of `SO3_Linear → UMAGate` before the `l = 1` readout. Optional zero-initialised
|
|
111
|
+
inputs (endpoint displacements, forces) leave it identical to a plain force head when off.
|
|
112
|
+
|
|
113
|
+
**Output projections, in order.** (1) `v[fixed] = 0`. (2) Subtract the mobile-atom mean velocity **only in systems with
|
|
114
|
+
no frozen atoms** (`_com_projection_batched`). Do not make this unconditional: in a system with one mobile atom it
|
|
115
|
+
zeroes that atom's velocity and silently kills training.
|
|
116
|
+
|
|
117
|
+
## Flow formulation
|
|
118
|
+
|
|
119
|
+
- **State:** Cartesian positions in Å; UMA always sees physical positions, wrapped into the cell just before each
|
|
120
|
+
forward pass. Interpolation happens in unwrapped space.
|
|
121
|
+
- **Minimum-image unwrap** of every target relative to the start (once, when a record is built):
|
|
122
|
+
`Δs = inv(cell) @ (r_target − r_start); Δs −= round(Δs); r_target_un = r_start + cell @ Δs`.
|
|
123
|
+
- **R↔P doubling:** every triplet gives two records, one per endpoint as start; the unwrap is recomputed per anchor.
|
|
124
|
+
- **Start distributions** (`FlowMatchingConfig`): `(R+P)/2` midpoint (conditional), transition-state denoising
|
|
125
|
+
(`ts_denoise_sigma`), path starts (`path_start_prob`, `path_noise_sigma`, `path_u_power`, optional σ schedule
|
|
126
|
+
`path_noise_sigma_endpoint`/`_saddle`). All new options default off.
|
|
127
|
+
- **Path:** `x_t = (1 − t) x0 + t S`, `t ~ U(0, 1)`; target as above.
|
|
128
|
+
- **Inference:** forward Euler, `K` steps per pass, optional restarts (`SF_NAPPLY` in the dumpers). Higher-order
|
|
129
|
+
integrators and larger `K` do not help; the error is in the field, not the integration.
|
|
130
|
+
|
|
131
|
+
## Data formats
|
|
132
|
+
|
|
133
|
+
All sources reduce to the same per-record dict: `start_pos`, `partner_un_pos`, `saddle_un_pos` (both unwrapped to the
|
|
134
|
+
start), `Z`, `cell`, `fixed`, `task_name`, `charge`, `spin`, `delta_norm`, `role` (`R2S`/`P2S`), `triplet_id`,
|
|
135
|
+
`metadata`. Triplets are stored as consecutive `[R, S, P]` frames. Small problems read `.traj` directly
|
|
136
|
+
(`TrajTripletDataset`); MaterialsSaddles reads ASE-LMDB shards (`MaterialsSaddlesDataset`, which also applies
|
|
137
|
+
`task_name_map`). Neighbour lists are rebuilt every forward pass (`AtomicData.from_ase`), since bonds change along the
|
|
138
|
+
flow.
|
|
139
|
+
|
|
140
|
+
## Training infrastructure
|
|
141
|
+
|
|
142
|
+
Plain PyTorch + Hugging Face `accelerate` (`saddleflow/utils/training.py`); no Hydra or Lightning.
|
|
143
|
+
|
|
144
|
+
- **Multi-node: SPMD, one process per GPU** (`RANK=$SLURM_PROCID`, `WORLD_SIZE=$SLURM_NTASKS`, static
|
|
145
|
+
`init_process_group`), as in `examples/MaterialsSaddles/run.sh`. Do not use `accelerate launch --num_machines`
|
|
146
|
+
across many nodes: its elastic rendezvous strands stragglers. `examples/MP20Bat/run.sh` still uses the old pattern.
|
|
147
|
+
- **Warmup and `--max-steps` count scheduler ticks, and accelerate ticks once per rank per optimizer step.** For a real
|
|
148
|
+
warmup of W optimizer steps pass `--warmup-steps W × world_size`. `--max-steps` is converted internally.
|
|
149
|
+
- **Fixed global batch, free node count.** Changing nodes at fixed global batch changes nothing but speed; never
|
|
150
|
+
resume a run on a different node count (the schedule is per-rank ticks).
|
|
151
|
+
- **Resume** (`--resume-from <checkpoint>`) continues mid-epoch with the same shuffle and skips trained batches.
|
|
152
|
+
`--init-weights` loads weights only (fresh optimizer and schedule) and re-snapshots the EMA.
|
|
153
|
+
- **EMA:** choose `d = 1 − 1/window` with `window ≈ total_steps/50`. Too high a decay on a short run leaves the shadow
|
|
154
|
+
at the initial weights (evaluation then looks broken; compare with live weights).
|
|
155
|
+
- **Large validation sets:** cap them with `--max-val-records`.
|
|
156
|
+
- **Precision:** fp32 is the safe default (the paper model is fp32). bf16 autocast is acceptable for UMA-S training
|
|
157
|
+
because the coordinate helpers are pinned to fp32 (see bug log).
|
|
158
|
+
|
|
159
|
+
## Evaluation
|
|
160
|
+
|
|
161
|
+
- **Score with Sella, never with ASE Dimer.** Dimer wanders to a different saddle in a sizeable fraction of cases and
|
|
162
|
+
fabricates a tail. Run Sella (via SaddleMill, `method = Sella`; example `examples/Sella_OC22` in
|
|
163
|
+
`SciLM-ai/SaddleMill`) from each prediction: UMA-S-1.2 with the subset's own task (`omat` for lemat/mp20bat,
|
|
164
|
+
`oc20`, `oc22`), first order, `fmax = 0.01` eV/Å, ≤ 300 steps, `delta0 = 0.1`, index check (exactly one eigenvalue
|
|
165
|
+
below −0.01 eV/Ų). For SaddleFlow outputs use `dataset_type = bulk`: they carry their own `FixAtoms` and all-zero
|
|
166
|
+
tags, and `oc` would re-freeze every tag-0 atom.
|
|
167
|
+
- **Check the task name against the subset before scoring.** Scoring through the wrong expert inflates errors.
|
|
168
|
+
- **Metric:** maximum per-atom displacement (minimum image) from the prediction to the saddle Sella reaches, over cases
|
|
169
|
+
that converge and verify as first order; report mean and median and the Sella force calls. Unconditional models are
|
|
170
|
+
scored against the Sella saddle, conditional models against the dataset label.
|
|
171
|
+
- **Collect strictly** (`sm_collect.py`): count only real Sella convergence and verified index-1; a desorption check
|
|
172
|
+
can mark an unoptimised input as converged at step 0; Sella crashes are failures, not dropped cases; a truncated
|
|
173
|
+
pass keeps the easy cases first and is biased.
|
|
174
|
+
- **Compare arms on identical starts** with paired tests. Different noise draws move medians by several hundredths
|
|
175
|
+
of an Å, more than most training changes.
|
|
176
|
+
|
|
177
|
+
## Latent-bug log — re-check these if behaviour looks wrong
|
|
178
|
+
|
|
179
|
+
- **Autocast silently quantises coordinates.** `wrap_positions` and `mic_displacement` do a Cartesian→fractional→
|
|
180
|
+
Cartesian round trip; under `torch.autocast` those matmuls run in bf16 (spacing 0.06 Å at 12 Å), so the structure
|
|
181
|
+
fed to UMA is corrupted while the target is exact. Training still reaches a *lower* loss. Both helpers now force
|
|
182
|
+
autocast off (`tests/test_autocast_precision.py`). Rule: never do arithmetic on absolute coordinates under autocast.
|
|
183
|
+
fp16 hides the bug; do not test for it with fp16.
|
|
184
|
+
- **UMA dropouts leak into training.** `.train()` enables UMA's composition and mixture dropouts; `FlowMatchingLoss`
|
|
185
|
+
re-calls `backbone.eval()`.
|
|
186
|
+
- **Time embedding base.** The transformer base 10000 is ~constant over `t ∈ [0, 1]`; the head uses geometric
|
|
187
|
+
frequencies from 1 to `half` cycles per unit time.
|
|
188
|
+
- **RMSD over all atoms dilutes error by √N.** All evaluation helpers take `mobile_mask`.
|
|
189
|
+
- **CoM projection** must stay conditional on having no frozen atoms (see Architecture).
|
|
190
|
+
- **AdaLN-zero:** the first time-MLP layer has zero gradient at step 0 only; if it persists, something is broken.
|
|
191
|
+
- **Force injection uses the first sample's task name for the whole batch.** Fine for homogeneous batches or with
|
|
192
|
+
`--no-inject-force` (all released recipes); wrong for mixed-subset batches with force injection.
|
|
193
|
+
- **Multi-node NCCL hang** on some clusters near an epoch boundary (a race between gradient all-reduce buckets):
|
|
194
|
+
`TORCH_DISTRIBUTED_DEBUG=DETAIL` serialises the collectives (~40 % slower) and fixes it; a per-step barrier does not.
|
|
195
|
+
- **LiC:** the Li is atom 126 in `LiC` and 112 in `LiC_simpler`; the negative cell `z` is intentional vacuum.
|
|
196
|
+
|
|
197
|
+
## Contributing
|
|
198
|
+
|
|
199
|
+
- Work on a branch, open a pull request, merge it; do not push to `main` directly.
|
|
200
|
+
- Commit messages: one sentence; add a short body only when the reason is not visible in the diff.
|
|
201
|
+
- Figures as PDF.
|
|
202
|
+
- Never commit data, checkpoints, or `CLAUDE.local.md`.
|
|
203
|
+
|
|
204
|
+
## License
|
|
205
|
+
|
|
206
|
+
MIT — see [`LICENSE`](LICENSE).
|
saddleflow-0.1.0/LICENSE
ADDED
|
@@ -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,233 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: saddleflow
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Flow-matching generation of transition-state structures for periodic materials.
|
|
5
|
+
Project-URL: Homepage, https://github.com/SciLM-ai/SaddleFlow
|
|
6
|
+
Project-URL: Repository, https://github.com/SciLM-ai/SaddleFlow
|
|
7
|
+
Project-URL: Model, https://huggingface.co/SciLM/SaddleFlow
|
|
8
|
+
Project-URL: Dataset, https://huggingface.co/datasets/SciLM/MaterialsSaddles
|
|
9
|
+
Author: Ilgar Baghishov, Sung Hoon Jung, Graeme Henkelman
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Python: <3.14,>=3.11
|
|
13
|
+
Requires-Dist: ase>=3.26.0
|
|
14
|
+
Requires-Dist: fairchem-core<2.24,>=2.19.0
|
|
15
|
+
Requires-Dist: huggingface-hub>=0.27.1
|
|
16
|
+
Requires-Dist: numpy<2.5,>=2.0
|
|
17
|
+
Requires-Dist: safetensors>=0.4
|
|
18
|
+
Requires-Dist: scipy>=1.15.0
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pre-commit; extra == 'dev'
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff==0.5.1; extra == 'dev'
|
|
24
|
+
Provides-Extra: eval
|
|
25
|
+
Requires-Dist: sella>=2.5; extra == 'eval'
|
|
26
|
+
Provides-Extra: extras
|
|
27
|
+
Requires-Dist: pymatgen>=2025.1.9; extra == 'extras'
|
|
28
|
+
Provides-Extra: search
|
|
29
|
+
Requires-Dist: sella>=2.5; extra == 'search'
|
|
30
|
+
Provides-Extra: train
|
|
31
|
+
Requires-Dist: accelerate>=1.0.0; extra == 'train'
|
|
32
|
+
Requires-Dist: lmdb<=1.7.3,>=1.6.2; extra == 'train'
|
|
33
|
+
Requires-Dist: matplotlib>=3.7; extra == 'train'
|
|
34
|
+
Requires-Dist: pyarrow; extra == 'train'
|
|
35
|
+
Requires-Dist: pyyaml; extra == 'train'
|
|
36
|
+
Requires-Dist: tqdm; extra == 'train'
|
|
37
|
+
Requires-Dist: wandb; extra == 'train'
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
|
|
40
|
+
# SaddleFlow
|
|
41
|
+
|
|
42
|
+
Generative AI for **transition-state structures** in periodic materials.
|
|
43
|
+
|
|
44
|
+
SaddleFlow uses flow matching on a fine-tuned [UMA](https://huggingface.co/facebook/UMA) backbone to propose
|
|
45
|
+
saddle points. The released model is unconditional, like a Dimer search: from a single minimum plus noise it
|
|
46
|
+
flows to a nearby first-order saddle, which Sella then converges in 25–61 force calls instead of 196–352
|
|
47
|
+
(MaterialsSaddles paper). A conditional recipe that also takes the product, like NEB, is in
|
|
48
|
+
[`examples/MP20Bat/`](https://github.com/SciLM-ai/SaddleFlow/tree/main/examples/MP20Bat/).
|
|
49
|
+
|
|
50
|
+
## Use the pretrained model
|
|
51
|
+
|
|
52
|
+
### Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "saddleflow[search]"
|
|
56
|
+
python -c "import torch; print(torch.cuda.is_available())" # should print True
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Linux on ARM (aarch64), e.g. NVIDIA GH200:** PyPI has only CPU builds of torch for aarch64, so the line above
|
|
60
|
+
silently installs one. Install the CUDA build first, then SaddleFlow:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install torch==2.13.0 --index-url https://download.pytorch.org/whl/cu129
|
|
64
|
+
pip install "saddleflow[search]"
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`torch==2.13.0` matches the newest supported fairchem-core (2.23); with fairchem-core 2.19–2.21 use `torch==2.8.0`
|
|
68
|
+
from the same index.
|
|
69
|
+
|
|
70
|
+
**Access.** The weights are gated: accept the license at [`SciLM/SaddleFlow`](https://huggingface.co/SciLM/SaddleFlow)
|
|
71
|
+
and, to converge proposals with Sella, at [`facebook/UMA`](https://huggingface.co/facebook/UMA); then run
|
|
72
|
+
`hf auth login`. The first `from_pretrained()` downloads 5.6 GB.
|
|
73
|
+
|
|
74
|
+
**Hardware.** One GPU. On one GH200 a proposal (20 network evaluations) takes about 1.8 s on its own, and 16–64
|
|
75
|
+
proposals batched together run at 2–3 per second for 40–80-atom cells. GPU memory is about 6 GB for one proposal,
|
|
76
|
+
12–15 GB for 16, and 26–34 GB for 64 (`batch_size` sets how many run together). CPU works but is slow. fairchem
|
|
77
|
+
compiles kernels on first use and caches them under `~/.triton` and `~/.cache`; on clusters with a small home
|
|
78
|
+
quota, set `TRITON_CACHE_DIR`, `TORCHINDUCTOR_CACHE_DIR`, and `XDG_CACHE_HOME` to a scratch directory.
|
|
79
|
+
|
|
80
|
+
### Run
|
|
81
|
+
|
|
82
|
+
[`examples/quickstart/`](https://github.com/SciLM-ai/SaddleFlow/tree/main/examples/quickstart/) runs all of this on a real 61-atom battery cell.
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from ase.io import read
|
|
86
|
+
from saddleflow import SaddleFlow, SaddleFlowSearch, find_saddles
|
|
87
|
+
|
|
88
|
+
sf = SaddleFlow.from_pretrained() # SciLM/SaddleFlow, GPU if available
|
|
89
|
+
atoms = read("reactant.traj")
|
|
90
|
+
guesses = sf.propose(atoms, n=8, seed=0, task_name="omat") # 8 ase.Atoms, one per noise draw
|
|
91
|
+
|
|
92
|
+
# Converge them with Sella on UMA-S-1.2 and keep the distinct first-order saddles.
|
|
93
|
+
from fairchem.core import FAIRChemCalculator
|
|
94
|
+
from fairchem.core.calculate.pretrained_mlip import get_predict_unit
|
|
95
|
+
calc = FAIRChemCalculator(get_predict_unit("uma-s-1p2", device="cuda"), task_name="omat")
|
|
96
|
+
saddles = find_saddles(atoms, calc, n=8, model=sf, seed=0)
|
|
97
|
+
|
|
98
|
+
# Or use it like an ASE optimizer: atoms ends on a verified first-order saddle.
|
|
99
|
+
atoms.calc = calc
|
|
100
|
+
SaddleFlowSearch(atoms, sf, seed=0).run(fmax=0.01)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
From the command line: `saddleflow propose reactant.traj -o proposals.traj --n 8` and
|
|
104
|
+
`saddleflow search reactant.traj -o saddles.traj --n 8 --task omat`.
|
|
105
|
+
|
|
106
|
+
Use `task_name="omat"` for bulk materials and `"oc20"` or `"oc22"` for adsorbates on catalyst surfaces. The start
|
|
107
|
+
noise defaults to the training value (0.3 Å) and cannot be raised; each proposal is five flow passes of four
|
|
108
|
+
Euler steps. `examples/MaterialsSaddles/export_pretrained.py` turns a training checkpoint into this format, and
|
|
109
|
+
`check_pretrained.py` checks that it reproduces recorded evaluation endpoints.
|
|
110
|
+
|
|
111
|
+
## Repository
|
|
112
|
+
|
|
113
|
+
- `examples/MaterialsSaddles/`: training, evaluation and export of the released model on all four
|
|
114
|
+
MaterialsSaddles subsets.
|
|
115
|
+
- `examples/MP20Bat/`: the conditional recipe on the `mp20bat` subset.
|
|
116
|
+
- `examples/LiC/`, `examples/LiC_simpler/`: small Li-on-graphene examples for quick experiments.
|
|
117
|
+
|
|
118
|
+
See [`CLAUDE.md`](https://github.com/SciLM-ai/SaddleFlow/blob/main/CLAUDE.md) for the method and the training recipe.
|
|
119
|
+
|
|
120
|
+
## Install for training and development
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
conda create -n saddleflow python=3.12 -y
|
|
124
|
+
conda activate saddleflow
|
|
125
|
+
pip install -e '.[train,search]'
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
`train` adds what the training and evaluation scripts in `examples/` need (accelerate, wandb, matplotlib, pyarrow,
|
|
129
|
+
lmdb); `search` adds Sella for the saddle-search evaluation below, which is not needed to train or to sample. The
|
|
130
|
+
paper model was trained with fairchem-core 2.19 (`pip install 'fairchem-core==2.19.*'` to match it). On aarch64,
|
|
131
|
+
install a CUDA build of torch first, as above.
|
|
132
|
+
|
|
133
|
+
Use **Python 3.12** — fairchem-core builds cleanly on 3.12 but has had wheel issues on 3.11/3.13 on some platforms.
|
|
134
|
+
|
|
135
|
+
First UMA load downloads the `uma-s-1p2` checkpoint from HuggingFace and requires a valid `HF_TOKEN` (or a
|
|
136
|
+
`hf auth login`). If pip builds Sella from source with NVIDIA's `nvc` compiler, set `CC=gcc CXX=g++`.
|
|
137
|
+
|
|
138
|
+
## Sample from your own conditional checkpoint
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import torch
|
|
142
|
+
from ase.io import read
|
|
143
|
+
|
|
144
|
+
from saddleflow.flow.sampler import sample_saddles
|
|
145
|
+
# ... load_model is provided per-example; see examples/MP20Bat/sample_and_distance_eval.py
|
|
146
|
+
|
|
147
|
+
# Load a trained checkpoint (architecture is rebuilt from the run's config.json).
|
|
148
|
+
ckpt_dir = "$SCRATCH/SaddleFlow_mp20bat/runs/<RUN>/checkpoint_final"
|
|
149
|
+
loss_module, _config = load_model(ckpt_dir, device="cuda", use_ema=True)
|
|
150
|
+
|
|
151
|
+
# Read reactant and product geometries (ASE-readable formats).
|
|
152
|
+
R = read("my_reactant.traj")
|
|
153
|
+
P = read("my_product.traj")
|
|
154
|
+
|
|
155
|
+
# Run deterministic Mode-1 sampling (sigma_inf=0, n_perturbations=1, K Euler steps).
|
|
156
|
+
candidates = sample_saddles(
|
|
157
|
+
sample_dict_from_atoms(R, P), # builds {start_pos, partner_un_pos, Z, cell, fixed, ...}
|
|
158
|
+
loss_module.backbone,
|
|
159
|
+
loss_module.global_attn,
|
|
160
|
+
loss_module.velocity_head,
|
|
161
|
+
sigma_inf=0.0,
|
|
162
|
+
n_perturbations=1,
|
|
163
|
+
K=50,
|
|
164
|
+
device="cuda",
|
|
165
|
+
partner_pos=..., # MIC-unwrapped partner positions
|
|
166
|
+
# Plus any optional heads / force backbones the run uses; see eval scripts.
|
|
167
|
+
)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For end-to-end working examples, see `examples/MP20Bat/sample_and_distance_eval.py` (a 50-case eval that loads a checkpoint, samples saddles for held-out triplets, and writes parity plots and 4-frame `.traj` files for visualization).
|
|
171
|
+
|
|
172
|
+
## Training your own model
|
|
173
|
+
|
|
174
|
+
The production example is `examples/MP20Bat/`:
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
cd $SCRATCH/SaddleFlow_mp20bat # so SLURM logs land on scratch
|
|
178
|
+
sbatch $WORK/codes/SaddleFlow/examples/MP20Bat/run.sh
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This trains on the full `mp20bat` subset (~34,742 triplets) for 60 epochs with 4 nodes × 3 A100s, then auto-runs a 50-case post-training eval. Heavy outputs (checkpoints, eval `.npz`, `.traj` files) all land under `$SCRATCH/SaddleFlow_mp20bat/runs/<TIMESTAMP>/`. See [`examples/MP20Bat/README.md`](https://github.com/SciLM-ai/SaddleFlow/blob/main/examples/MP20Bat/README.md) for the full pipeline (training → full-test-set eval at K=10 → K=10 vs K=50 stability → paper-style parity figures).
|
|
182
|
+
|
|
183
|
+
**For full-dataset / large-scale training (all four MaterialsSaddles subsets — lemat + oc20 + oc22 + mp20bat = 34 M triplets — at 128+ nodes), use [`examples/MaterialsSaddles/`](https://github.com/SciLM-ai/SaddleFlow/tree/main/examples/MaterialsSaddles/) instead.** Its `run.sh` uses srun-native SPMD (each SLURM task = one rank, env-var DDP), which scales reliably past 128 nodes. The MP20Bat launcher above is the proven 4-node paper config; its `accelerate launch --num_machines=N` pattern is fragile at large scale — see [`CLAUDE.md`](https://github.com/SciLM-ai/SaddleFlow/blob/main/CLAUDE.md) "Training infrastructure" for why.
|
|
184
|
+
|
|
185
|
+
For a quick sanity-check on small data:
|
|
186
|
+
|
|
187
|
+
- [`examples/LiC_simpler/`](https://github.com/SciLM-ai/SaddleFlow/tree/main/examples/LiC_simpler/) — **start here.** One Li hop on pristine graphene (1 triplet), ~20 min on one GPU. The site is 6-fold symmetric and the data contains only *one* of the six equivalent saddles, so whether the model recovers all six is a visual pass/fail on the physics. **It passes:** with the backbone unfrozen and per-block time-FiLM, all 48 sampled trajectories end in one of the six saddle petals — a six-fold orbit from one training saddle, with hexatic order **0.98–1.00** over two independent runs. That recipe is the script's default; a frozen backbone reaches only 0.91 (see its [README](https://github.com/SciLM-ai/SaddleFlow/blob/main/examples/LiC_simpler/README.md)).
|
|
188
|
+
- [`examples/LiC/`](https://github.com/SciLM-ai/SaddleFlow/tree/main/examples/LiC/) — defective graphene: 12 training saddles, 167 held-out ones (each Sella-reconverged at fmax 0.005 and verified index-1), and 10 bond midpoints that look like saddles but are not. An unconditioned model carries a Li thrown anywhere on the sheet to the nearest saddle; ~4 h on one GPU. Its [README](https://github.com/SciLM-ai/SaddleFlow/blob/main/examples/LiC/README.md) reproduces the trajectory figure.
|
|
189
|
+
|
|
190
|
+
Both use an *unconditioned* model rather than the `(R, P)`-conditioned scheme below: on graphene the midpoint of R and P already sits on the saddle, so conditioning would leave the flow nothing to learn.
|
|
191
|
+
|
|
192
|
+
## Method (in brief)
|
|
193
|
+
|
|
194
|
+
- **Flow matching** over atomic Cartesian coordinates with straight-line Optimal Transport from `x_0` to `x_1 = saddle`.
|
|
195
|
+
- **Product-conditional anchoring.** `x_0 = (R + P)/2` is the PBC-correct geodesic midpoint of the reactant–product pair, and the velocity head is conditioned on a per-atom Δ_partner = MIC(partner − x_t) at every flow step. This re-parameterises the L2-Bayes-optimal predictor from `E[saddle] − R ≈ midpoint − R` (large) to `E[saddle] − midpoint ≈ 0` (small) — the head only has to predict the residual deviation of the saddle from the midpoint, not rediscover the midpoint itself.
|
|
196
|
+
- **Velocity field** = pretrained **UMA-S-1.2** (fairchem, 6.6M active params) as the equivariant backbone, equivariant time-FiLM injected at each backbone block, and a small `VelocityHead` on top. Selectively-unfrozen UMA blocks at low LR (1e-5) let the backbone adapt its layer-N l ≥ 1 outputs to the velocity-prediction task.
|
|
197
|
+
- **R↔P doubling.** Each triplet contributes both `(start=R, partner=P)` and `(start=P, partner=R)` training pairs, so the head sees the saddle from both directions and the parity isn't a learned bias.
|
|
198
|
+
- **Inference perturbation.** A small Gaussian `ε_inf` on the start position at `t=0` lets multiple integrations from the same `(R, P)` pair land in different angular wedges of the local environment, producing a small ensemble of saddle candidates that can be clustered downstream.
|
|
199
|
+
|
|
200
|
+
## Evaluation — score against the saddle the prediction actually reaches
|
|
201
|
+
|
|
202
|
+
RMSD to a stored label answers "is this near *that* saddle", which is not the question. Run a saddle optimiser from the prediction and measure how far it had to move:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
# 1. Integrate the flow and dump candidates (--ckpt2 runs the two-stage cascade).
|
|
206
|
+
python examples/MaterialsSaddles/dump_predictions.py \
|
|
207
|
+
--data-glob '<MaterialsSaddles>/mp20bat/*.aselmdb' \
|
|
208
|
+
--ckpt <stage1>/checkpoint_final --ckpt2 <stage2>/checkpoint_final \
|
|
209
|
+
--tag cascade --outdir preds --shard $i --nshards $N
|
|
210
|
+
|
|
211
|
+
# 2. Optimise each candidate to a saddle and verify it is genuinely first-order.
|
|
212
|
+
python examples/MaterialsSaddles/sella_eval.py \
|
|
213
|
+
--src preds/cascade_00.traj --out sella_00.json --check-index
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Then keep records with `nneg == 1`. Reported per model: distance moved (rmsd/maxd), convergence rate, and force calls — a prediction that is close to a saddle *and* cheap to converge is the useful one.
|
|
217
|
+
|
|
218
|
+
**Use Sella, not the ASE Dimer.** On identical structures the Dimer walked to a different saddle in ~8% of cases, which silently inflates the error tail (in the worst decile, predictions sat 0.17 Å from the label but 2.21 Å from where the Dimer stopped). Sella converged 100% vs 77%, in 44 vs 384 force calls, with 93% of results verified index-1.
|
|
219
|
+
|
|
220
|
+
## Built on
|
|
221
|
+
|
|
222
|
+
- [fairchem](https://github.com/facebookresearch/fairchem) (Meta FAIR) — UMA backbone, `AtomicData`, PBC-aware graph building, ASE Calculator integration.
|
|
223
|
+
- [ASE](https://wiki.fysik.dtu.dk/ase/) — atomic simulation environment.
|
|
224
|
+
- [PyTorch](https://pytorch.org) + [HuggingFace `accelerate`](https://huggingface.co/docs/accelerate) — training loop, multi-GPU / multi-node scaling.
|
|
225
|
+
|
|
226
|
+
## Authors
|
|
227
|
+
|
|
228
|
+
Ilgar Baghishov, Sung Hoon Jung and Graeme Henkelman (The University of Texas at Austin).
|
|
229
|
+
|
|
230
|
+
## License
|
|
231
|
+
|
|
232
|
+
The code is MIT — see [`LICENSE`](https://github.com/SciLM-ai/SaddleFlow/blob/main/LICENSE). The released weights are a fine-tuned version of Meta's UMA-M-1.1
|
|
233
|
+
and are distributed under the FAIR Chemistry License v1 on the Hugging Face model page.
|