Download this testcase.

Water–Salt Water Avalanche

This test case simulates the collapse of a dense granular column immersed in a two-fluid environment composed of fresh water and sea water. The grain column is initially surrounded by denser sea water, while the rest of the domain is filled with lighter fresh water.

The case cannot be run standalone: it requires the output of the depot testcase (../depot/depot.py) to initialize a compact granular column. The script can, however, generate a synthetic initial packing using a hexagonal arrangement for testing purposes.

Keywords

FEM, DEM, Unresolved, Friction, Two-Fluids

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

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_two_fluids.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])

Particle Packing Function

Define a helper function to create an initial hexagonal particle arrangement when no pre-deposited configuration is available. The packing density is controlled by the range of particle radii.

use_pre_deposit = False


def hexagonal_packing(p, x0, y0, lx, ly, rmin, rmax):
    """Set all the particles’ center positions using a hexagonal arrangement.

    Parameters
    ----------
    p : migflow.scontact.ParticleProblem
        Particle problem object.
    x0, y0 : float
        Coordinates of the bottom-left corner of the packing region.
    lx, ly : float
        Width and height of the region to fill with particles.
    rmin, rmax : float
        Minimum and maximum particle radii.
    """
    x = np.arange(x0 + rmax, x0 + lx - rmax, 2 * rmax)
    y = np.arange(y0 + rmax, y0 + ly - rmax, 2 * (3**0.5) * rmax)
    x, y = np.meshgrid(x, y)
    x1 = np.arange(2 * rmax + x0, x0 + lx - rmax, 2 * rmax)
    y1 = np.arange(y0 + 3**0.5 * rmax + rmax, y0 + ly - rmax, 2 * (3**0.5) * rmax)
    x1, y1 = np.meshgrid(x1, y1)
    x = np.concatenate([x.reshape(-1), x1.reshape(-1)])
    y = np.concatenate([y.reshape(-1), y1.reshape(-1)])
    order = np.argsort(y)
    x = x[order]
    y = y[order]
    for xi, yi in zip(x, y):
        z = np.random.random((1))
        r = rmin + (rmax - rmin) * z
        p.add_particle((xi, yi), r, r**2 * np.pi * rhop, material="particle")

Physical Parameters

Define the physical properties for the particles, fresh water, and sea water. The density contrast between the two fluids drives buoyancy and stratification.

g = np.array([0, -9.81])  # gravity vector [m/s²]
rhop = 1500  # particle density [kg/m³]
r = 2e-3  # particle radius [m]
# Fresh water (light phase)
rhof = 1000  # density [kg/m³]
nuf = 1e-6  # kinematic viscosity [m²/s]
# Sea water (dense phase)
rhom = 1050  # density [kg/m³]
num = 1e-6  # kinematic viscosity [m²/s]

Particle Problem

Create the particle problem, load boundaries from the mesh, and initialize the grain assembly either from a pre-deposited configuration or by generating a regular packing.

p = scontact.ParticleProblem(2)
p.set_fixed_contact_geometry(0)  # workaround for contact geometry flag
p.load_msh_boundaries(
    mesh_filename, ["Top", "Bottom", "Left", "Right"], material="Wall"
)
if use_pre_deposit:
    p1 = scontact.ParticleProblem(2)
    p1.read_mig("../depot/output", iteration=-1)
    p.add_particles(p1.position(), p1.r(), p1.mass(), v=p1.velocity())
else:
    hexagonal_packing(p, 0, 0, 0.1, 0.199, 0.9 * r, 1.1 * r)
p.set_friction_coefficient(1.0, "Wall", "Particle")
p.set_friction_coefficient(0.5, "Particle", "Particle")

Fluid Problem

Define the fluid problem with two phases (fresh water and sea water). The density and viscosity fields are specified as lists for the two components. The particle information (positions, velocities, contact forces) is passed to the fluid solver for coupling.

f = fluid.FluidProblem(2, g, [nuf * rhof, num * rhom], [rhof, rhom])
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()
)

Initial Concentration Field

The concentration field defines the spatial distribution of sea water (c = 1) and fresh water (c = 0). Initially, the left region is filled with sea water to represent the salt water column surrounding the grains.

x = f.coordinates()
c = np.zeros(f.n_nodes())
c[x[:, 0] < 0.1] = 1
f.set_concentration_cg(c)

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.

t = 0  # initial time
i = 0  # iteration counter
tic = time.time()
outf = 10  # number of iterations between outputs
dt = 1e-3  # time step [s]
tEnd = 2  # final time [s]
mass = np.pi * p.r() ** 2 * rhop  # particle masses
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=10, external_particles_forces=g * mass)
    t += dt
    i += 1

Plot

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