Note
Go to the end to download the full example code.
Integrators and Minimizers#
LinearIntegrator and
RotationIntegrator advance the simulation
state in time. A System holds one of each — one
for translational degrees of freedom (position, velocity) and one for
rotational degrees of freedom (orientation, angular velocity).
JaxDEM also supports energy minimization: instead of integrating the
equations of motion, you can drive the system towards a potential-energy
minimum. Minimizers are not integrator subclasses — they are optax
gradient-transformation constructor functions (such as
jaxdem.minimizers.fire()) passed to
create() via minimizer /
minimizer_kw, as described in the Minimizers section below.
Let’s see how to choose, configure, and swap them.
Linear vs Rotation Integrators#
Every integration step calls both integrators in sequence:
step_before_force()step_before_force()Force evaluation
step_after_force()step_after_force()
This split lets you mix and match: you could pair a Velocity Verlet linear integrator with a SPIRAL rotation integrator, or disable rotation entirely while keeping translation active.
import jax.numpy as jnp
import jaxdem as jdem
state = jdem.State.create(pos=jnp.zeros((1, 3)))
system = jdem.System.create(
state.shape,
linear_integrator_type="verlet",
rotation_integrator_type="verletspiral",
)
print("Linear integrator:", type(system.linear_integrator).__name__)
print("Rotation integrator:", type(system.rotation_integrator).__name__)
Linear integrator: VelocityVerlet
Rotation integrator: VelocityVerletSpiral
Choosing an Integrator#
Integrators are selected by their registered name when calling
create(). Some integrators accept
additional keyword arguments through linear_integrator_kw or
rotation_integrator_kw. Consult the API reference for the specific
parameters of each integrator.
Available Linear Integrators#
The following linear integrators are registered:
# (the empty key ``""`` is a registered no-op; we filter it out)
print("Linear integrators:", sorted(k for k in jdem.LinearIntegrator._registry if k))
Linear integrators: ['euler', 'langevin', 'verlet', 'verletrescaling', 'vicsekextrinsic', 'vicsekintrinsic']
Available Rotation Integrators#
The following rotation integrators are registered:
print(
"Rotation integrators:", sorted(k for k in jdem.RotationIntegrator._registry if k)
)
Rotation integrators: ['spiral', 'verletspiral']
See the API documentation of each class for constructor parameters and algorithmic details.
Deactivating an Integrator#
Passing None as the integrator type (the empty string "" is
equivalent) selects the base no-op integrator, which leaves the
corresponding degrees of freedom untouched. This is useful when you want
to freeze translation or rotation.
# Freeze rotation — only translate:
system = jdem.System.create(
state.shape,
linear_integrator_type="verlet",
rotation_integrator_type=None,
)
print("Rotation integrator (deactivated):", type(system.rotation_integrator).__name__)
Rotation integrator (deactivated): RotationIntegrator
Freeze translation — only rotate:
system = jdem.System.create(
state.shape,
linear_integrator_type=None,
rotation_integrator_type="verletspiral",
)
print("Linear integrator (deactivated):", type(system.linear_integrator).__name__)
Linear integrator (deactivated): LinearIntegrator
Passing Constructor Arguments#
Some integrators take additional parameters. Pass them via
linear_integrator_kw or rotation_integrator_kw:
system = jdem.System.create(
state.shape,
linear_integrator_type="langevin",
rotation_integrator_type=None,
linear_integrator_kw={"gamma": 0.5, "temperature": 0.1, "k_B": 1.0},
)
print("Langevin gamma:", system.linear_integrator.gamma)
Langevin gamma: 0.5
Minimizers#
Minimizers in JaxDEM are standard optax optimizers that descend the potential energy landscape. You configure them by passing the constructor function (such as jaxdem.fire, jaxdem.damped_newtonian, or standard optax optimizers like optax.adam) to minimizer, along with any keyword arguments in minimizer_kw.
Inside System.create, the optimizer is constructed and wrapped in a custom wrapper (CustomGradientTransformation) that keeps track of the constructor function and arguments for serialization.
For checkpoint serialization, JaxDEM saves the import path of the constructor
function (e.g. "jaxdem.minimizers.fire" or "optax.adam") and the dictionary
of keyword parameters. Upon restoration, it resolves and calls the function to
recreate the optimizer.
We therefore recommend defining custom constructor functions and custom
target functions (target_fn) in an importable module (not in
__main__), so that system snapshots can be restored later. This is a
recommendation, not a requirement: minimization itself works exactly the
same with functions defined in your main script or a notebook — only
saving and reloading the system through a checkpoint needs the import
path to be resolvable.
Note
Minimizers and integrators are independent.
The minimizer and the linear/rotation integrators live in separate
fields of the system and never touch each other’s configuration or
internal state: minimize() does not
advance time or use the integrators, and
step() ignores the minimizer. You can
first minimize and then integrate, integrate and then minimize, or
alternate between them in any order without one affecting the other.
Note
Using Composite Optimizers (e.g., `optax.chain`)
If you want to use a composite optimizer like optax.chain, you cannot pass it directly as an instantiated object because the checkpoint writer does not support arbitrary nested object serialization.
Instead, define a simple wrapper function that constructs the chain. As
above, the minimizer works no matter where this function is defined; placing
it in a separate importable module (rather than your main script or
__main__) is only needed so the checkpoint writer can save its import
path and restore the minimizer upon loading.
# In an importable module, e.g., my_optimizers.py
import optax
def my_chained_optimizer(learning_rate=1e-3, max_grad_norm=1.0):
return optax.chain(
optax.clip_by_global_norm(max_grad_norm),
optax.adam(learning_rate)
)
# Then pass it to the system creation:
system = jdem.System.create(
...,
minimizer=my_optimizers.my_chained_optimizer,
minimizer_kw={"learning_rate": 1e-4, "max_grad_norm": 0.5}
)
system = jdem.System.create(
state.shape,
minimizer=jdem.fire,
minimizer_kw={"dt": 1e-2},
)
print("Minimizer:", system.minimizer.type_name)
Minimizer: fire
The minimize() Routine#
JaxDEM provides a convenience method
minimize() that runs a
while_loop until the potential energy converges or a maximum step
count is reached.
state = jdem.State.create(
pos=jnp.array([[0.0, 0.0], [1.5, 0.0]]),
rad=jnp.array([1.0, 1.0]),
)
system = jdem.System.create(
state.shape,
minimizer=jdem.fire,
minimizer_kw={"dt": 1e-2},
)
state, system, steps, pe = system.minimize(
state, system, max_steps=500, pe_tol=1e-12, pe_diff_tol=1e-12
)
print(f"Converged in {steps} steps, PE = {pe:.6e}")
Converged in 1 steps, PE = 0.000000e+00
The Integration Loop#
For reference, the full per-step sequence executed by
step() is:
apply()— enforce boundary conditionsstep_before_force()step_before_force()Collider + force manager — compute forces and torques
step_after_force()step_after_force()
The step_before_force() / step_after_force() split lets multi-stage
schemes (such as Velocity Verlet) position their updates around the force
evaluation correctly.
Fixed Particles#
Particles with state.fixed = True are immobile: the integrator
multiplies velocity updates by (1 - fixed) so their velocity stays
constant. See The Simulation State for how to set this field.
Total running time of the script: (0 minutes 2.265 seconds)