Note
Go to the end to download the full example code.
Materials, Matchmakers, and Material Tables#
JaxDEM separates material definitions from force laws:
Materialstores per-material scalar properties.MaterialMatchmakerdefines how to mix two material properties into an effective pair property.MaterialTablestores both scalar properties and precomputed pair tables used by force models.
This guide shows how to create materials, choose a matchmaker, and pass a
material table into System. For how force models
consume these properties, see Force Models.
Creating Materials#
You create materials through
Material.create.
Each registered material type defines its own required fields.
import jax.numpy as jnp
import numpy as np
import jaxdem as jdem
elastic = jdem.Material.create(
"elastic",
density=2500.0,
young=2.0e5,
poisson=0.25,
)
frictional = jdem.Material.create(
"elasticfrict",
density=1200.0,
young=8.0e4,
poisson=0.35,
mu=0.6,
e=1.0,
)
lj = jdem.Material.create(
"lj",
density=1.0,
epsilon=1.5,
)
print("elastic type:", elastic.type_name)
print("frictional type:", frictional.type_name)
print("lj type:", lj.type_name)
elastic type: elastic
frictional type: elasticfrict
lj type: lj
You can inspect available material types in the
Factory registry (the private Material._registry dictionary):
print("Registered materials:", list(jdem.Material._registry.keys()))
Registered materials: ['elastic', 'elasticfrict', 'cundallstrackparams', 'lj']
Creating a Material Table#
from_materials() converts a list of material objects
into a
Structure-of-Arrays representation and computes effective pair properties.
When a material does not define a property, fill supplies the value.
With harmonic mixing, a zero-filled property makes the mixed pair value
zero. Merely storing a property name does not make that interaction active.
harmonic_matcher = jdem.MaterialMatchmaker.create("harmonic")
mat_table = jdem.MaterialTable.from_materials(
[elastic, frictional],
matcher=harmonic_matcher,
fill=0.0,
)
print("Number of materials:", len(mat_table))
print("Stored scalar keys:", sorted(mat_table.props.keys()))
print("Stored pair keys:", sorted(mat_table.pair.keys()))
# Scalar per-material arrays (shape: (M,))
print("young:", mat_table.young)
print("mu (elastic filled with 0.0):", mat_table.mu)
# Effective pair arrays (shape: (M, M))
print("young_eff:\n", mat_table.young_eff)
print("poisson_eff:\n", mat_table.poisson_eff)
print("mu_eff:\n", mat_table.mu_eff)
Number of materials: 2
Stored scalar keys: ['density', 'e', 'mu', 'mu_r', 'poisson', 'young']
Stored pair keys: ['density_eff', 'e_eff', 'mu_eff', 'mu_r_eff', 'poisson_eff', 'young_eff']
young: [200000. 80000.]
mu (elastic filled with 0.0): [0. 0.6]
young_eff:
[[200000. 114285.71428571]
[114285.71428571 80000. ]]
poisson_eff:
[[0.25 0.29166667]
[0.29166667 0.35 ]]
mu_eff:
[[0. 0. ]
[0. 0.6]]
Matchmakers#
A MaterialMatchmaker controls how
the material table computes effective pair properties.
For a positive scalar material property P, the two built-in averages are
Both return P for two identical materials. The harmonic mean is dominated
by the smaller value and becomes zero when either value is zero, which is
why missing properties and their fill values affect mixed interactions.
These are the table’s mixing rules; a force model with its own constitutive
reduction, such as Hertz’s effective modulus, can read the per-material
properties directly instead. Choose a registered mixing rule with
MaterialMatchmaker.create.
linear_matcher = jdem.MaterialMatchmaker.create("linear")
mat_table_linear = jdem.MaterialTable.from_materials(
[elastic, frictional],
matcher=linear_matcher,
)
print("Registered matchmakers:", list(jdem.MaterialMatchmaker._registry.keys()))
print("harmonic young_eff[0,1] =", mat_table.young_eff[0, 1])
print("linear young_eff[0,1] =", mat_table_linear.young_eff[0, 1])
Registered matchmakers: ['harmonic', 'linear']
harmonic young_eff[0,1] = 114285.71428571429
linear young_eff[0,1] = 140000.0
Using Material IDs in a Simulation#
Particles select their material via state.mat_id.
Force models then query pair values from system.mat_table.
Pass the table to State.create too if density should determine mass
and
inertia. Passing it only to System.create does not update existing
masses.
In this 2D demonstration, volume is disk area, so density is interpreted as
mass per area (or a three-dimensional density with unit out-of-plane depth).
Use consistent units; the spring law reads young_eff as a
force/length
stiffness coefficient rather than converting a bulk modulus automatically.
state = jdem.State.create(
pos=jnp.array([[0.0, 0.0], [1.5, 0.0]]),
rad=jnp.array([1.0, 1.0]),
mat_id=jnp.array([0, 1]), # particle 0 uses elastic, particle 1 uses frictional
mat_table=mat_table,
)
system = jdem.System.create(
state.shape,
force_model_type="spring",
mat_table=mat_table,
)
np.testing.assert_allclose(state.mass, mat_table.density[state.mat_id] * state.volume)
print("Masses from density times area:", state.mass)
print(
"Required properties for spring:", system.force_model.required_material_properties
)
print("Effective stiffness used by pair (0,1):", system.mat_table.young_eff[0, 1])
state, system = jdem.System.initialize(state, system)
state, system = system.step(state, system)
print("Resulting forces:\n", state.force)
Masses from density times area: [7853.98163397 3769.91118431]
Required properties for spring: ('young_eff',)
Effective stiffness used by pair (0,1): 114285.71428571429
Resulting forces:
[[-57110.8096169 0. ]
[ 57110.8096169 0. ]]
Default Material Table in System Creation#
If you omit mat_table, create()
builds a default single-material table.
default_system = jdem.System.create(state.shape, force_model_type="spring")
print("Default number of materials:", len(default_system.mat_table))
print("Default matcher:", default_system.mat_table.matcher.type_name)
print("Default scalar keys:", sorted(default_system.mat_table.props.keys()))
Default number of materials: 1
Default matcher: harmonic
Default scalar keys: ['density', 'poisson', 'young']
Total running time of the script: (0 minutes 1.931 seconds)