Download this testcase.

2D Granular column collapse onto a fluid bed

This example simulates the collapse of a granular column onto a fluid bed using the PFEM-DEM approach. It showcases the interaction between granular materials and fluids, capturing the dynamics of the collapse and subsequent fluid-grain interactions. The aim is to visualise the type of free surface wave generated by the impact of the grains on the fluid bed. This test case follows the setup described in: - W. Sarlin, C. Morize, A. Sauret, P. Gondret, Nonlinear regimes of tsunami waves generated by a granular collapse, J. Fluid. Mech. 919 (2021) R6.

# Keywords
# --------
# PFEM, fluid-grains, granular column collapse, 2D
#
# Description
# -----------
# The test demonstrates:
# - How to perform a PFEM-DEM simulation with moving boundaries.
import numpy as np
import sys
import shutil
import os

Gmsh pfem branch

This test requires the alphashapes branch of GMSH to use the Particle Finite Element (PFEM) module. PFEM is a lagrangian method where the mesh is regenerated at each time step, making it suitable for problems with large deformations and free surfaces.

sys.path.insert(0, os.environ["GMSH_PFEM_DIR"])

import gmsh
from migflow import fluid, time_integration, scontact, pfem

Output Directory

Create a clean output directory for simulation results.

outputdir = "output" if len(sys.argv) < 2 else sys.argv[1]
shutil.rmtree(outputdir, ignore_errors=True)
os.makedirs(outputdir)

Geometrical parameters and mesh generation

L = 2.0  # length of the domain
H = 1.0  # height of the domain
H_0_array = np.array([0.39, 0.29, 0.29])  # initial column heights
L_0_array = np.array([0.145, 0.1, 0.05])  # initial column lengths
h_0_array = np.array([0.04, 0.08, 0.25])  # initial fluid heights

H_0 = H_0_array[0]
L_0 = L_0_array[0]
l_0 = L - L_0
h_0 = h_0_array[0]
t_gate = 0.01 * L_0  # gate thickness
y_gate = h_0 + 1e-4  # initial gate position
h_gate = y_gate + 1.2 * H_0  # gate height
u_gate = 1  # m/s; gate upward velocity

Mesh parameters

geo_mesh_size = L / 10
mesh_size = 0.01 * l_0
smax = mesh_size
smin = 0.2 * mesh_size
dmax = 6 * smax
alpha = 1.3

Initial fluid mesh

gmsh.model.add("ModelInit")
gmsh.model.occ.addRectangle(L_0, 0, 0, l_0, h_0)
gmsh.model.occ.synchronize()
gmsh.model.mesh.setSizeCallback(lambda *args: mesh_size)
gmsh.model.mesh.generate(2)
nodeTags, coords, _ = gmsh.model.mesh.getNodes()

Solid domain

gmsh.model.add("ModelGeo")


def set_geo_mesh(y_gate=1e-4):
    name = gmsh.model.getCurrent()
    gmsh.model.setCurrent("ModelGeo")
    dimtag = gmsh.model.getEntities(2)
    gmsh.model.occ.remove(dimtag, True)
    box = gmsh.model.occ.addRectangle(0, 0, 0, L, H)
    step = gmsh.model.occ.addRectangle(0, 0, 0, L_0, h_0)
    gate = gmsh.model.occ.addRectangle(L_0, y_gate, 0, t_gate, h_gate)  # gate
    gmsh.model.occ.cut([(2, box)], [(2, gate), (2, step)])
    gmsh.model.occ.synchronize()
    gmsh.model.mesh.setSizeCallback(lambda *args: geo_mesh_size)
    gmsh.model.mesh.generate(2)
    geoEntities = gmsh.model.getEntities(1)
    gmsh.model.setCurrent(name)
    return geoEntities


