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.
Files changed (100) hide show
  1. saddleflow-0.1.0/.claude/scheduled_tasks.lock +1 -0
  2. saddleflow-0.1.0/.github/workflows/publish.yml +36 -0
  3. saddleflow-0.1.0/.gitignore +75 -0
  4. saddleflow-0.1.0/CLAUDE.md +206 -0
  5. saddleflow-0.1.0/LICENSE +21 -0
  6. saddleflow-0.1.0/PKG-INFO +233 -0
  7. saddleflow-0.1.0/README.md +194 -0
  8. saddleflow-0.1.0/examples/LematBulk/README.md +50 -0
  9. saddleflow-0.1.0/examples/LematBulk/run.sh +170 -0
  10. saddleflow-0.1.0/examples/LiC/README.md +114 -0
  11. saddleflow-0.1.0/examples/LiC/plot_throws.py +262 -0
  12. saddleflow-0.1.0/examples/LiC/random_throw.py +223 -0
  13. saddleflow-0.1.0/examples/LiC/test_set.traj +0 -0
  14. saddleflow-0.1.0/examples/LiC/train.py +174 -0
  15. saddleflow-0.1.0/examples/LiC/train_set.traj +0 -0
  16. saddleflow-0.1.0/examples/LiC_simpler/README.md +139 -0
  17. saddleflow-0.1.0/examples/LiC_simpler/make_small_cell.py +145 -0
  18. saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_debug_zips/structure_rank_None_data.zip +0 -0
  19. saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_status_csvs/status_rank_None.csv +1 -0
  20. saddleflow-0.1.0/examples/LiC_simpler/neb/NEB_trajes/collected_ts_rank_None.traj +0 -0
  21. saddleflow-0.1.0/examples/LiC_simpler/neb/README.md +75 -0
  22. saddleflow-0.1.0/examples/LiC_simpler/neb/config.ini +47 -0
  23. saddleflow-0.1.0/examples/LiC_simpler/neb/data/LiC_simpler_RP.traj +0 -0
  24. saddleflow-0.1.0/examples/LiC_simpler/neb/make_input.py +41 -0
  25. saddleflow-0.1.0/examples/LiC_simpler/neb/run.sh +37 -0
  26. saddleflow-0.1.0/examples/LiC_simpler/neb/saddlemill.log +137 -0
  27. saddleflow-0.1.0/examples/LiC_simpler/neb/traj_files_ordered.json +1 -0
  28. saddleflow-0.1.0/examples/LiC_simpler/one_saddle.traj +0 -0
  29. saddleflow-0.1.0/examples/LiC_simpler/small_n5_one_saddle.traj +0 -0
  30. saddleflow-0.1.0/examples/LiC_simpler/small_n5_six_saddles.traj +0 -0
  31. saddleflow-0.1.0/examples/LiC_simpler/train.py +210 -0
  32. saddleflow-0.1.0/examples/LiC_simpler/viz_checkpoints.py +299 -0
  33. saddleflow-0.1.0/examples/MP20Bat/README.md +152 -0
  34. saddleflow-0.1.0/examples/MP20Bat/analysis_fmax_parity.py +165 -0
  35. saddleflow-0.1.0/examples/MP20Bat/compare_K10_K50.py +511 -0
  36. saddleflow-0.1.0/examples/MP20Bat/data_prep.py +263 -0
  37. saddleflow-0.1.0/examples/MP20Bat/eval_full_testset_K10.py +586 -0
  38. saddleflow-0.1.0/examples/MP20Bat/replot_parity.py +162 -0
  39. saddleflow-0.1.0/examples/MP20Bat/run.sh +201 -0
  40. saddleflow-0.1.0/examples/MP20Bat/sample_and_distance_eval.py +730 -0
  41. saddleflow-0.1.0/examples/MP20Bat/train.py +532 -0
  42. saddleflow-0.1.0/examples/MaterialsSaddles/README.md +258 -0
  43. saddleflow-0.1.0/examples/MaterialsSaddles/analysis_fmax_parity.py +165 -0
  44. saddleflow-0.1.0/examples/MaterialsSaddles/build_pairs.py +72 -0
  45. saddleflow-0.1.0/examples/MaterialsSaddles/check_pretrained.py +56 -0
  46. saddleflow-0.1.0/examples/MaterialsSaddles/compare_K10_K50.py +511 -0
  47. saddleflow-0.1.0/examples/MaterialsSaddles/data_prep.py +297 -0
  48. saddleflow-0.1.0/examples/MaterialsSaddles/dump_multistart.py +82 -0
  49. saddleflow-0.1.0/examples/MaterialsSaddles/dump_predictions.py +196 -0
  50. saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws.py +103 -0
  51. saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws2.py +117 -0
  52. saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws3.py +125 -0
  53. saddleflow-0.1.0/examples/MaterialsSaddles/dump_throws_batched.py +141 -0
  54. saddleflow-0.1.0/examples/MaterialsSaddles/eval_full_testset_K10.py +645 -0
  55. saddleflow-0.1.0/examples/MaterialsSaddles/eval_mp20bat_testset.sh +92 -0
  56. saddleflow-0.1.0/examples/MaterialsSaddles/export_pretrained.py +76 -0
  57. saddleflow-0.1.0/examples/MaterialsSaddles/replot_parity.py +162 -0
  58. saddleflow-0.1.0/examples/MaterialsSaddles/run.sh +300 -0
  59. saddleflow-0.1.0/examples/MaterialsSaddles/sample_and_distance_eval.py +730 -0
  60. saddleflow-0.1.0/examples/MaterialsSaddles/sella_eval.py +139 -0
  61. saddleflow-0.1.0/examples/MaterialsSaddles/sm_collect.py +72 -0
  62. saddleflow-0.1.0/examples/MaterialsSaddles/train.py +1008 -0
  63. saddleflow-0.1.0/examples/MaterialsSaddles/verify_hyperparams.py +183 -0
  64. saddleflow-0.1.0/examples/quickstart/README.md +9 -0
  65. saddleflow-0.1.0/examples/quickstart/quickstart.py +35 -0
  66. saddleflow-0.1.0/examples/quickstart/reactant.traj +0 -0
  67. saddleflow-0.1.0/examples/scaling_bench/README.md +113 -0
  68. saddleflow-0.1.0/examples/scaling_bench/analyze.py +312 -0
  69. saddleflow-0.1.0/examples/scaling_bench/bench_launch.sh +166 -0
  70. saddleflow-0.1.0/examples/scaling_bench/prestage.sh +45 -0
  71. saddleflow-0.1.0/examples/scaling_bench/sweep.sh +62 -0
  72. saddleflow-0.1.0/pyproject.toml +53 -0
  73. saddleflow-0.1.0/saddleflow/__init__.py +14 -0
  74. saddleflow-0.1.0/saddleflow/cli.py +59 -0
  75. saddleflow-0.1.0/saddleflow/data/__init__.py +31 -0
  76. saddleflow-0.1.0/saddleflow/data/convert_to_db.py +124 -0
  77. saddleflow-0.1.0/saddleflow/data/core.py +200 -0
  78. saddleflow-0.1.0/saddleflow/data/db_dataset.py +80 -0
  79. saddleflow-0.1.0/saddleflow/data/materials_saddles_dataset.py +430 -0
  80. saddleflow-0.1.0/saddleflow/data/traj_dataset.py +123 -0
  81. saddleflow-0.1.0/saddleflow/data/transforms.py +100 -0
  82. saddleflow-0.1.0/saddleflow/flow/__init__.py +17 -0
  83. saddleflow-0.1.0/saddleflow/flow/matching.py +1096 -0
  84. saddleflow-0.1.0/saddleflow/flow/sampler.py +416 -0
  85. saddleflow-0.1.0/saddleflow/models/__init__.py +11 -0
  86. saddleflow-0.1.0/saddleflow/models/eigenmode_head.py +56 -0
  87. saddleflow-0.1.0/saddleflow/models/global_attn.py +96 -0
  88. saddleflow-0.1.0/saddleflow/models/time_film.py +118 -0
  89. saddleflow-0.1.0/saddleflow/models/time_filmed_backbone.py +300 -0
  90. saddleflow-0.1.0/saddleflow/models/velocity_head.py +483 -0
  91. saddleflow-0.1.0/saddleflow/pretrained.py +210 -0
  92. saddleflow-0.1.0/saddleflow/search.py +146 -0
  93. saddleflow-0.1.0/saddleflow/utils/__init__.py +34 -0
  94. saddleflow-0.1.0/saddleflow/utils/backbone.py +138 -0
  95. saddleflow-0.1.0/saddleflow/utils/checkpointing.py +69 -0
  96. saddleflow-0.1.0/saddleflow/utils/eval.py +416 -0
  97. saddleflow-0.1.0/saddleflow/utils/forces.py +167 -0
  98. saddleflow-0.1.0/saddleflow/utils/training.py +686 -0
  99. saddleflow-0.1.0/tests/test_autocast_precision.py +70 -0
  100. 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).
@@ -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.