Skip to content

Two-phase flow

MILET implements McKenzie-type two-phase flow — porous melt migration through a compacting viscous matrix — as a switchable extension of the Stokes solve (do_two_phase = true in the [twophase] section). The implementation lives in src/physics/TwoPhase.jl; the design spec is doc/design/two_phase_flow.md. At zero porosity the system reduces to single-phase Stokes, and a regression test enforces this limit.

Governing equations (3-field formulation)

Following Katz (2008) and Rhebergen, Wells & Katz (2014), with incompressible phases and the Boussinesq approximation, the unknowns are the solid velocity us, the fluid (melt) pressure pf, and the compaction pressure pc:

(2ηφε˙(us))+pf+pc=ρ¯g(momentum)us+pcζφ=0(compaction closure)(kφμf(pfρfg))us=0(fluid mass + Darcy)

The mixture density ρ¯=(1φ)ρs+φρf drives buoyancy (twophase_buoyancy, same volume-mean perturbation convention as the single-phase buoyancy_cellfield; body_force_full = true disables the reference subtraction). Melt buoyancy relative to the solid enters the pf equation through the (kφ/μf)ρfg term, so melt percolates upward even in a static matrix. The compaction pressure is the diagnostic pc=ζφus, realized weakly by the closure equation.

Constitutive closures

Built cell-wise from the marker-projected porosity by twophase_cell_fields (src/physics/TwoPhase.jl):

ClosureFormulaFunctionConfig keys
Permeabilitykφ=k0(φ+φminφ0)npermeabilityk0, phi0, n_perm, phi_min
Compaction viscosityζφ=ηφ/(φ+φmin)inv_compaction_viscosityphi_min
Melt-weakened matrix viscosityηφ=ηseαφφmelt_weakened_viscosityalpha_phi
ηs

is the visco-elasto-plastic matrix viscosity from the marker projection (see Rheology); αφ27 follows Mei et al. (2002). The regularization porosity φmin (default 104) keeps both closures finite as φ0.

The compaction length

δ=ζφkφμf

is available as a diagnostic (compaction_length) and is the resolution guide: the grid should resolve δ wherever compaction dynamics matter.

Weak form and discretization

build_twophase_forms assembles the symmetric indefinite bilinear form with the same integration-by-parts conventions as src/physics/Stokes.jl:

a((u,pf,pc),(v,qf,qc))=Ω2ηφε˙(u):ε˙(v)Ω(pf+pc)vΩqfuΩkφμfpfqfΩqcuΩpcqcζφl((v,qf,qc))=Ωρ¯gvΩkφμfρfgqf(+Ωχσold:ε˙(v)+tractions)

The optional χσold term is the visco-elastic stress memory, with the same convention as build_stokes_forms; free-slip penalty and traction boundary conditions are reused from the single-phase path.

The (kφ/μf)pfqf pressure Laplacian removes the pressure nullspace whenever kφ>0.

Spaces (build_twophase_spaces): Q2 velocity (the existing FESpaceBundle velocity space, fe_order_v = 2) and two fresh Q1 scalar spaces for pf and pc (fe_order_p = 1), combined into a 3-field MultiFieldFESpace (X3/Y3). By default pf has no Dirichlet condition (flux-free walls — melt cannot leave the domain); pf_surface0 = true imposes homogeneous Dirichlet pf=0 on the "surface" boundary tag so melt escapes freely at the surface (extraction sink).

Solver: solve_twophase! assembles an AffineFEOperator and solves the monolithic 3-field system with a cached MUMPS factorization (cached_mumps_solve!, src/solvers/Mumps.jl) — the sparsity pattern is constant across steps, so only the numeric factorization is repeated. See Linear solvers.

Driver coupling (src/time_stepping/Driver.jl): when do_two_phase is on, the step loop replaces picard_stokes! with a Picard iteration (n_picard iterations) of project_markers_to_cells!project_porosity_to_cells!solve_twophase!recover_invariants!. The solved pf takes the pressure role downstream (state.ph = pfh), and the per-cell compaction pressure is recovered as cell means by compaction_pressure_cells.

Porosity transport on markers

Porosity lives on the markers (mk.porosity::Vector{Float32}, src/particles/Particles.jl): it advects with the markers, is inherited on reseeding, and is checkpointed (format v3). Markers initialize at φ=0 (or uniformly at phi_init when set). project_porosity_to_cells! produces the arithmetic host-cell mean φc that feeds the closures.

Porosity evolves by operator splitting, after the two-phase solve (update_marker_porosity!):

DφDt=(1φ)us=(1φ)pcζφ

evaluated per marker from the host cell's solved pc/ζφ, then clamped to [0,φmax] (phi_max, default 0.5). The update is pure per-marker arithmetic and threads over markers on the Cartesian fast path.

Melting coupling

With two-phase flow enabled, melting sources porosity instead of deleting melt: in apply_marker_phase_and_melt! (Driver.jl), each marker's melt-fraction change ΔF from apply_melting! is added to its porosity (clamped to [0,φmax]) — melt stays in the pores and percolates via the Darcy solve rather than being removed by threshold extraction. Freezing (ΔF<0) consumes porosity first. The helper melt_to_porosity! (TwoPhase.jl) implements the same rule.

Melt-segregation CFL

The Darcy segregation flux can outrun the solid-velocity CFL. The melt velocity is

uf=uskφμfφ(pfρfg),

and max_melt_segregation_speed estimates max|ufus| cell-wise from the solved pf. The driver then limits the step to

ΔtCFLhminmax|ufus|

with CFL = maxxystep, on top of the usual limits in Time stepping.

The φ = 0 limit (regression gate)

As φ0, kφ becomes tiny and 1/ζφφmin/η becomes negligible: the pf equation degenerates to u=0 and pc0, recovering single-phase Stokes. test/smoke/test_twophase.jl enforces this on the falling-block case: with do_two_phase = true and φ=0, the two-phase velocity field matches single-phase Stokes to a relative difference of 4×105, and the total pressure pf+pc (mean-free — the split between pf and pc is regularization-dependent at φ=0, but their sum is the physical pressure) matches to 104. The test gate is set at 5×103 to absorb conditioning variation of the dimensional system.

Configuration

All keys live in the [twophase] TOML section (defaults from cases/_defaults.toml; loaded into cfg.twophase_* by src/io/TOMLLoader.jl and bundled into the TwoPhaseParams struct):

toml
[twophase]
do_two_phase = false
k0           = 1e-12    # reference permeability at phi0 (m²)
phi0         = 0.01     # reference porosity
n_perm       = 3.0      # permeability exponent
mu_fluid     = 1.0      # melt viscosity (Pa·s)
rho_fluid    = 2800.0   # melt density (kg/m³)
alpha_phi    = 27.0     # matrix melt-weakening exponent (Mei et al. 2002)
phi_min      = 1e-4     # regularization porosity (phi → 0 limit)
phi_max      = 0.5      # marker porosity cap
pf_surface0  = false    # Dirichlet p_f = 0 at the surface (melt escapes)
phi_init     = 0.0      # uniform initial marker porosity

See also Parameter file, Melting, and Markers.