geoEntities = set_geo_mesh(y_gate)
gmsh.model.addPhysicalGroup(1, [16], -1, "bottom")
gmsh.model.addPhysicalGroup(1, [15], -1, "right")
gmsh.model.addPhysicalGroup(1, [13], -1, "left")
gmsh.model.addPhysicalGroup(1, [7], -1, "step_top")
gmsh.model.addPhysicalGroup(1, [6], -1, "step_right")
gmsh.model.addPhysicalGroup(1, [9, 10, 11, 12], -1, "gate")

PFEM mesh and size fields

gmsh.model.add("ModelFluid")
alphaDomainTag = gmsh.model.addDiscreteEntity(2, -1, [])
for dim, tag in geoEntities:
    gmsh.model.addDiscreteEntity(dim, tag, [])
alphaBoundaryTag = gmsh.model.addDiscreteEntity(1, -1, [])
gmsh.model.mesh.addNodes(2, alphaDomainTag, nodeTags, coords)
gmsh.model.addPhysicalGroup(1, [16], -1, "bottom")
gmsh.model.addPhysicalGroup(1, [15], -1, "right")
gmsh.model.addPhysicalGroup(1, [13], -1, "left")
gmsh.model.addPhysicalGroup(1, [7], -1, "step_top")
gmsh.model.addPhysicalGroup(1, [6], -1, "step_right")
gmsh.model.addPhysicalGroup(1, [9, 10, 11, 12], -1, "gate")
gmsh.model.addPhysicalGroup(1, [alphaBoundaryTag], -1, "freeSurface")
gmsh.model.addPhysicalGroup(2, [alphaDomainTag], -1, "domain")

Size fields

sizeFieldConstant = gmsh.model.mesh.field.add("Box")
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "VIn", mesh_size)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "VOut", 10 * mesh_size)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "XMin", 0.0)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "XMax", L)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "YMin", 0.0)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "YMax", H)
gmsh.model.mesh.field.setNumber(sizeFieldConstant, "Thickness", 0.001)

sizeFieldDistFS = gmsh.model.mesh.field.add("AlphaShapeDistance")
gmsh.model.mesh.field.setNumber(sizeFieldDistFS, "Tag", alphaBoundaryTag)
gmsh.model.mesh.field.setNumber(sizeFieldDistFS, "SamplingLength", 0.25 * smin)
sizeFieldRefine1 = gmsh.model.mesh.field.add("Threshold")
gmsh.model.mesh.field.setNumber(sizeFieldRefine1, "InField", sizeFieldDistFS)
gmsh.model.mesh.field.setNumber(sizeFieldRefine1, "SizeMin", smin)
gmsh.model.mesh.field.setNumber(sizeFieldRefine1, "SizeMax", smax)
gmsh.model.mesh.field.setNumber(sizeFieldRefine1, "DistMin", 0.0)
gmsh.model.mesh.field.setNumber(sizeFieldRefine1, "DistMax", dmax)

bndEntities = []
sizeFieldBnd = []
for b in bndEntities:
    sf = gmsh.model.mesh.field.add("AlphaShapeDistance")
    gmsh.model.mesh.field.setNumber(sf, "Tag", b)
    gmsh.model.mesh.field.setNumber(sf, "SamplingLength", 0.25 * smin)
    sizeFieldBnd.append(gmsh.model.mesh.field.add("Threshold"))
    gmsh.model.mesh.field.setNumber(sizeFieldBnd[-1], "InField", sf)
    gmsh.model.mesh.field.setNumber(sizeFieldBnd[-1], "SizeMin", smin)
    gmsh.model.mesh.field.setNumber(sizeFieldBnd[-1], "SizeMax", smax)
    gmsh.model.mesh.field.setNumber(sizeFieldBnd[-1], "DistMin", 0.0)
    gmsh.model.mesh.field.setNumber(sizeFieldBnd[-1], "DistMax", dmax)
