Skip to content

Commit 2c148cd

Browse files
committed
Add Euler-Lagrange solid particles (phase 1)
Lagrangian solid-particle model (particles_lagrange) next to the Lagrangian bubbles. Rigid spheres are tracked individually; the gas acts on them through quasi-steady drag (Gidaspow, Parmar et al., Osnes et al.), optional pressure-gradient and added-mass forces and an optional stochastic drag fluctuation. With two-way coupling the particles act back on the gas through momentum and energy sources projected with a Gaussian kernel. No collisions in phase 1. - Solver: src/simulation/m_particles_EL.fpp and m_particles_EL_kernels.fpp. - Shared module m_euler_lagrange.fpp for bubbles and particles: cell location, domain and cell-volume helpers, input parsing and start-up, void-fraction and evolution files, MPI-IO restart read/write. Bubble results are unchanged. - Coupling: force deposit normalized by sum(G V); the gas receives the particle momentum change m du/dt (including the added-mass reaction) and the work -F.u_p; pressure terms -(alpha_p/alpha_f) grad p and -(alpha_p/alpha_f) div(p u) as in the bubble solver, so a quiescent cloud stays at rest. - Inputs registered in toolchain/mfc/params/definitions.py, validated by check_particles_lagrange (unsupported combinations rejected), documented in docs/documentation/case.md with references in docs/references.bib. - Examples 2D_particle_curtain and 2D_particle_hemisphere with goldens; 12 particle regression tests, including a quiescent cloud and a closed box. - Shared-code changes: halo width and buffers, beta exchange, solid-particle MPI handover, particle gradients inside the RHS direction loop, parallel-I/O volume-fraction slot, post-process naming and rank-count handling.
1 parent c7feed2 commit 2c148cd

67 files changed

Lines changed: 6502 additions & 656 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/documentation/case.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -802,6 +802,7 @@ To restart the simulation from $k$-th time step, see @ref running "Restarting Ca
802802
| `lag_mg_wrt` | Logical | Add the bubble gas mass to the database file |
803803
| `lag_betaT_wrt` | Logical | Add the bubble heat flux model coefficient to the database file |
804804
| `lag_betaC_wrt` | Logical | Add the bubble mass flux model coefficient to the database file |
805+
| `lag_voidfrac_wrt` | Logical | Add the particle volume fraction in the host cell to the database file (`particles_lagrange` only) |
805806

806807
The table lists formatted database output parameters. The parameters define variables that are outputted from simulation and file types and formats of data as well as options for post-processing.
807808

@@ -1084,6 +1085,41 @@ When ``polytropic = 'F'``, the gas compression is modeled as non-polytropic due
10841085

10851086
- `kahan_summation` uses Kahan compensated summation when smearing the bubble contributions onto the Eulerian void fraction, reducing the round-off sensitivity of the accumulation to the summation order. It is not compatible with `--mixed` precision builds.
10861087

