Download this testcase.

Granular Column Collapse in Fluid

This test case simulates the collapse of a granular column in a viscous fluid. The column of grains can either be imported from a pre-deposited configuration (see ../depot/depot.py) or generated automatically using a regular hexagonal packing. The case demonstrates the coupling between a dense granular phase and a single-phase incompressible fluid.

Keywords

FEM, DEM, Unresolved, Friction

Description

The granular column initially rests in hydrostatic equilibrium inside a rectangular box filled with water. Upon release, the grains collapse and form a dense avalanche driven by gravity and resisted by viscous drag.

import os, sys, shutil, subprocess
import time
import numpy as np
from migflow import fluid, scontact, time_integration

np.random.seed(0)

Output Directory

Create an output directory for results and generate a 2D mesh using Gmsh.

outputdir = "output" if len(sys.argv) < 2 else sys.argv[1]
geomesh_filename = f"mesh_unresolved.geo"
mesh_filename = f"{outputdir}/mesh.msh"
shutil.rmtree(outputdir, ignore_errors=True)
os.makedirs(outputdir)
subprocess.call(["gmsh", "-2", geomesh_filename, "-o", mesh_filename])

Hexagonal Packing Function

Define a helper function to create a regular hexagonal packing of grains when no pre-deposited configuration is available.

use_pre_deposit = False


def hexagonal_packing(p, x0, y0, lx, ly, rmin, rmax):
    """Create a hexagonal particle arrangement inside a rectangular region.

    Parameters
    ----------
    p : migflow.scontact.ParticleProblem
        Particle problem instance.
    x0, y0 : float
        Bottom-left corner of the region.
    lx, ly : float
        Width and height of the region.
    rmin, rmax : float
        Minimum and maximum particle radii.
    """
    for x in np.arange(x0 + rmax, x0 + lx - rmax, 2 * rmax):
        for y in np.arange(y0 + rmax, y0 + ly - rmax, 2 * (3**0.5) * rmax):
            z = np.random.random((1))
            r = rmin + (rmax - rmin) * z
            p.add_particle((x, y), r, np.pi * r**2 * rhop)
    for x in np.arange(2 * rmax + x0, x0 + lx - rmax, 2 * rmax):
        for y in np.arange(
            y0 + (3**0.5) * rmax + rmax, y0 + ly - rmax, 2 * (3**0.5) * rmax
        ):
            z = np.random.random((1))
            r = rmin + (rmax - rmin) * z
            p.add_particle((x, y), r, np.pi * r**2 * rhop)

Physical Parameters

The simulation parameters are defined below, including fluid viscosity, densities, gravity, and the typical particle radius.

g = np.array([0, -9.81])  # gravity [m/s²]
rhop = 1500  # particle density [kg/m³]
rho = 1000  # fluid density [kg/m³]
nu = 1e-6  # fluid kinematic viscosity [m²/s]
mu = rho * nu  # fluid dynamic viscosity [Pa·s]
r = 2e-3  # mean particle radius [m]
h = 0.2  # domain height [m]
lx = 0.1  # granular column width [m]
ly = 0.165  # granular column height [m]

Particle Problem

The particle problem is created and the boundary geometry is loaded. Depending on use_pre_deposit, the initial configuration is either imported from a previous depot testcase or generated on the fly.

p = scontact.ParticleProblem(2)
p.set_fixed_contact_geometry(0)
p.load_msh_boundaries(mesh_filename, ["Top", "Bottom", "Left", "Right"])

if use_pre_deposit:
    # Load compact column from depot testcase
    p1 = scontact.ParticleProblem(2)
    p1.read_mig("../depot/output", 10)
    p.add_particles(p1.position(), p1.r(), p1.mass(), v=p1.velocity())
else:
    # Generate hexagonal packing in the left side of the domain
    eps = 1e-4 * r
    hexagonal_packing(p, eps, eps, lx, ly, 0.9 * r, 1.1 * r)

# Set particle–particle and particle–wall friction
p.set_friction_coefficient(0.5)
p.write_mig(outputdir, 0)

Fluid Problem

The fluid problem is initialized using the same geometry and coupled to the particle phase. All walls are considered no-slip, and the mean pressure is set to zero.

f = fluid.FluidProblem(2, g, mu, rho)
f.load_msh(mesh_filename)
f.set_wall_boundary("Bottom")
f.set_wall_boundary("Left")
f.set_wall_boundary("Right")
f.set_wall_boundary("Top")
f.set_mean_pressure(0)
f.set_particles(
    p.delassus(), p.volume(), p.position(), p.velocity(), p.omega(), p.contact_forces()
)

Time Integration

The simulation runs for a short time interval with a fixed time step. At each iteration, the fluid phase is solved and the fluid-grain forces are applied to the particles, and then system evolves according to the contact dynamics solver. The simulation runs until the avalanche stabilizes.

i = 0
outf = 10  # number of iterations between outputs
dt = 1e-3  # time step [s]
t = 0  # initial time [s]
tEnd = 5  # final time [s]
mass = np.pi * p.r() ** 2 * rhop
tic = time.time()
while t < tEnd:
    print(f"{i = :4d} ----- {t = :g}")
    if i % outf == 0:
        p.write_mig(outputdir, t)
        f.write_mig(outputdir, t)
    time_integration.iterate(
        f, p, dt, min_nsub=1, external_particles_forces=g * mass, contact_tol=1e-4 * r
    )
    t += dt
    i += 1

Plot

python3 -m migflow.plot.migplot output --actors fluid particles --fluid-field pressure