Skip to content

Commit 05c86ea

Browse files
timduignanclaudevsimkus
authored
Add OrbMol-v2 with learnable electrostatics (#162)
* Port orbmol-v2 (learnable electrostatics) from the reference codebase Wholesale port from the reference codebase (post the ZBL-sum + Coulomb constant fix, with ZBL-sum + correct Coulomb constant fixes), targeting only the s11doh8x:v199 public release. Drops all backwards-compat for prior electrostatics_config checkpoints; skips Fukui, global_context, and self_message internal-only features. New: - orb_models/forcefield/models/coulomb_module.py: CoulombModule + direct/PME - scripts/convert_orbmol_v2_ckpt.py: extract EMA-applied flat state_dict from wandb-format checkpoint (orbmol_v2() expects flat state_dicts per orb-models S3 convention) Modified: - forcefield_heads.py: ChargeConditionedEnergyHead, LatentChargeHead, LatentSpinHead. Added EnergyHead.absolute_energy() helper. - conservative_regressor.py: coulomb_module field, latent_charges/spins predicted before energy, ChargeConditionedEnergyHead path, Coulomb energy + explicit forces/virial plumbing. - pair_repulsion.py: default node_aggregation "mean" -> "sum". - pretrained.py: orbmol_v2_architecture() and orbmol_v2() loader mirroring the source codebase. CoulombModule() defaults (no erf damping); enforce_total_charge=True; no coulomb_constant override. Verified against gold values internal reference values for s11doh8x:v199 (with EMA applied, CPU fp32): - H2O energy: -2079.86339 eV (diff 2.7e-7) - H2O forces[0]: matches all components within 4e-6 eV/A - Cu fcc energy: -178549.38592 eV (relative diff ~6e-10) - Cu fcc stress (Voigt 6): matches within 5e-6 eV/A^3 All 88 existing forcefield tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * orbmol-v2 cleanup: preserve pair_repulsion BC, simplify absolute_energy - Revert pair_repulsion.py default to "mean" (preserves BC for older orb-v3 conservative models trained pre ZBL-sum cutoff). - ConservativeForcefieldRegressor accepts pair_repulsion_node_aggregation kwarg (defaults to "mean"); orbmol_v2_architecture passes "sum" explicitly. - EnergyHead.absolute_energy: drop fp64 arg, always do the addition in fp64 and return fp64. OMol references reach ~1e5 eV so kJ/mol resolution requires fp64; option only added confusion. Used only by ChargeConditioned path so legacy heads are unaffected. - ConservativeForcefieldRegressor.predict drops fp64_energy arg accordingly. - Delete scripts/convert_orbmol_v2_ckpt.py — core/scripts/misc/export_model.py is the existing tool used for all other public orb-models releases. - Update orbmol_v2() docstring to point at core's export_model.py. Re-verified gold values match (H2O 2.7e-7; Cu fp32-noise level on energy, 1e-6 on stress). All 88 forcefield tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add BC guard tests for orbmol-v2 port - test_pair_repulsion_default_aggregation_is_mean: catches anyone changing the regressor default from "mean" to "sum", which would silently break all public orb-v3 conservative models on reload. - test_pair_repulsion_sum_when_specified: confirms orbmol-v2 opt-in via the kwarg works. - test_energy_head_does_not_have_absolute_energy: guards against the fp64-promoting helper migrating onto the base EnergyHead, where it would alter v3 conservative omol/omat/mpa predictions. - test_orbmol_v2_architecture_uses_sum_zbl: integration check that the architecture wires CoulombModule + sum-aggregation ZBL + latent_charges/latent_spins heads. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * orbmol_v2: HF weights URL, bump nvalchemiops, add network smoke test - Default weights_path now points at HF (orbital-materials/orbmol-v2/resolve/main). Matches the date-in-filename versioning convention used by other public orb-models S3 ckpts; no separate revision pin (HF main behaves like a stable S3 path). - Bump nvalchemi-toolkit-ops>=0.3.1: the orbmol_v2 PME path uses particle_mesh_ewald(..., hybrid_forces=True) which only exists on >=0.3.1. - New tests/forcefield/test_orbmol_v2_smoke.py: end-to-end check that the published weights produce the gold H2O / Cu predictions from internal reference tests. Gated by ORB_RUN_NETWORK_TESTS=1 since it downloads ~100 MB; opt-in only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Cap nvalchemi-toolkit-ops at <0.4 Upstream bumped APIs between 0.3.0 and 0.3.1 (added the hybrid_forces parameter we depend on for PME). Upper-bounding at the next minor prevents 0.4.x from silently breaking the orbmol-v2 PME path on user upgrades — matches the bound style of other deps internally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Document orbmol-v2 in MODELS.md Adds an entry under OrbMol Models describing the learnable electrostatics extension (LatentChargeHead, LatentSpinHead, CoulombModule, and the ChargeConditionedEnergyHead) with usage example. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix MODELS.md: bare-1/r Coulomb for non-periodic, drop size-consistency note s11doh8x trains with CoulombModule defaults (no erf damping); the non-periodic Coulomb path is bare 1/r, not erf-damped. Also drops the size-consistency wording from the head description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Polish docs and docstrings for orbmol-v2 - README.md: add orbmol-v2 release note in the "What's new" section - coulomb_module.py: fix misleading docstrings (the non-periodic path is bare 1/r when sigma is None, not erf-damped); update stale 14.33 → 14.40 reference for COULOMB_CONSTANT; minor typo fix - forcefield_heads.py: drop size-consistency claims from ChargeConditionedEnergyHead docstrings (separate concern; not asserted in the public release) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Ported changes from the internal repo * Soften LatentSpinHead claims; document fp64 energy default - Don't assert per-atom spins are physical observables (we haven't validated this; reword as "auxiliary per-atom features that take the system's spin multiplicity into account") in MODELS.md and README.md. - Add a note in the README orbmol-v2 update that energies are now fp64 by default for kJ/mol resolution against OMol25-scale references; opt out via fp64_energy=False on predict(). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Soften charge/spin head wording; bump torch-sim-atomistic to 0.6.0 - MODELS.md and README.md: rephrase LatentChargeHead/LatentSpinHead as predicting per-atom *latent features* (constrained at the system level) rather than asserting per-atom charges and spins. Caveat blockquote added in the previous commit explains the emergent nature. - pyproject.toml: bump dev `torch-sim-atomistic` from >=0.5.1 to >=0.6.0. The 0.6.0 release adds `SimState.has_extras()`, which the existing `test_forcefield_adapter_parses_spin_and_charge_from_simstate` test already calls; 0.5.2 (allowed by the old constraint) does not have this method. All 121 forcefield tests now pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Handle node/graph features/targets in from_ase_atoms_list * Update examples to orbmol_v2 * Refocus README orbmol-v2 update on CoulombModule + headline benchmarks Per Ben's review feedback (PR #162 thread): - First bullet now leads with the CoulombModule, not the latent heads - Adds Speed (H100 QPS at 1k/10k atoms, periodic systems) and Accuracy (GSCDB138 Normalized Error Ratio, ex single-atom-species reactions) highlights - Moves the LatentChargeHead / LatentSpinHead detail and the Caution blockquote out of the README; they remain in MODELS.md - Links out to MODELS.md for the full architecture description Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Pass explicit n_node to scatter_mean (port of #3074) aggregate_nodes(..., reduction="mean") now forwards n_node directly to scatter_mean, which takes pre-computed group sizes instead of building a divisor inside the graph from scatter_sum(ones, ...). The old form triggered a torch.compile + autograd miscompile (~1 eV on H2O / ethanol / NaCl). Math is bit-identical in eager. Drops segment_mean() and its test (unused after this change). Ports orbital-materials/orb#3074. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Compile-safe heads + drop in-place ops on Coulomb path (port of #3074) LatentChargeHead / LatentSpinHead centering now uses repeat_interleave over n_node instead of gather-by-node_batch_index. Compiles cleanly under dynamic shapes. conservative_regressor drops the in-place interaction_energy += coulomb_energy. pair_repulsion casts the poly cutoff exponent with p.to(torch.float32) instead of float(p), which avoids a Tensor.item() graph break, and zeros stress entries beyond 1e10. Ports orbital-materials/orb#3074. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Enable full-model torch.compile for orbmol-v2 (port of #3074) Removes the backbone-only .compile() overrides on the {Conservative,Direct}ForcefieldRegressor classes. model.compile(...) now wraps the full forward via standard nn.Module.compile(). Note: the GraphRegressor.compile() override was already absent in public, so no change there. CoulombModule's PME path is split into two @torch.compiler.disable helpers (_particle_mesh_ewald and _estimate_pme_params_and_neighbors) so dynamo skips nvalchemiops's ctypes-using PME entirely while the rest of the regressor compiles. End-to-end (per core PR): ~1.5-1.6x faster inference (1.62x at 10k atoms, single H100); compiled-vs-eager numerical diff at float-noise level. Adds tests/forcefield/test_direct_regressor.py and tests/expensive_tests/test_compilation.py; updates conftest and test_conservative compile tests. Adds scripts/compile_numerical_check.py for offline diagnosis (adapted to public pretrained.orbmol_v2 loader). Ports orbital-materials/orb#3074. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Switch orbmol-v2 default to teqabfhg (no per-atom spin head) Updates weights_path to the new teqabfhg checkpoint (orbmol-v2-teqabfhg-20260523.ckpt) which has no LatentSpinHead. Adds use_per_atom_spins to orb_v3_conservative_architecture (default False) so the spin head and ChargeConditionedEnergyHead.use_spins are controlled together. System-level charge/spin conditioning via ChargeSpinConditioner is unchanged. A future spin-enabled checkpoint can pass use_per_atom_spins=True from its pretrained.* entry without further architecture changes. Updates test_backwards_compatibility goldens to match the new checkpoint: - H2O: -2079.86339 -> -2079.86222 eV - Cu fcc: -178549.3860 -> -178550.9810 eV; stress retuned - forces/stress vectors regenerated on Mac arm64 CPU Old checkpoint URL (orbmol-v2-s11doh8x-20260507.ckpt) was removed from S3 and now 404s; users loading that file via state_dict manually will hit a shape mismatch on the energy head MLP and the missing latent spin head. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Docs: drop LatentSpinHead language from MODELS.md orbmol_v2's default checkpoint (teqabfhg) has no per-atom spin head. Updates the MODELS.md description to mention only LatentChargeHead and clarifies that system-level total charge / spin multiplicity still flow through the ChargeSpinConditioner. Caution paragraph updated to drop the per-atom spin language. README headline GSCDB138 numbers reflect the previous s11doh8x checkpoint and will be refreshed once a full per-category breakdown for teqabfhg is available. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Refresh README orbmol-v2 benchmarks for teqabfhg Updates GSCDB138 numbers from the s11doh8x measurement to the new teqabfhg checkpoint (same 109 evaluable subsets / 5152 reactions): - Overall NER: 6.05 -> 1.62 (was 1.83 on s11doh8x) - NC, TC, TM, BH, INC, ISO per-category numbers refreshed - ISO regresses slightly (1.03 -> 1.36); other categories improve Adds a second bullet covering GMTKN55 WTMAD-2 (5.41 -> 4.37 kcal/mol), WIGGLE150 RMSE (1.23 -> 1.19), and the two new evaluation entries BEGDB (MAE 0.235) and ACONFL (RMSE 0.40). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * scripts/speed.py: set charge/spin on random crystals (orbmol_v2 support) orbmol_v2's ChargeSpinConditioner requires total_charge and spin_multiplicity in system_features. Without them the adapter's _get_charge_and_spin returns an empty dict and the conditioner errors. Defaulting to charge=0, spin=1 (singlet) matches the convention used in the internal speed harness. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Refresh README speed bullet with teqabfhg numbers Replaces the s11doh8x-era "44 QPS at 1k atoms / 9 QPS at 10k atoms / within ~5% of v1" claim with absolute ms timings from BENCH on the new teqabfhg checkpoint (full-model torch.compile, single GPU, periodic systems): 30/42/116/191 ms at 100/1k/5k/10k atoms. Notes that the no-spin variant is 1-12% faster than the prior spin-having v2 development variant at every size we tested. The v1 parity claim is removed pending a fresh v1-vs-v2 comparison run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * README speed bullet: v1 vs v2-teqabfhg table from BENCH Replaces the standalone v2-teqabfhg ms numbers with a v1-vs-v2 table at 100/1k/5k/10k atoms. v2 is slower at small sizes (electrostatics overhead) and faster at large sizes; crossover around 5k atoms, 1.46x speedup at 10k. v1 runs with backbone-only compile (its best available), v2 with full-model compile thanks to the port of orbital-materials/orb#3074. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * README speed bullet: 3-column table including v1 full-compile The full-model torch.compile changes ported from #3074 also benefit OrbMol-v1, not just v2. New BENCH numbers show v1-full-compile is 1.3-1.8x faster than the old backbone-only workaround (biggest at 10k atoms: 278 -> 158 ms). v2-teqabfhg is ~20-60% slower than v1 full-compile at the same system size, reflecting the real cost of the PME / Coulomb path; the previous narrative ("v2 crosses over and becomes faster than v1 at 5k+ atoms") was an artifact of comparing v1-backbone-compile to v2-full-compile. Also softens the lead-in line about LES being free; it isn't, but the accuracy gain (3.7x lower NER) is the trade. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * finetune.py: detect conservative-vs-direct from model, not name string Previously is_conservative_model = "conservative" in args.base_model, which misclassified orbmol_v2 as direct. Build the model first, then check "grad_forces" in model.loss_weights, and push custom CLI weights via model.loss_weights.update(...). * Address Vaidas review comments - Remove scripts/compile_numerical_check.py (covered by internal tests now) - MODELS.md: reword charge/spin requirement to match orbmol-v1 phrasing - README.md: drop internal commit ref; trim May 2026 update to two bullets (electrostatics+headline result; full-model compile speedup) - test_backwards_compatibility.py: restore full-precision cu_energy_gold (-178550.98098) * Address Vaidas follow-up review comments - finetune.py: use isinstance(model, ConservativeForcefieldRegressor) instead of inspecting loss_weights; fix stale comment - pretrained.py:418: drop "spins" from orbmol_v2 docstring - README.md:27: drop "applies to v1 and v2" parenthetical (compile speedup applies to all models, the parenthetical was misleading) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Vaidas Simkus <vaidas@orbitalmaterials.com>
1 parent 6857ef6 commit 05c86ea

29 files changed

Lines changed: 1640 additions & 429 deletions

FINETUNING_GUIDE.md

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ Number of atoms: 3
151151
```bash
152152
python finetune.py \
153153
--data_path /path/to/your/dataset.db \
154-
--base_model orb_v3_conservative_omol \
154+
--base_model orbmol_v2 \
155155
--energy_loss_weight 0.1 \
156156
--forces_loss_weight 1.0 \
157157
--stress_loss_weight 0.0 \
@@ -164,7 +164,7 @@ python finetune.py \
164164
```bash
165165
python finetune.py \
166166
--data_path /path/to/your/dataset.db \
167-
--base_model orb_v3_conservative_omol \
167+
--base_model orbmol_v2 \
168168
--custom_reference_energies /path/to/reference_energies.json \
169169
--energy_loss_weight 0.1 \
170170
--forces_loss_weight 1.0
@@ -175,7 +175,7 @@ python finetune.py \
175175
```bash
176176
python finetune.py \
177177
--data_path /path/to/your/dataset.db \
178-
--base_model orb_v3_conservative_omol \
178+
--base_model orbmol_v2 \
179179
--custom_reference_energies /path/to/reference_energies.json \
180180
--trainable_reference_energies \
181181
--energy_loss_weight 0.1 \
@@ -187,7 +187,7 @@ python finetune.py \
187187
```bash
188188
python finetune.py \
189189
--data_path /path/to/your/dataset.db \
190-
--base_model orb_v3_conservative_omol \
190+
--base_model orbmol_v2 \
191191
--trainable_reference_energies \
192192
--energy_loss_weight 0.1 \
193193
--forces_loss_weight 1.0
@@ -258,7 +258,7 @@ Lines starting with `#` are treated as comments and ignored.
258258

259259
The script automatically handles the differences between conservative and direct models:
260260

261-
- **Conservative models** (e.g., `orb_v3_conservative_omol`):
261+
- **Conservative models** (e.g., `orbmol_v2`):
262262
- Use `grad_forces` and `grad_stress` as **loss-weight keys**
263263
- Compute forces via automatic differentiation
264264

@@ -276,7 +276,7 @@ If you prefer to write your own finetuning script, you can use the clean API dir
276276
from orb_models.forcefield import pretrained
277277

278278
# Load model with custom configuration
279-
model, atoms_adapter = pretrained.orb_v3_conservative_omol(
279+
model, atoms_adapter = pretrained.orbmol_v2(
280280
device='cuda',
281281
precision='float32-high',
282282
train=True,
@@ -319,7 +319,7 @@ import torch
319319
from orb_models.forcefield import pretrained
320320

321321
# Load model architecture (set train=False for inference)
322-
model, atoms_adapter = pretrained.orb_v3_conservative_omol(train=False)
322+
model, atoms_adapter = pretrained.orbmol_v2(train=False)
323323

324324
# Load your finetuned checkpoint
325325
model.load_state_dict(torch.load('path/to/finetuned_checkpoint.pt'))
@@ -331,7 +331,7 @@ You can also specify loss weights when loading for further finetuning:
331331

332332
```python
333333
# Load for continued finetuning with different loss weights
334-
model, atoms_adapter = pretrained.orb_v3_conservative_omol(
334+
model, atoms_adapter = pretrained.orbmol_v2(
335335
train=True,
336336
loss_weights={'energy': 0.5, 'grad_forces': 20.0}
337337
)
@@ -358,7 +358,7 @@ Finetuning on ORCA wB97M-V data with different reference scheme:
358358
```bash
359359
python finetune.py \
360360
--data_path my_dataset.db \
361-
--base_model orb_v3_conservative_omol \
361+
--base_model orbmol_v2 \
362362
--custom_reference_energies my_refs.json \
363363
--energy_loss_weight 1.0 \
364364
--forces_loss_weight 10.0 \
@@ -370,7 +370,7 @@ python finetune.py \
370370
from orb_models.forcefield import pretrained
371371
import torch
372372

373-
model, atoms_adapter = pretrained.orb_v3_conservative_omol(train=False)
373+
model, atoms_adapter = pretrained.orbmol_v2(train=False)
374374
model.load_state_dict(torch.load('checkpoints/my_finetuned_model.pt'))
375375
# Reference energies from my_refs.json are now loaded!
376376
```
@@ -384,7 +384,7 @@ from orb_models.forcefield import pretrained
384384
from orb_models.dataset.ase_sqlite_dataset import AseSqliteDataset
385385

386386
# Load model with configuration
387-
model, atoms_adapter = pretrained.orb_v3_conservative_omol(
387+
model, atoms_adapter = pretrained.orbmol_v2(
388388
device='cuda',
389389
train=True,
390390
train_reference_energies=False, # Fixed reference energies

MODELS.md

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,27 @@ We provide several pretrained models that can be used to calculate energies, for
77
These models are a continuation of the [`orb-v3`](#v3-models) series trained on the [Open Molecules 2025 (OMol25)](https://arxiv.org/pdf/2505.08762) dataset—over 100M high-accuracy DFT calculations (ωB97M-V/def2-TZVPD) on diverse molecular systems including metal complexes, biomolecules, and electrolytes. Note: The training data does not contain periodic systems and these models have not been carefully tested on periodic systems.
88

99
There are two options:
10-
* `orb-v3-conservative-omol`
11-
* `orb-v3-direct-omol`
10+
* `orbmol-v1-conservative`
11+
* `orbmol-v1-direct`
1212

1313
See below for more explanation of this naming convention. Both models have `inf` neighbors, ensuring a continuous PES.
1414

15+
### OrbMol-v2 (learnable electrostatics)
16+
17+
* `orbmol-v2`
18+
19+
OrbMol-v2 extends the OrbMol architecture with **learnable per-atom electrostatics**: a `LatentChargeHead` predicts per-atom latent charges constrained to sum to the system total charge, and a `CoulombModule` adds a long-range Coulomb energy on top of the GNN, direct bare-1/r Coulomb sum for non-periodic systems, Particle Mesh Ewald via `nvalchemiops` for periodic systems. The energy head (`ChargeConditionedEnergyHead`) is conditioned on the per-atom charges. Similar to orbmol-v1, system-level total charge and spin are required.
20+
21+
Trained on OMol25 and OPoly26 (ωB97M-V/def2-TZVPD); supports both periodic and non-periodic systems. Stress is enabled via `model.enable_stress()` if needed.
22+
23+
```python
24+
from orb_models.forcefield.pretrained import orbmol_v2
25+
model, atoms_adapter = orbmol_v2(device="cuda")
26+
# atoms.info["charge"] and atoms.info["spin"] (multiplicity, = 2S+1) must be set.
27+
```
28+
29+
> **Caution:** While the model does predict per-atom charge values as a latent feature in the charge head, the model has not seen any per-atom charge values during training; these are emergent from optimisation against energies and forces alone. They should therefore be treated with caution: while in at least some cases they appear to correspond to the correct physical values, the reliability and generality of this correspondence is unclear and is the subject of ongoing investigations.
30+
1531
### [V3 Models](https://arxiv.org/abs/2504.06231)
1632

1733
V3 models use the following naming convention: ```orb-v3-X-Y-Z``` where:

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,20 @@ Alternatively, you can use Docker to run orb-models; [see instructions below](#d
2121

2222
### Updates
2323

24+
**May 2026**: Release of OrbMol-v2 — adds a `CoulombModule` for long-range electrostatics on top of the OrbMol architecture, using direct Coulomb summation for non-periodic systems and Particle Mesh Ewald (via `nvalchemiops`) for periodic. Trained on OMol25 and OPoly26 (ωB97M-V/def2-TZVPD); load with `pretrained.orbmol_v2(device="cuda")`. See [MODELS.md](MODELS.md) for the full architecture description.
25+
26+
* **Long-range electrostatics and learnable charges.** GSCDB138 Normalized Error Ratio drops from **6.05 → 1.62** (3.7× lower, comparable to a good DFT functional).
27+
* **Full-model compilation.** `model.compile(...)` now wraps the full regressor for all models, giving ~1.7× speedup at 10k atoms on a single 80 GB GPU.
28+
29+
`model.predict(...)["energy"]` now returns **fp64** by default to preserve kJ/mol resolution against OMol-scale references (~1e4–1e5 eV). Pass `fp64_energy=False` to opt out.
30+
2431
**February 2026**: Improved GPU-accelerated graph construction with [ALCHEMI Toolkit-Ops](https://github.com/NVIDIA/nvalchemi-toolkit-ops) and batched simulation with [TorchSim](https://github.com/TorchSim/torch-sim):
2532

2633
* Alchemi-based graph construction (GPU-accelerated, up to 12x faster for large single systems, and sub-linear batch scaling delivering >100x graph construction speed-up for large batches of small systems)
2734
* TorchSim wrapper for batched optimisation and simulation, see [usage with TorchSim](#usage-with-torchsim)
2835
* Alchemi-based D3 dispersion correction module, see [D3 correction](#d3-correction)
2936

37+
3038
**August 2025**: Release of the [OrbMol potentials](https://www.orbitalindustries.com/posts/orbmol-extending-orb-to-molecular-systems):
3139

3240
* Trained on the [Open Molecules 2025 (OMol25)](https://arxiv.org/pdf/2505.08762) dataset—over 100M high-accuracy DFT calculations (ωB97M-V/def2-TZVPD) on diverse molecular systems including metal complexes, biomolecules, and electrolytes.
@@ -189,7 +197,7 @@ from ase.build import molecule
189197
from orb_models.forcefield import pretrained
190198

191199
device = "cpu" # or device="cuda"
192-
orbff, atoms_adapter = pretrained.orb_v3_conservative_omol(
200+
orbff, atoms_adapter = pretrained.orbmol_v2(
193201
device=device,
194202
precision="float32-high", # or "float32-highest" / "float64
195203
)

examples/NaClWaterMD.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ def run_md_simulation(
6060
# Set the calculator
6161
# Note: If you encounter compilation errors (e.g., Triton issues on clusters),
6262
# you can disable compilation by adding compile=False:
63-
# orbff, atoms_adapter = pretrained.orb_v3_conservative_omol(device=device, compile=False)
64-
orbff, atoms_adapter = pretrained.orb_v3_conservative_omol(device=device)
63+
# orbff, atoms_adapter = pretrained.orbmol_v2(device=device, compile=False)
64+
orbff, atoms_adapter = pretrained.orbmol_v2(device=device)
6565
atoms.calc = ORBCalculator(orbff, atoms_adapter=atoms_adapter, device=device)
6666

6767
# Set the initial velocities

finetune.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
from orb_models.common.training.util import get_optim, init_device
3030
from orb_models.common.utils import seed_everything
3131
from orb_models.forcefield import pretrained
32+
from orb_models.forcefield.models.conservative_regressor import (
33+
ConservativeForcefieldRegressor,
34+
)
3235

3336
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
3437

@@ -439,26 +442,30 @@ def run(args):
439442
# GPUs and does not appear to hurt training
440443
precision = "float32-high"
441444

442-
# Prepare loss weights if specified
443-
loss_weights = {}
444-
is_conservative_model = "conservative" in args.base_model
445+
# Instantiate model with configuration
446+
base_model = args.base_model
447+
model, atoms_adapter = getattr(pretrained, base_model)(
448+
device=device,
449+
precision=precision,
450+
train=True,
451+
train_reference_energies=args.trainable_reference_energies,
452+
)
453+
454+
# Detect conservative vs direct from the instantiated model type.
455+
is_conservative_model = isinstance(model, ConservativeForcefieldRegressor)
445456

457+
# Map CLI loss-weight flags onto the keys the instantiated model expects.
458+
loss_weights: dict[str, float] = {}
446459
if args.energy_loss_weight is not None:
447460
loss_weights["energy"] = args.energy_loss_weight
448461

449462
if args.forces_loss_weight is not None:
450-
# Key depends on model type
451-
if is_conservative_model:
452-
loss_weights["grad_forces"] = args.forces_loss_weight
453-
else: # direct model
454-
loss_weights["forces"] = args.forces_loss_weight
463+
key = "grad_forces" if is_conservative_model else "forces"
464+
loss_weights[key] = args.forces_loss_weight
455465

456466
if args.stress_loss_weight is not None:
457-
# Key depends on model type
458-
if is_conservative_model:
459-
loss_weights["grad_stress"] = args.stress_loss_weight
460-
else: # direct model
461-
loss_weights["stress"] = args.stress_loss_weight
467+
key = "grad_stress" if is_conservative_model else "stress"
468+
loss_weights[key] = args.stress_loss_weight
462469

463470
if args.equigrad_loss_weight is not None:
464471
if not is_conservative_model:
@@ -471,16 +478,7 @@ def run(args):
471478
for key, val in loss_weights.items():
472479
logging.info(f" {key}: {val}")
473480
logging.info("=" * 60)
474-
475-
# Instantiate model with configuration
476-
base_model = args.base_model
477-
model, atoms_adapter = getattr(pretrained, base_model)(
478-
device=device,
479-
precision=precision,
480-
train=True,
481-
train_reference_energies=args.trainable_reference_energies,
482-
loss_weights=loss_weights if loss_weights else None,
483-
)
481+
model.loss_weights.update(loss_weights)
484482

485483
# Handle custom reference energies if provided
486484
if args.custom_reference_energies:
@@ -678,12 +676,15 @@ def main():
678676
type=str,
679677
help="Base model to finetune.",
680678
choices=[
679+
"orbmol_v2",
680+
"orb_v3_conservative_omol",
681+
"orb_v3_direct_omol",
682+
"orbmol_v1_conservative",
683+
"orbmol_v1_direct",
681684
"orb_v3_conservative_inf_omat",
682685
"orb_v3_conservative_20_omat",
683686
"orb_v3_direct_inf_omat",
684687
"orb_v3_direct_20_omat",
685-
"orb_v3_conservative_omol",
686-
"orb_v3_direct_omol",
687688
"orb_v2",
688689
],
689690
)

orb_models/common/models/base.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ def loss(self, batch: T) -> ModelOutput:
3636
"""Encodes to latents before message passing."""
3737
raise NotImplementedError()
3838

39+
def prepare_for_inference(self) -> None:
40+
"""Hook called before inference. Override to enable inference-only features."""
41+
3942

4043
class RegressorModelMixin[T: AbstractAtomBatch](ModelMixin[T]):
4144
"""Model Mixin for our regression models."""

orb_models/common/models/segment_ops.py

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def aggregate_nodes(
4444
if reduction == "sum":
4545
return scatter_sum(tensor, segments, dim=0, dim_size=count)
4646
elif reduction == "mean":
47-
return scatter_mean(tensor, segments, dim=0, dim_size=count)
47+
return scatter_mean(tensor, segments, n_node, dim=0, dim_size=count)
4848
elif reduction == "max":
4949
return segment_max(tensor, segments, num_segments=count)
5050
else:
@@ -61,11 +61,6 @@ def segment_max(data: torch.Tensor, segment_ids: torch.Tensor, num_segments: int
6161
return scatter_max(data, segment_ids, dim=0, dim_size=num_segments)
6262

6363

64-
def segment_mean(data: torch.Tensor, segment_ids: torch.Tensor, num_segments: int):
65-
"""Computes index based mean over segments of a tensor."""
66-
return scatter_mean(data, segment_ids, dim=0, dim_size=num_segments)
67-
68-
6964
def segment_softmax(
7065
data: torch.Tensor,
7166
segment_ids: torch.Tensor,
@@ -222,6 +217,7 @@ def scatter_std(
222217
def scatter_mean(
223218
src: torch.Tensor,
224219
index: torch.Tensor,
220+
count: torch.Tensor,
225221
dim: int = -1,
226222
out: torch.Tensor | None = None,
227223
dim_size: int | None = None,
@@ -231,6 +227,7 @@ def scatter_mean(
231227
Args:
232228
src (torch.Tensor): The source tensor.
233229
index (torch.Tensor): The indices of elements to scatter.
230+
count (torch.Tensor): Pre-computed group sizes (e.g. n_node).
234231
dim (int, optional): The dimension along which to index. Defaults to -1.
235232
out (Optional[torch.Tensor], optional): The output tensor. Defaults to None.
236233
dim_size (Optional[int], optional): Size of the output tensor. Defaults to None.
@@ -241,20 +238,13 @@ def scatter_mean(
241238
out = scatter_sum(src, index, dim, out, dim_size)
242239
dim_size = out.size(dim)
243240

244-
index_dim = dim
245-
if index_dim < 0:
246-
index_dim = index_dim + src.dim()
247-
if index.dim() <= index_dim:
248-
index_dim = index.dim() - 1
249-
250-
ones = torch.ones(index.size(), dtype=src.dtype, device=src.device)
251-
count = scatter_sum(ones, index, index_dim, None, dim_size)
252-
count[count < 1] = 1
253-
count = _broadcast(count, out, dim)
241+
divisor = count.to(dtype=out.dtype)
242+
divisor = divisor.clamp(min=1)
243+
divisor = _broadcast(divisor, out, dim)
254244
if out.is_floating_point():
255-
out.true_divide_(count)
245+
out.true_divide_(divisor)
256246
else:
257-
out.div_(count, rounding_mode="floor")
247+
out.div_(divisor, rounding_mode="floor")
258248
return out
259249

260250

0 commit comments

Comments
 (0)