1088+
#### 9.3 Euler-Lagrange Solid Particle Model
1089+
1090+
| Parameter | Type | Description |
1091+
| ---: | :---: | :--- |
1092+
| `particles_lagrange` | Logical | Lagrangian solid particle model switch |
1093+
| `particle_pp%%rho0ref_particle` | Real | Particle material density |
1094+
| `particle_params%%nparticles_glb` | Integer | Global number of particles |
1095+
| `particle_params%%input_path` | String | Path to the particle input file |
1096+
| `particle_params%%solver_approach` | Integer | 1: One-way coupling, 2: two-way coupling |
1097+
| `particle_params%%stationary` | Logical | Keep the particles fixed in space (default false) |
1098+
| `particle_params%%qs_force` | Integer | Quasi-steady drag: 0 off, 1 Gidaspow, 2 Parmar, 3 Osnes |
1099+
| `particle_params%%qs_fluct_force` | Logical | Quasi-steady drag fluctuation force |
1100+
| `particle_params%%pressure_gradient_force`| Logical | Pressure-gradient force |
1101+
| `particle_params%%added_mass_force` | Integer | Added-mass force: 0 off, 1 on |
1102+
| `particle_params%%mu_ref(i)` | Real | Reference viscosity of fluid $i$ for the drag (inviscid cases) |
1103+
| `particle_params%%suth(i)` | Real | Sutherland constant of fluid $i$ (optional) |
1104+
| `particle_params%%interpolation_order` | Integer | Even order (2 to 8) of the fluid-to-particle barycentric interpolation |
1105+
| `particle_params%%epsilonb` | Real | Standard deviation scaling for the Gaussian kernel |
1106+
| `particle_params%%charwidth` | Real | Domain virtual depth (z direction, for 2D simulations) |
1107+
| `particle_params%%valmaxvoid` | Real | Maximum particle volume fraction permitted |
1108+
| `particle_params%%write_particles` | Logical | Write the particle evolution to `D/lag_particle_evol_<rank>.dat` |
1109+
| `particle_params%%write_void_evol` | Logical | Write the volume fraction evolution over time |
1110+
1111+
- `particles_lagrange` activates the Euler-Lagrange solid particle model: rigid spherical particles are tracked individually and projected onto the grid with the Gaussian kernel of \cite Maeda18. It requires a 2D or 3D case, `model_eqns = 2` and `fd_order = 2` or `4` (the particle gradient stencil and halo width), and cannot be combined with `bubbles_lagrange`, `igr` or `cyl_coord`. The added-mass force needs two-way coupling (`solver_approach = 2`), which computes the fluid acceleration it uses, and `qs_fluct_force` needs a quasi-steady drag model (`qs_force > 0`). Restarts and particle post-processing output (`lag_db_wrt`) need ``parallel_io = 'T'``, which writes the particle restart file. A restart must use the same number of MPI ranks as the run that wrote the particle restart file (each rank reads the particles that rank wrote, as for Lagrangian bubbles); the simulation aborts otherwise. The restart file keeps each particle's drag-fluctuation state and random seed, so a restarted run continues the same random sequence. Known limitation: the added-mass force uses the fluid acceleration from the previous Runge-Kutta stage, which is not saved, so the first stage after a restart uses zero fluid acceleration. With `added_mass_force = 1` a restarted run therefore differs slightly from an uninterrupted one (about 5e-5 relative in the particle velocities over the next 10 steps of the regression case); without the added-mass force, restarts reproduce the uninterrupted run to round-off. The interpolation stencil must stay within the ghost layers holding valid data, so `interpolation_order` is at most 8, and at most `4 + fd_order` with the pressure-gradient or added-mass force (6 for `fd_order = 2`). Periodic and reflective (symmetry) boundaries and particle collisions are not yet supported.
1112+
1113+
- `input_path` Path to the particle input file. Each row specifies one particle, with columns `x y z u v w radius`; any extra columns are ignored.
1114+
1115+
- `qs_force` selects the quasi-steady drag correlation: Gidaspow \cite Gidaspow1994, Parmar et al. \cite Parmar2010 with the volume-fraction correction of \cite Sangani1991, or Osnes et al. \cite Osnes2023, whose Loth et al. \cite Loth2021 single-particle drag uses the continuous reformulation of \cite Daoud2026. In inviscid cases without chemistry, the drag viscosity of fluid $i$ is `mu_ref(i)`, corrected with Sutherland's law when `suth(i)` is given.
1116+
1117+
- `pressure_gradient_force` and `added_mass_force` use the fluid pressure, density and velocity gradients at the particles, taken from the reconstructed cell-face states. `fd_order` sets the stencil of the gradients in the two-way source terms and the particle halo width.
1118+
1119+
- `qs_fluct_force` adds the stochastic quasi-steady force fluctuations of \cite Osnes2023 and \cite Lattanzi2022, advanced once per time step as an Ornstein-Uhlenbeck process whose relaxation rate uses the radial distribution function of \cite MaAhmadi1986.
1120+
1121+
- The Lagrangian post-processing flags (`lag_db_wrt`, `lag_pos_wrt`, `lag_vel_wrt`, ...) also apply to particles, which are written as the lag_particles point mesh in the Silo output. `lag_voidfrac_wrt` adds the particle volume fraction in each particle's host cell.
1122+
10871123
### 10. Velocity Field Setup {#sec-velocity-field-setup}
10881124

10891125
| Parameter | Type | Description |

‎docs/module_categories.json‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
"m_bubbles_EE",
3030
"m_bubbles_EL",
3131
"m_bubbles_EL_kernels",
32+
"m_particles_EL",
33+
"m_particles_EL_kernels",
34+
"m_euler_lagrange",
3235
"m_qbmm",
3336
"m_hypoelastic",
3437
"m_phase_change",

‎docs/references.bib‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,84 @@ @article{Maeda18
195195
doi = {10.1016/j.jcp.2018.05.029}
196196
}
197197

