MACE-POLAR + External Field — Organic Force Field
A fork of MACE and MACE-POLAR-1 targeting an external-field-responsive organic force field.
PolarHead— independent E_polar branch on top of a frozen PolarMACE foundation. Physics-aware multipole expansion of the polarisation energy:The outputE_polar(E) = m_l0 (0th) + <m_l1, E> (1st: dipole · field) + m_l0 · |E|² (2nd isotropic: trace α) + <m_l2, Y_2(E)> (2nd anisotropic: traceless α) → MLP → E_polar_iLinearis zero-initialised soE_polar ≡ 0at warm start (bit-wise equivalent to the frozen backbone). Enabled by default.- Per-atom external field
[N, 3](upstream supports only[num_graphs, 3]uniform fields). - Split E_app formula —
qcouples to the per-graph mean fieldE_uniform,μcouples to the per-atom fieldE_i:Restores translation invariance for neutral molecules under MLMM-style spatially varying embedding fields.E_app = Σ_i q_i (E_uniform · R_i) + Σ_i μ_i · E_i - Sign convention unified —
external_fieldis the physical fieldE_phys. Internal blocks expectE_input = -E_phys; bothMACECalculatorandHDF5Dataset(training) negate at load viaAtomicData.from_config(negate_external_field=True)(default). - TorchScript export —
scripts/convert_polar_to_pt.pyproduces libtorch-loadable.ptfor downstream C++ MLMM (sander).
conda create -n macepol python=3.9 -y && conda activate macepol
pip install torch --index-url https://download.pytorch.org/whl/cu118
cd mace-polar && pip install -e .
pip install git+https://github.com/WillBaldwin0/graph_electrostatics.gitPins: e3nn==0.4.4, PyTorch>=2.2, ase. See pyproject.toml.
import os
os.environ["TORCH_FORCE_NO_WEIGHTS_ONLY_LOAD"] = "1"
from ase import Atoms
from mace.calculators import MACECalculator
WEIGHTS = os.environ.get("MACE_POLAR_WEIGHTS", "../mace-polar-mlip")
calc = MACECalculator(
model_paths=f"{WEIGHTS}/MACE-POLAR-1-S.model",
device="cpu", default_dtype="float64",
)
water = Atoms(
"OH2",
positions=[[0.0, 0.0, 0.1173],
[0.0, 0.7572, -0.4692],
[0.0,-0.7572, -0.4692]],
)
water.info["charge"] = 0
water.info["spin"] = 0
water.info["external_field"] = [0.0, 0.0, 0.1] # V/Å, physical (E_phys)
# Or per-atom: [[Ex, Ey, Ez], ...] of shape (N, 3)
water.calc = calc
energy = water.get_potential_energy() # eV
forces = water.get_forces() # eV/Å
charges = calc.results["charges"] # (N,)
MACECalculator.check_state()skipsatoms.infondarrays — changingexternal_fieldalone won't trigger recalculation. Workaround:calc.results = {}before each call.
| Model | Notes |
|---|---|
MACE-POLAR-1-{S,M,L}.model |
upstream OMol25 baselines (Batatia et al., 2026, ωB97M-V) |
macepol-ef |
this fork's production checkpoint (PolarHead-equipped, fine-tuned for external-field response) — recommended for MLMM/sander |
Foundation weights live in the sibling ../mace-polar-mlip/ directory
(not in git); override via MACE_POLAR_WEIGHTS=/path/to/weights.
Under MLMM-style per-atom field E_i, the legacy formula
Σ_i E_i · (q_i R_i + μ_i) breaks translation invariance: shifting a
neutral molecule by d leaves a residual d · Σ_i E_i q_i ≠ 0 because
Σ_i E_i q_i need not vanish when fields differ per atom. This produces
origin-dependent forces in periodic MLMM (e.g. sander vlimit violations).
The split formula puts q onto the per-graph mean E_uniform:
E_app = E_uniform · Σ_i q_i R_i + Σ_i μ_i · E_i
= E_uniform · D_charge + Σ_i μ_i · E_i
- Neutral molecules → translation invariant (Σ q = 0 ⇒ d·Σq term vanishes).
- Charged molecules → ΔE = Q · E_uniform · d on translation, which is the correct work done by the field on a net charge (physical, not bug). Forces are well-defined and translation-invariant in both cases.
- Uniform field → reduces to textbook
−p · E, identical to the legacy formula. So the split is a no-op for upstream training data and a stability fix for downstream MLMM inference.
# From scratch
python scripts/run_train.py \
--name=PolarMACE_run \
--train_file=train.xyz \
--model=PolarMACE \
--r_max=5.0 --device=cuda
# Fine-tune from a foundation checkpoint
python scripts/run_train.py \
--name=ft_run \
--foundation_model=../mace-polar-mlip/MACE-POLAR-1-S.model \
--train_file=train.xyz --valid_file=valid.xyz \
--model=PolarMACE --device=cudaOptional WandB: pip install wandb && export WANDB_PROJECT=macepol-ft.
python scripts/convert_polar_to_pt.py # all S/M/L
python scripts/convert_polar_to_pt.py --model path/to/X.modelVerifies energy / charge / force diff vs eager model = 0 on a water test geometry; bails out otherwise.
| Aspect | upstream | this fork |
|---|---|---|
| External-field shape | [num_graphs, 3] uniform |
[N, 3] per-atom |
E_app formula |
Σ E_i · (q_i R_i + μ_i) |
split: q→E_uniform, μ→E_i |
external_field sign |
E_input = -E_phys (implicit) |
E_phys (negated at load) |
PolarHead (E_polar) |
❌ | ✅ default on |
| TorchScript | partial | scatter / irreps_tools made TS-safe |
bash ./scripts/run_checks.sh # black + isort + pylint
python -m pytest tests/test_polar_models.py -v # polar tests@inproceedings{batatia2022mace,
title={{MACE}: Higher Order Equivariant Message Passing Neural Networks for Fast and Accurate Force Fields},
author={Ilyes Batatia and David P Kovacs and Gregor N. C. Simm and Christoph Ortner and Gabor Csanyi},
booktitle={Advances in Neural Information Processing Systems},
year={2022},
}
@misc{batatia2026macepolar,
title={MACE-POLAR-1: long-range electrostatics and field response in universal ML potentials},
author={Ilyes Batatia et al.},
year={2026},
eprint={2602.19411},
archivePrefix={arXiv},
}MIT (same as upstream). See LICENSE.md.