sizeFieldRefine = gmsh.model.mesh.field.add("Min")
gmsh.model.mesh.field.setNumbers(
    sizeFieldRefine, "FieldsList", [sizeFieldRefine1] + sizeFieldBnd
)
gmsh.model.mesh.computeAlphaShape(
    2,
    alphaDomainTag,
    alphaBoundaryTag,
    "ModelGeo",
    alpha,
    sizeFieldConstant,
    sizeFieldRefine,
    False,
)

Physical Parameters

g = np.array([0.0, -9.81])
rho = 1000
mu = 1e-3
rho_bubble = 1
sigma = 0.00728
use_bubble = False

DEM parameters

volume_corr = 0.8  # volume correction on the grains to account for 2D-3D discrepancies
rhop = 2500
friction = 0.9  # 0.2
friction_wall = 0.9
friction_gate = 0.0
r_particles = 0.0025
n_particles = int(L_0 / (2 * r_particles) * H_0 / (2 * r_particles) * 1.5)

Time parameters

cfl = 0.1
U = 10
U_init = 1.0
t = 0
dt = smin / U * cfl
settle_time = 0.03
tEnd = 1.0 + settle_time
outf = 10
print(f"dt = {dt}, U = {U}")

Fluid problem

f = fluid.FluidProblem(2, g, mu, rho, advection=False)
f.set_wall_boundary("bottom")
f.set_wall_boundary("right")


def gate_velocity(t):
    if t > settle_time:
        return u_gate
    return 0


f.set_wall_boundary("gate", velocity=[0, gate_velocity(t)])
f.set_wall_boundary("left")
f.set_wall_boundary("step_top")
f.set_wall_boundary("step_bottom")
f.set_strong_boundary("bottom", velocity=[0, 0])
f.set_strong_boundary("right", velocity_x=0)
f.set_strong_boundary("left", velocity_x=0)
f.set_strong_boundary("step_top", velocity_y=0)
f.set_strong_boundary("step_right", velocity_x=0)
f.set_open_boundary("freeSurface", pressure=0, viscous_flag=False)

DEM Problem

dem = scontact.ParticleProblem(2)
outer_body = dem.add_body((0, 0), 0, 0)
x0 = np.array([L_0, 0])
x1 = np.array([L, 0])
x2 = np.array([L, H])
x3 = np.array([0, H])
x4 = np.array([0, h_0])
x5 = np.array([L_0, h_0])
dem.add_segment_to_body(x0, x1, outer_body, "Wall", material="Wall")
dem.add_segment_to_body(x1, x2, outer_body, "Wall", material="Wall")
dem.add_segment_to_body(x2, x3, outer_body, "Wall", material="Wall")
dem.add_segment_to_body(x3, x4, outer_body, "Wall", material="Wall")
dem.add_segment_to_body(x4, x5, outer_body, "Wall", material="Wall")
dem.add_segment_to_body(x5, x0, outer_body, "Wall", material="Wall")
dem.add_particle_to_body(x0, 0, outer_body)
dem.add_particle_to_body(x1, 0, outer_body)
dem.add_particle_to_body(x2, 0, outer_body)
dem.add_particle_to_body(x3, 0, outer_body)
dem.add_particle_to_body(x4, 0, outer_body)
dem.add_particle_to_body(x5, 0, outer_body)

gate_body = dem.add_body((0, 0), 0, 0)
x0 = np.array([L_0, y_gate])
x1 = np.array([L_0 + t_gate, y_gate])
x2 = np.array([L_0 + t_gate, h_gate])
x3 = np.array([L_0, h_gate])
dem.add_segment_to_body(x0, x1, gate_body, "Gate", material="Gate")
dem.add_segment_to_body(x1, x2, gate_body, "Gate", material="Gate")
dem.add_segment_to_body(x2, x3, gate_body, "Gate", material="Gate")
dem.add_segment_to_body(x3, x0, gate_body, "Gate", material="Gate")
dem.add_particle_to_body(x0, 0, gate_body)
dem.add_particle_to_body(x1, 0, gate_body)
dem.add_particle_to_body(x2, 0, gate_body)
dem.add_particle_to_body(x3, 0, gate_body)
dem.body_velocity()[gate_body, 1] = u_gate

