Note
Go to the end to download the full example code.
Polydisperse sphere packing in one call#
build_sphere_system() is the sphere
counterpart of build_ga_system().
Give it per-particle radii and a target packing fraction, and it
returns a minimized State plus a ready-to-step System. It first
draws a loose random configuration with
random_sphere_configuration() at initial_phi (default 0.3,
or the target phi if that is lower). It then compresses the
configuration quasistatically to phi with FIRE (time step
fire_dt).
The small, dilute configurations below keep this construction example quick. Minimized here means that residual forces are small; it does not imply a rigid or jammed contact network. Length, mass, and energy use consistent reduced units, with the sphere radii setting the length scale.
The target fraction determines the box volume, rather than the amount of overlap between particles. For radii \(r_i\),
Thus a cubic box has side \(L=(\sum_i v_d(r_i)/\phi)^{1/d}\). The 2D example uses two radii, so each population contributes according to its area, not simply its particle count. Changing from 2D to 3D changes both this volume law and the number of position coordinates.
Construction alternates a small box rescale with force relaxation. For a
change from \(\phi\) to \(\phi'\), all box lengths are multiplied
by \(s=(\phi/\phi')^{1/d}\) while particle radii remain fixed. FIRE then
removes the forces introduced by moving particle centers closer together.
Its fire_dt controls the minimization trajectory; the final dt
controls the subsequent physical integration. They serve different purposes.
import jax
jax.config.update("jax_enable_x64", True) # type: ignore[no-untyped-call]
import numpy as np
from jaxdem.utils.particle_creation import build_sphere_system
from jaxdem.utils.packing_utils import compute_packing_fraction
def summarize(label, state, system, target_phi):
phi = float(compute_packing_fraction(state, system))
box = np.asarray(system.domain.box_size)
np.testing.assert_allclose(phi, target_phi, atol=1e-9)
state, system = system.evaluate_forces(state, system)
max_force = float(np.max(np.linalg.norm(np.asarray(state.force), axis=-1)))
assert np.isfinite(max_force) and max_force < 1e-6
system.check_overflow()
print(f"[{label}] N={state.N} phi={phi:.4f} box={box}")
print(f" maximum residual force = {max_force:.2e}")
Monodisperse 3D sphere packing, periodic, neighbor_list
state, system = build_sphere_system(
particle_radii=[0.1] * 8,
phi=0.20,
dim=3,
dt=1e-3,
collider_type="neighbor_list",
initial_phi=0.15,
compression_step=0.025,
compression_force_tol=1e-8,
max_n_min_steps_per_outer=2_000,
seed=0,
)
summarize("3D monodisperse, periodic, neighbor_list", state, system, 0.20)
[3D monodisperse, periodic, neighbor_list] N=8 phi=0.2000 box=[0.55129349 0.55129349 0.55129349]
maximum residual force = 0.00e+00
Bidisperse 2D disk packing, reflect walls, naive collider
state, system = build_sphere_system(
particle_radii=[0.08] * 6 + [0.12] * 2,
phi=0.30,
dim=2,
dt=1e-3,
domain_type="reflect",
collider_type="naive",
initial_phi=0.25,
compression_step=0.025,
compression_force_tol=1e-8,
max_n_min_steps_per_outer=2_000,
seed=1,
)
summarize("2D bidisperse, reflect, naive", state, system, 0.30)
[2D bidisperse, reflect, naive] N=8 phi=0.3000 box=[0.83887827 0.83887827]
maximum residual force = 0.00e+00
Short Verlet rollout on the 2D system
state, system = system.initialize(state, system)
state, system = system.step(state, system, n=10)
assert np.all(np.isfinite(state.pos))
system.check_overflow()
print("10 Verlet steps on the 2D system completed.")

ParaView rendering of this run’s VTKWriter output.
10 Verlet steps on the 2D system completed.
Total running time of the script: (0 minutes 17.879 seconds)