198+
% --- Lagrangian particle drag ---
199+
200+
@article{Daoud2026,
201+
author = {T. Daoud and T. Jackson and S. Balachandar},
202+
title = {A careful examination of closure models in {Euler--Lagrange} simulations of compressible multiphase flow in a planar shock particle curtain problem},
203+
journal = {International Journal of Multiphase Flow},
204+
pages = {105740},
205+
year = {2026},
206+
doi = {10.1016/j.ijmultiphaseflow.2026.105740}
207+
}
208+
209+
@article{Osnes2023,
210+
author = {A. N. Osnes and M. Vartdal and M. Khalloufi and J. Capecelatro and S. Balachandar},
211+
title = {Comprehensive quasi-steady force correlations for compressible flow through random particle suspensions},
212+
journal = {International Journal of Multiphase Flow},
213+
volume = {165},
214+
pages = {104485},
215+
year = {2023},
216+
doi = {10.1016/j.ijmultiphaseflow.2023.104485}
217+
}
218+
219+
@article{Loth2021,
220+
author = {E. Loth and J. T. Daspit and M. Jeong and T. Nagata and T. Nonomura},
221+
title = {Supersonic and hypersonic drag coefficients for a sphere},
222+
journal = {AIAA Journal},
223+
volume = {59},
224+
number = {8},
225+
pages = {3261--3274},
226+
year = {2021},
227+
doi = {10.2514/1.J060153}
228+
}
229+
230+
@article{Parmar2010,
231+
author = {M. Parmar and A. Haselbacher and S. Balachandar},
232+
title = {Improved drag correlation for spheres and application to shock-tube experiments},
233+
journal = {AIAA Journal},
234+
volume = {48},
235+
number = {6},
236+
pages = {1273--1276},
237+
year = {2010}
238+
}
239+
240+
@article{Sangani1991,
241+
author = {A. S. Sangani and D. Z. Zhang and A. Prosperetti},
242+
title = {The added mass, {Basset}, and viscous drag coefficients in nondilute bubbly liquids undergoing small-amplitude oscillatory motion},
243+
journal = {Physics of Fluids A},
244+
volume = {3},
245+
number = {12},
246+
pages = {2955--2970},
247+
year = {1991}
248+
}
249+
250+
@book{Gidaspow1994,
251+
author = {D. Gidaspow},
252+
title = {Multiphase Flow and Fluidization: Continuum and Kinetic Theory Descriptions},
253+
publisher = {Academic Press},
254+
year = {1994}
255+
}
256+
257+
@article{MaAhmadi1986,
258+
author = {D. Ma and G. Ahmadi},
259+
title = {An equation of state for dense rigid sphere gases},
260+
journal = {Journal of Chemical Physics},
261+
volume = {84},
262+
number = {6},
263+
pages = {3449--3450},
264+
year = {1986}
265+
}
266+
267+
@article{Lattanzi2022,
268+
author = {A. M. Lattanzi and V. Tavanashad and S. Subramaniam and J. Capecelatro},
269+
title = {Stochastic model for the hydrodynamic force in {Euler--Lagrange} simulations of particle-laden flows},
270+
journal = {Physical Review Fluids},
271+
volume = {7},
272+
pages = {014301},
273+
year = {2022}
274+
}
275+
198276
% --- Elasticity ---
199277
200278
@article{Rodriguez19,
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# 2D Shock–Particle Curtain Interaction
2+
3+
A Mach 1.66 planar shock, held by a Dirichlet inflow, hits a 2 mm thick curtain of glass particles (radius 57.5 µm, volume fraction 0.21) in 82.7 kPa air between slip walls. It uses the Euler-Lagrange solid-particle solver (`particles_lagrange`) with two-way coupling, Osnes quasi-steady drag, pressure-gradient force and added mass.
4+
5+
The setup follows the multiphase shock tube experiment of Wagner et al. (2012):
6+
> J. L. Wagner, S. J. Beresh, S. P. Kearney, W. M. Trott, J. N. Castaneda, B. O. Pruett, and M. R. Baer, "A multiphase shock tube for shock wave interactions with dense particle fields", Experiments in Fluids, vol. 52, no. 6, pp. 1507–1517, 2012. https://doi.org/10.1007/s00348-012-1272-x
7+
8+
`gen_particles.py` writes the particles (seeded, uniform in the curtain band) to `input/particles.dat`; `case.py` calls it when pre_process runs, or run it yourself with `python3 gen_particles.py`. In 2D each particle stands for a slab of depth `charwidth`, so projected particles may overlap.
9+
10+
```shell
11+
./mfc.sh run examples/2D_particle_curtain/case.py -n 4
12+
```
13+
14+
## Result
15+
16+
Density-gradient schlieren |∇ρ|/ρ with the particles (white) at t = 500 µs.
17+
18+
<img src="result.png"/>
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
#!/usr/bin/env python3
2+
# Planar shock (driven by a Dirichlet inflow) hitting a dense particle curtain between slip walls
3+
# (Euler-Lagrange, two-way coupling). The particles come from gen_particles.py.
4+
import argparse
5+
import json
6+
import math
7+
import os
8+
9+
import gen_particles
10+
11+
parser = argparse.ArgumentParser(prog="2D_particle_curtain", formatter_class=argparse.ArgumentDefaultsHelpFormatter)
12+
parser.add_argument("--mfc", type=json.loads, default="{}", metavar="DICT", help="MFC's toolchain's internal state.")
13+
args = parser.parse_args()
14+
15+
# Write input/particles.dat only when ./mfc.sh run runs pre_process (validate, test and docs also load this file)
16+
if args.mfc.get("command") == "run" and "pre_process" in args.mfc.get("targets", []):
17+
gen_particles.write_particles(os.path.dirname(os.path.abspath(__file__)))
18+
19+
# Air (ideal gas)
20+
gamma = 1.4
21+
R_air = 287.0 # J/(kg K)
22+
23+
24+
def normal_shock(M, p1, rho1):
25+
"""Pressure, density and lab-frame velocity behind a normal shock of Mach number M moving into still gas (p1, rho1)."""
26+
p2 = p1 * (1.0 + 2.0 * gamma / (gamma + 1.0) * (M**2 - 1.0))
27+
rho2 = rho1 * (gamma + 1.0) * M**2 / ((gamma - 1.0) * M**2 + 2.0)
28+
u2 = 2.0 / (gamma + 1.0) * math.sqrt(gamma * p1 / rho1) * (M - 1.0 / M)
29+
return p2, rho2, u2
30+
31+
32+
# Ambient air (Wagner et al. 2012) and the state behind the incident shock
33+
M_shock = 1.66
34+
p_amb, T_amb = 82700.0, 296.4
35+
rho_amb = p_amb / (R_air * T_amb)
36+
p_post, rho_post, u_post = normal_shock(M_shock, p_amb, rho_amb)
37+
38+
# Grid: x in [-0.2, 0.3], y in [0, 0.015] (slip walls at y = 0 and y = 0.015)
39+
xb, xe, yb, ye = -0.2, 0.3, 0.0, 0.015
40+
Nx, Ny = 2000, 30
41+
dx = (xe - xb) / Nx
42+
43+
print(
44+
json.dumps(
45+
{
46+
# Logistics
47+
"run_time_info": "T",
48+
# Computational domain
49+
"x_domain%beg": xb,
50+
"x_domain%end": xe,
51+
"y_domain%beg": yb,
52+
"y_domain%end": ye,
53+
"m": Nx - 1,
54+
"n": Ny - 1,
55+
"p": 0,
56+
"cfl_adap_dt": "T",
57+
"cfl_target": 0.4,
58+
"n_start": 0,
59+
"t_stop": 5.0e-4,
60+
"t_save": 5.0e-5,
61+
# Simulation algorithm
62+
"model_eqns": 2,
63+
"num_fluids": 1,
64+
"num_patches": 2,
65+
"time_stepper": 3,
66+
"weno_order": 5,
67+
"weno_eps": 1.0e-16,
68+
"mapped_weno": "T",
69+
"mp_weno": "T",
70+
"riemann_solver": 2,
71+
"wave_speeds": 1,
72+
"avg_state": 2,
73+
"bc_x%beg": -17,
74+
"bc_x%end": -8,
75+
"bc_y%beg": -15,
76+
"bc_y%end": -15,
77+
# Output
78+
"format": 1,
79+
"precision": 2,
80+
"prim_vars_wrt": "T",
81+
"parallel_io": "T",
82+
"lag_db_wrt": "T",
83+
"lag_voidfrac_wrt": "T",
84+
# Patch 1: ambient air
85+
"patch_icpp(1)%geometry": 3,
86+
"patch_icpp(1)%x_centroid": 0.5 * (xb + xe),
87+
"patch_icpp(1)%y_centroid": 0.5 * (yb + ye),
88+
"patch_icpp(1)%length_x": xe - xb,
89+
"patch_icpp(1)%length_y": ye - yb,
90+
"patch_icpp(1)%vel(1)": 0.0,
91+
"patch_icpp(1)%vel(2)": 0.0,
92+
"patch_icpp(1)%pres": p_amb,
93+
"patch_icpp(1)%alpha_rho(1)": rho_amb,
94+
"patch_icpp(1)%alpha(1)": 1.0,
95+
# Patch 2: post-shock state on the left (x < -5 mm); the Dirichlet inflow holds it
96+
"patch_icpp(2)%geometry": 3,
97+
"patch_icpp(2)%alter_patch(1)": "T",
98+
"patch_icpp(2)%x_centroid": -0.1025,
99+
"patch_icpp(2)%y_centroid": 0.5 * (yb + ye),
100+
"patch_icpp(2)%length_x": 0.195,
101+
"patch_icpp(2)%length_y": ye - yb,
102+
"patch_icpp(2)%vel(1)": u_post,
103+
"patch_icpp(2)%vel(2)": 0.0,
104+
"patch_icpp(2)%pres": p_post,
105+
"patch_icpp(2)%alpha_rho(1)": rho_post,
106+
"patch_icpp(2)%alpha(1)": 1.0,
107+
# Fluid: air
108+
"fluid_pp(1)%eos": "ideal_gas",
109+
"fluid_pp(1)%gamma": 1.0 / (gamma - 1.0),
110+
"fluid_pp(1)%cv": 717.5,
111+
# Lagrangian particles
112+
"particles_lagrange": "T",
113+
"fd_order": 2,
114+
"particle_pp%rho0ref_particle": 2520.0,
115+
"particle_params%input_path": "input/particles.dat",
116+
"particle_params%nparticles_glb": gen_particles.n_particles,
117+
"particle_params%solver_approach": 2,
118+
"particle_params%qs_force": 3,
119+
"particle_params%pressure_gradient_force": "T",
120+
"particle_params%added_mass_force": 1,
121+
"particle_params%mu_ref(1)": 1.716e-5,
122+
"particle_params%suth(1)": 110.4,
123+
"particle_params%interpolation_order": 2,
124+
"particle_params%epsilonb": 1.0,
125+
"particle_params%valmaxvoid": 0.9,
126+
"particle_params%charwidth": gen_particles.charwidth,
127+
},
128+
indent=4,
129+
)
130+
)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#!/usr/bin/env python3
2+
# Writes <outdir>/input/particles.dat for the particle curtain (default outdir: this example's directory).
3+
# case.py calls it when pre_process runs; the test suite calls it for the Example test.
4+
import math
5+
import os
6+
import random
7+
import sys
8+
9+
# Glass particles, radius 57.5 um, volume fraction 0.21 in the band x in [0, 2 mm] across the channel y in [0, 15 mm].
10+
# In 2D each particle stands for a slab of depth charwidth, so projected particles may overlap (collisions are not modeled).
11+
rp = 57.5e-6
12+
x_curtain = (0.0, 2.0e-3)
13+
y_channel = (0.0, 0.015)
14+
vf = 0.21
15+
charwidth = 2.5e-4 # the grid spacing
16+
n_particles = round(vf * (x_curtain[1] - x_curtain[0]) * (y_channel[1] - y_channel[0]) * charwidth / (4.0 / 3.0 * math.pi * rp**3))
17+
18+
19+
def write_particles(outdir):
20+
"""Uniform random (seeded) positions in the curtain band, one line per particle: x, y, z, u, v, w, radius."""
21+
random.seed(1)
22+
os.makedirs(os.path.join(outdir, "input"), exist_ok=True)
23+
with open(os.path.join(outdir, "input", "particles.dat"), "w") as f:
24+
for _ in range(n_particles):
25+
x = random.uniform(*x_curtain)
26+
y = random.uniform(y_channel[0] + rp, y_channel[1] - rp)
27+
f.write(f"{x:.16e} {y:.16e} 0.0 0.0 0.0 0.0 {rp:.16e}\n")
28+
29+
30+
if __name__ == "__main__":
31+
write_particles(sys.argv[1] if len(sys.argv) > 1 else os.path.dirname(os.path.abspath(__file__)))
136 KB
Loading
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# 2D Blast Wave Through a Particle Half-Ring
2+
3+
A Mach 10 cylindrical blast wave, released from a high-pressure driver at the origin, hits a half-ring of 1612 magnesium particles (radius 50 µm) in air. It uses the Euler-Lagrange solid-particle solver (`particles_lagrange`) with two-way coupling, Osnes quasi-steady drag, pressure-gradient force and added mass.
4+
5+
`gen_particles.py` writes the particles (seeded random placement without overlaps, radius 11.05–12.65 mm) to `input/particles.dat`; `case.py` calls it when pre_process runs, or run it yourself with `python3 gen_particles.py`.
6+
7+
```shell
8+
./mfc.sh run examples/2D_particle_hemisphere/case.py -n 4
9+
```
10+
11+
## Result
12+
13+
Density-gradient schlieren |∇ρ|/ρ with the particles (white) at t = 50 µs.
14+
15+
<img src="result.png"/>

0 commit comments

Comments
 (0)