Note
Go to the end to download the full example code.
Geometric-asperity deformable particles: step-by-step pipeline#
This example builds a packing of deformable particles (DPs) whose nodes
are the surface asperities of a geometric-asperity body. Each DP is a
soft body held together by a DeformableParticleModel
(or a plastic variant) that applies the standard DP energies —
area/volume (ec), element-measure (em), edges (el), bending
(eb), and surface tension (gamma).
Pipeline:
create_ga_state()withparticle_type='dp'builds a State where each node carriesmass / nvandunion_volume / nv, and shares abond_idwith every other node in its body.distribute_bodies()places each body’s bounding sphere at the target initial packing fraction and applies a uniform-random per-body rotation (the rotation physically rotates the DP nodes around their centroid — confirmed in the tests).create_dp_container()builds the bonded-force container on the placed state. Surface / interior nodes are auto-detected by a per-body convex-hull test (ga_surface_mask()), so a body with a solid core gets an interior node hooked up via “fan”-style struts; a hollow body has every node on the surface with no extra edges.quasistatic_compress_to_packing_fraction()compresses to the target true-body packing fraction while the DP bonds keep each body self-consistent.Build a Verlet system (with the container as
bonded_force_model) and run a few time steps.
Imports
import jax
import numpy as np
jax.config.update("jax_enable_x64", True) # type: ignore[no-untyped-call]
import jaxdem as jdem
from jaxdem.utils.particle_creation import (
create_ga_state,
create_dp_container,
distribute_bodies,
)
from jaxdem.utils.packing_utils import (
compute_packing_fraction,
quasistatic_compress_to_packing_fraction,
)
Parameters#
6 deformable particles in 3D, 12 surface asperities each. core_type
is ‘hollow’ here — try ‘solid’ to get a central node connected to every
surface node via interior_edges='fan'.
N = 6
nv = 12
dim = 3
particle_radius = 0.5
asperity_radius = 0.1
initial_phi_bb = 0.2
target_phi = 0.14 # true-body phi — DP hulls are less dense than bounding spheres
seed = 0
1) Build the DP state#
DP nodes get equal shares of mass and union volume. bond_id groups
them by body. No rigid-body frame is applied; each node’s pos_c is
its world-frame position.
state = create_ga_state(
N=N,
nv=nv,
dim=dim,
particle_radius=particle_radius,
asperity_radius=asperity_radius,
particle_type="dp",
core_type="hollow",
n_samples=50_000,
seed=seed,
mesh_kwargs={"steps": 1_000},
)
print(f"DP state: N={state.N} nodes, {N} bodies")
DP state: N=72 nodes, 6 bodies
Place + orient bodies
state, box_size = distribute_bodies(
state,
phi=initial_phi_bb,
domain_type="periodic",
seed=seed,
randomize_orientation=True,
)
print(f"placed at bounding-sphere phi={initial_phi_bb}: box = {np.asarray(box_size)}")
placed at bounding-sphere phi=0.2: box = [2.65053448 2.65053448 2.65053448]
3) Build the DP container#
The container is built from the placed state so that its
initial_edge_lengths / initial_bendings / etc. reflect the
actual rest configuration. is_surface is auto-detected via
convex-hull per body; for hollow DPs every node is on the hull.
interior_edges='fan' connects any interior (core) node to every
surface node in its body — it’s a no-op here because there are no
interior nodes, but gets exercised for core_type='solid'.
container = create_dp_container(
state,
em=1.0,
ec=1.0,
eb=0.1, # bending is stiff for small triangles; keep this modest
el=0.3,
gamma=None, # no surface tension in this example
interior_edges="fan",
)
print(f"container: {type(container).__name__}")
print(
f" elements: {container.elements.shape if container.elements is not None else None}"
)
print(f" edges: {container.edges.shape if container.edges is not None else None}")
print(
f" adjacency:{container.element_adjacency.shape if container.element_adjacency is not None else None}"
)
container: DeformableParticleModel
elements: (120, 3)
edges: (180, 2)
adjacency:(180, 2)
4) Compress with FIRE, DP bonds active#
Compression calls scale_to_packing_fraction which translates every
body’s centroid uniformly (internal geometry preserved) and then FIRE
relaxes. Because the DP container is wired into the System, bonded
forces keep each body coherent; contact forces from other bodies can
still locally deform it.
mats = [jdem.Material.create("elastic", young=1.0, poisson=0.5, density=1.0)]
mat_table = jdem.MaterialTable.from_materials(
mats, matcher=jdem.MaterialMatchmaker.create("harmonic")
)
fire_system = jdem.System.create(
state_shape=state.shape,
dt=1e-2,
minimizer=jdem.minimizers.fire,
minimizer_kw={"dt": 1e-2},
domain_type="periodic",
force_model_type="spring",
collider_type="naive",
mat_table=mat_table,
domain_kw={"box_size": box_size},
bonded_force_model=container,
)
phi0 = float(compute_packing_fraction(state, fire_system))
print(f"before compression: true-body phi = {phi0:.4f}")
state, fire_system, final_phi, final_pe = quasistatic_compress_to_packing_fraction(
state,
fire_system,
target_phi=target_phi,
step=5e-3,
max_n_min_steps_per_outer=50_000,
)
print(f"after compression: phi = {float(final_phi):.4f} PE = {float(final_pe):.3e}")
before compression: true-body phi = 0.0162
after compression: phi = 0.1400 PE = 8.952e-17
5) Short Verlet rollout (dynamics)#
dataclasses.replace swaps the FIRE setup for plain Verlet
while keeping everything else — the post-compression box size, the
material table, the collider, and crucially the DP bonded-force
container — so no manual rebuild is needed. Passing "" disables
the rotation integrator: DP nodes carry no rigid-body orientation.
import dataclasses
import jax.numpy as jnp
from jaxdem.integrators import LinearIntegrator, RotationIntegrator
sim_system = dataclasses.replace(
fire_system,
linear_integrator=LinearIntegrator.create("verlet"),
rotation_integrator=RotationIntegrator.create(
""
), # disable rotation: DPs have no rigid-body orientation
dt=jnp.asarray(1e-3, dtype=float),
)
for _ in range(100):
state, sim_system = sim_system.step(state, sim_system)
print("100 Verlet steps completed")
100 Verlet steps completed
Total running time of the script: (0 minutes 17.166 seconds)