Tutorial 1. Granular column collapse
In this tutorial, we'll simulate a very simple basic granular column collapse with the modified Cam-Clay. The simulation involves two parts: (1) gravity pack and (2) collapse.
Pre-requisites:
Before starting, please make sure that you have completed the following items:
- Installed the HydraxMPM.
- Comfortable with JAX - basics, and JAX - The Sharp Bits
- Read through an overview of the code structure
- Have Paraview
It is recommended that you follow along with the tutorial. Stuck or in a rush? then see code available in GitHub under /tutorials/t1_granular_column.py.
Learning objectives
By the end of this tutorial you will be able to
- Understand the overall code style of a HydraxMPM script
Step 1: create the project file and folders
Create a project directory containing the driver script and two sub-folders with as follows:
# driver script
/tutorial/t1_granular_column.py
# output of gravity pack
/tutorial/output/t1_pack/
# output of collapse
/tutorial/output/t2_collapse
/tutorial/output/t1_pack/
and /tutorial/output/t2_collapse/
folders, respectively.
Step 2: import modules
Import HydraxMPM and supporting the JAX dependencies.
HydraxMPM is top-level package. It contains all the bits and pieces under the same namespace, e.g., hdx.Particles
or hdx.Gravity
.
Step 3: create points representing initial granular column
Create rectangular column a rectangular column of material points. Particles are spaced evenly given a cell size and the number of particles per cell (in one direction). We pad so that material points do not touch the boundary.
column_width = 0.3 # [m]
column_height = 0.4 # [m]
ppc = 2
cell_size = 0.025 # [m]
sep = cell_size / ppc
padding = 4.5 * sep
# create padded meshgrid of positions
x = jnp.arange(0, column_width + sep, sep) + padding - sep
y = jnp.arange(0, column_height + sep, sep) + padding - sep
xv, yv = jnp.meshgrid(x, y)
position_stack = jnp.array(list(zip(xv.flatten(), yv.flatten())))
Tip
There are several ways of initializing material points. See the how-to initialize material points .
Tip
Matplotlib or Pyvista may be used visualize initial positions. To visualize the particles copy and paste the following (note plt.show works only when GUI is active).
Step 4: create the simulation config
This is a juicy sandwich of all common general simulation parameters.
config = hdx.MPMConfig(
origin=jnp.array([0.0, 0.0]),
end=jnp.array([column_width + padding+2*sep, 0.5]),
project="t1_pack",
ppc=ppc,
cell_size=cell_size,
num_points=len(position_stack),
shapefunction="cubic",
num_steps=60000,
store_every=500,
default_gpu_id=0,
dt=3 * 10**-5, # time step[s]
file=__file__, # store current location of file for output
)
hdx.MPMConfig
is the config dataclass used for all MPM simulations.origin
contains start x and y coordinates of the domain boundaryend
contains end x and y coordinates of the domainproject
is the name of the project - mainly used for output purposesppc
is the number of particles per cell (in one direction). this is only used inhdx.discretize
function (more on that later)cell_size
is background grid cell sizenum_points
is number of material pointsshapefunction
the interpolation type from particle to grid. Two shapefunctions are supported, eitherlinear
orcubic
. Cubic is better in most cases.num_steps
the total iteration countstore_every
output every nth stepdefault_gpu_id
if you are working on a shared GPU workstation, this is the parameter you change to avoid making the other person(s) angry! Runnvidia-smi
, in your terminal to find the id of an empty GPU.dt
is a constant time stepfile=__file__
this records the path of your driver script, which is important to save relative output in the correct folder.
Tip
origin
and end
will be of len 2 for 2D plane strain (x,y), and len 3 for 3D (x,y,z).
why a master config?
This is an easy way to ensure, same size arrays are set throughout all dataclasses.
Lets see the summary of the config
If all went well you should get the following output:~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
project: tutorial1
dim: 2
num_points: 625
num_cells: 1400
num_interactions: 10000
domain origin: (0, 0)
domain end: (1.399999976158142, 0.6000000238418579)
dt: 3.0000000000000004e-05
total time: 12.000000000000002
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Step 4: create the Material
Lets create a material with some initial bulk density and a particle density
# Initial bulk density material parameters
phi_0 = 0.8 # [-] initial solid volume fraction
rho_p = 1400 # [kg/m^3] particle (skeletan density)
rho_0 = rho_p * phi_0 # [kg/m^3] initial bulk density
Now the material dataclass
material = hdx.ModifiedCamClay(
config=config,
nu=0.3,
M=0.7,
R=1,
lam=0.0186,
kap=0.0010,
rho_p=rho_p,
p_t=1.0,
ln_N=jnp.log(1.29),
phi_ref_stack=phi_0 * jnp.ones(config.num_points),
)
Lets break this down:
hdx.ModifiedCamClay
is the Modified Cam Clay material class- We always pass
config
along when creating HydraxMPM dataclasses. - The only non-parameter input is the reference solid volume fraction.
Tip
Materials in HydraxMPM are normally initialize either with a reference pressure or reference solid volume fraction. Given one, we can find the other.
Tip
Material may be used in MPM analysis or single integration analysis. Sometimes its good to do both
Step 5: create the Nodes
and Particles
data classes
We initialize particle stresses to match the known pressure state, given a predefined solid-volume fraction above.
Here we finally make use of JAX vmap, to get the stress tensor.
def get_stress_ref(p_ref):
return -p_ref * jnp.eye(3)
stress_stack = jax.vmap(get_stress_ref)(material.p_ref_stack)
Pass all positions and stresses to the Particles
dataclass.
particles = hdx.Particles(
config=config, position_stack=position_stack, stress_stack=stress_stack
)
The discretize
function determines initial particle volume by dividing the number of particles in a cell by the cell size.
particles, nodes = hdx.discretize(
config=config, particles=particles, nodes=nodes, density_ref=rho_0
)
Step 6: create the Gravity
and Domain
Gravity is slowly ramped up
stop_ramp_step = config.num_steps
increment = jnp.array([0.0, -9.8]) / stop_ramp_step
gravity = hdx.Gravity(config=config, increment=increment, stop_ramp_step=stop_ramp_step)
Creating the outside domain box.
Step 7: create the Solver
The solver determines how the background grid and material points interact.
Tip
We recommend using the hdx.ASFLIP
for granular materials
Step 8: Run gravity pack
print("Start gravity pack")
carry, accumulate = hdx.run_solver_io(
config=config,
solver=solver,
particles=particles,
nodes=nodes,
material_stack=[material],
forces_stack=[gravity, box],
callbacks=(
hdx.io_vtk_callback(
config,
particle_output=(
"stress_stack",
"phi_stack",
),
),
),
)
solver, particles, new_nodes, material_stack, forces_stack = carry
print("Gravity pack done")
Step 9: Modify script to incorporate collapse
config = config.replace(
origin=jnp.array([0.0, 0.0]),
end=jnp.array([1.2, 0.5]),
project="t2_collapse",
)
solver = hdx.USL_ASFLIP(config)
nodes = hdx.Nodes(config)
gravity = hdx.Gravity(config=config, gravity=jnp.array([0.0, -9.8]))
box = hdx.NodeLevelSet(config, mu=0.7)