dem.set_fixed_contact_geometry(0)
dem.set_friction_coefficient(10)

dem.set_friction_coefficient(friction, "Grain", "Grain")
dem.set_friction_coefficient(friction_gate, "Grain", "Gate")
dem.set_friction_coefficient(friction_wall, "Grain", "Wall")

initial grains deposition

ratio = 1.2
diameter_max = 2.0 * r_particles
diameter_min = diameter_max / ratio
np.random.seed(0)
u = np.random.power(1, n_particles)  # probability density function [0,1]
d = (
    diameter_max * diameter_min / (diameter_max + u * (diameter_min - diameter_max))
)  # random particle diameter following the given "u" law
radii = d / 2
boundaries = np.array([[0, H], [0, h_0], [L_0, h_0], [L_0, H]])
xp, radii = scontact.fastdeposit_2d(boundaries, radii, "deposit.svg")
eps = 1e-4 * diameter_max
for x, r in zip(xp, radii):
    if x[1] < h_0 + H_0:
        dem.add_particle(x + eps, r, np.pi * r**2 * rhop, "Grain")

Simulation Loop

Time integration of coupled fluid–particle motion.

i = 0
gmsh.option.setNumber("General.Verbosity", 0)
while t < tEnd:
    print(f"{i:4d}, {t:.6g}/{tEnd:.6g}, {dt:.6g}")
    nodetag, oelemtag, oparamcoord, _ = gmsh.model.mesh.computeAlphaShape(
        2,
        alphaDomainTag,
        alphaBoundaryTag,
        "ModelGeo",
        alpha,
        sizeFieldRefine,
        sizeFieldRefine,
        boundaryTolerance=0.001 * smin,
        usePreviousMesh=True,
    )
    oparamcoord = oparamcoord.reshape((-1, 3))

    ordered_node_tags = pfem.prepareMeshForMigflow(
        i, alphaDomainTag, f, nodetag, oelemtag, oparamcoord
    )
    f.set_particles(
        dem.delassus() * volume_corr,
        dem.volume() * volume_corr,
        dem.position(),
        dem.velocity(),
        dem.omega() * 0,
        dem.contact_forces(),
    )

    pfem.applySurfaceTension(f, sigma, False, g, rho_bubble)

    if i % outf == 0:
        f.write_mig(outputdir, t)
        dem.write_mig(outputdir, t)

    f.implicit_euler(dt)

    if t < settle_time:
        dem.body_velocity()[gate_body, 1] = 0
    else:
        dem.body_velocity()[gate_body, 1] = u_gate

    drag = f.compute_node_force()
    external_forces = drag + g * dem.r() ** 2 * np.pi * rhop * volume_corr
    time_integration._advance_particles(dem, external_forces, dt, 1, 1e-6)

    if y_gate > H / 3:
        dem.body_velocity()[gate_body, 1] = 0
    y_gate += dem.body_velocity()[gate_body, 1] * dt
    geoEntities = set_geo_mesh(y_gate)

    dx = np.zeros((f.coordinates().shape[0], 3))
    dx[:, :2] = f.velocity() * dt
    dx = dx.reshape((-1,))
    gmsh.model.mesh.advectMeshNodes(
        2,
        alphaDomainTag,
        alphaBoundaryTag,
        "ModelGeo",
        ordered_node_tags,
        dx,
        0.01 * smin,
    )

    _, newCoords, _ = gmsh.model.mesh.getNodes(2, alphaDomainTag)

    f.set_coordinates(newCoords)

    t += dt
    i += 1

    max_U = np.max(
        [
            U_init,
            np.max(np.linalg.norm(f.velocity(), axis=1)),
            np.max(np.linalg.norm(dem.velocity(), axis=1)),
        ]
    )
    dt = smin / max_U * cfl

Plot

python3 -m migflow.plot.migplot output --actors fluid particles --fluid-field velocity --show-edges 1