Custom integrations¶
This page is for embedding SO3LR into your own simulation code, through the potential energy function and the JAX-MD interface. For standard simulations use the CLI, which also runs the faster flash engine, and for Python-level work use the ASE calculator.
Which interface should I use?¶
| You want to… | Use |
|---|---|
| Run standard MD, optimization or evaluation | The CLI |
| Use ASE tooling (optimizers, NEB, vibrations) | So3lrCalculator |
| Write a custom JAX-MD simulation | to_jax_md |
| Embed SO3LR in a non-JAX-MD code | So3lrPotential |
The potential energy function¶
So3lrPotential is a pure jax function which takes a Graph as input and
returns the per-atom potential energies with shape (n_atoms,), so you sum
them to get the total energy. It works with jax.jit, jax.vmap and
jax.grad, and has no JAX-MD dependency.
from so3lr import So3lrPotential, Graph
# `Graph` is built from your own system. See "The Graph object" below.
graph = Graph(...)
so3lr_potential = So3lrPotential(model="so3lr-tauri-m")
energy = so3lr_potential(graph).sum()
The full signature is in the API reference.
The Graph object¶
The Graph object is a collections.namedtuple which abstracts the molecule
as a graph. Its fields are:
| Field | Contents |
|---|---|
nodes |
Atomic numbers, (n_atoms,) |
positions |
Cartesian positions, Å, (n_atoms, 3) |
centers, others, edges |
Short-range pairs within the model's cutoff, both directions, and their displacement vectors (n_pairs, 3) |
idx_i_lr, idx_j_lr, edges_lr |
Long-range pairs within lr_cutoff, each pair once, and their displacement vectors |
total_charge, num_unpaired_electrons |
Shape (1,) |
cell |
Lattice vectors as columns (atoms.cell.T), or None in vacuum |
theory_mask |
The theory-level head to read. Required; use jnp.eye(16)[5][None]. |
k_grid, k_smearing, k_grid_disp, k_smearing_disp |
K-space grids and smearings; optional |
residue_segments, residue_charge |
Per-fragment charge constraints; optional |
mask |
Not read by So3lrPotential |
Listing a long-range pair in both directions double-counts it. Unused index
slots may be padded with n_atoms in both rows, as jax-md does.
To integrate SO3LR into an existing code, write a function that turns your
input structure into a Graph:
def system_to_graph(my_system) -> Graph:
"""Translate your internal representation into a SO3LR Graph."""
...
Forces¶
The network reads the displacement vectors in edges and edges_lr, so the
gradient with respect to graph.positions is not the force. Rebuild the
displacements from the positions inside the function you differentiate:
import jax
def with_positions(R, template):
"""The template's pairs, with displacements recomputed from positions R."""
return template._replace(
positions=R,
edges=R[template.centers] - R[template.others],
edges_lr=R[template.idx_i_lr] - R[template.idx_j_lr],
)
@jax.jit
def energy_and_forces(R, template):
def total_energy(positions):
return so3lr_potential(with_positions(positions, template)).sum()
energy, grad = jax.value_and_grad(total_energy)(R)
return energy, -grad
This is the free-space form; in a periodic box, apply the minimum-image
convention to the differences. to_jax_md does
this for you.
Auxiliary outputs¶
Like the ASE calculator, the potential can return intermediate quantities:
See Observables for what aux contains.
Driving JAX-MD directly¶
JAX-MD is a molecular dynamics library
optimized for GPUs, and to_jax_md connects a So3lrPotential to it.
A minimal gas-phase example¶
import jax
import jax.numpy as jnp
import numpy as np
from ase import Atoms
from jax_md import space, quantity
from so3lr import to_jax_md, So3lrPotential
atoms = Atoms("H2", positions=[(0, 0, 0), (0, 0, 0.74)])
positions = jnp.array(atoms.get_positions())
atomic_numbers = jnp.array(atoms.get_atomic_numbers())
# Assume there is no box (free space)
box = None
displacement, shift = space.free()
neighbor_fn, neighbor_fn_lr, energy_fn = to_jax_md(
potential=So3lrPotential(model="so3lr-tauri-m", lr_cutoff=100.0), # gas phase
displacement_or_metric=displacement,
box_size=box,
species=atomic_numbers,
capacity_multiplier=1.25,
buffer_size_multiplier_sr=1.25,
buffer_size_multiplier_lr=1.25,
minimum_cell_size_multiplier_sr=1.0,
disable_cell_list=True,
fractional_coordinates=False,
)
energy_fn = jax.jit(energy_fn)
force_fn = jax.jit(quantity.force(energy_fn))
# Allocate the short- and long-range neighbor lists
nbrs = neighbor_fn.allocate(positions, box=box)
nbrs_lr = neighbor_fn_lr.allocate(positions, box=box)
energy = energy_fn(positions, neighbor=nbrs.idx, neighbor_lr=nbrs_lr.idx, box=box)
forces = force_fn(positions, neighbor=nbrs.idx, neighbor_lr=nbrs_lr.idx, box=box)
print("Energy =", np.array(energy))
print("Forces =", np.array(forces))
to_jax_md arguments¶
| Argument | Meaning |
|---|---|
potential |
A So3lrPotential instance. |
displacement_or_metric |
From jax_md.space.free() or space.periodic_general(...). |
box_size |
None for free space, otherwise the box. |
species |
Atomic numbers. |
capacity_multiplier |
One capacity multiplier for both lists; None (default) uses each list's buffer multiplier. |
buffer_size_multiplier_sr / _lr |
Short- and long-range buffer headroom (default 1.25). |
minimum_cell_size_multiplier_sr / _lr |
Cell-list cell size multipliers (default 1.0). |
disable_cell_list |
True for small or non-periodic systems. |
fractional_coordinates |
Whether positions are fractional. Defaults to True and must match your space choice. |
total_charge, num_unpaired_electrons |
Net charge (e) and spin of the system (default 0). |
precision |
Dtype of the auxiliary arrays. None follows the positions. |
Anything else is forwarded to jax_md.partition.neighbor_list.
to_jax_md returns a short-range neighbor function, a long-range neighbor
function, and an energy function taking
(positions, neighbor=..., neighbor_lr=..., box=...). The energy function
returns the total energy, or with has_aux=True the per-atom energies and the
auxiliary outputs.
Periodic systems¶
For a periodic box, use space.periodic_general and pass the box through. A
jax-md box holds the lattice vectors as columns, so transpose the ASE cell:
from jax_md import space
box = jnp.array(atoms.get_cell().array.T) # lattice vectors as columns
positions = jnp.array(atoms.get_scaled_positions()) # fractional, to match below
displacement, shift = space.periodic_general(
box, fractional_coordinates=True
)
neighbor_fn, neighbor_fn_lr, energy_fn = to_jax_md(
potential=So3lrPotential(model="so3lr-tauri-m"),
displacement_or_metric=displacement,
box_size=box,
species=atomic_numbers,
capacity_multiplier=1.25,
buffer_size_multiplier_sr=1.25,
buffer_size_multiplier_lr=1.25,
minimum_cell_size_multiplier_sr=1.0,
disable_cell_list=False,
fractional_coordinates=True,
)
Every box length must be at least twice lr_cutoff. In a smaller box,
interactions with other periodic images are dropped without an error, so check
it yourself or use the ASE calculator.
For k-space electrostatics, also pass k_grid=... and k_smearing=... to the
energy function. See Long-range electrostatics.
Neighbor lists overflow silently
Neighbor lists are allocated with a fixed capacity. During a simulation you
must check did_buffer_overflow and reallocate:
nbrs = neighbor_fn.update(positions, nbrs)
if nbrs.did_buffer_overflow:
nbrs = neighbor_fn.allocate(positions, box=box)
Otherwise interactions are dropped with no error and the forces are wrong. The CLI handles this for you.
Units and other traps¶
- Units. JAX-MD's metal units have their own time unit. Multiply a
timestep in picoseconds by
units["time"]and a temperature in kelvin byunits["temperature"], withunits = jax_md.units.metal_unit_system(). A rawdt=0.0005runs steps about 98× too short, with no visible error. - Minimizer settings.
jax_md.minimize.fire_descent's defaults are too aggressive for SO3LR. The CLI usesdt_start=0.05,dt_max=0.1,n_min=2. - One dtype throughout. Keep positions, masses and kT in the same precision, or the neighbour-list update inside a compiled loop can fail.
Worked examples¶
The example notebooks contain complete NVT and NPT loops with neighbor-list handling, trajectory writing and checkpointing:
examples/nvt_jaxmd.ipynbexamples/npt_jaxmd.ipynbexamples/production_run.ipynb