1. A Poisson problem as a conservation law
\(-\nabla\cdot(\nabla u) = f\)
This equation is already in conservation form: the flux is
grad u, and its divergence balances the source.
AbstractConservativePDE requires the same
decomposition: a diffusive flux A, an optional
advective flux F, a reaction term R and a
source f. On a Cartesian mesh,
TPFADiffusionFlux (two-point flux approximation) is the
simplest consistent approximation of the diffusive flux. The flux
through a face is proportional to the difference between the values
of the two neighbouring cells, divided by the distance between
them.
import jax
import jax.numpy as jnp
from scimba_jax.linear_approximation.finite_volume import (
EllipticFVScheme,
TPFADiffusionFlux,
)
from scimba_jax.linear_approximation.meshes.cartesian_mesh import cartesian_mesh
from scimba_jax.linear_approximation.variables.variables_fv import VariablesFV
from scimba_jax.nonlinear_approximation.model_class.funcparam_matrix import (
ParamMatrixFunction,
)
from scimba_jax.nonlinear_approximation.model_class.funcparam_scalar import (
ParamScalarFunction,
)
from scimba_jax.physical_models.abstract_conservative_pde import (
AbstractConservativePDE,
)
from scimba_jax.physical_models.abstract_physical_conservative_model import (
AbstractPhysicalConservativeModel,
)
class Poisson2D(AbstractConservativePDE):
"""``-div(grad u) = f``, written as a conservation law: div(flux) = source."""
def __init__(self):
super().__init__(dim=2)
def construct_A(self, u): # the diffusion tensor multiplying grad(u)
return ParamMatrixFunction(u.dims, lambda _sp, _x: jnp.eye(2), f_type=u.f_type)
def construct_f(self): # the source, one value per cell
return ParamScalarFunction(
{"x": 2, "mu": 0},
lambda _sp, x: 2.0 * (x[0] * (1.0 - x[0]) + x[1] * (1.0 - x[1])),
f_type="x",
)
def construct_F(self, u):
return None # no first-order (advective) flux here
def construct_R(self, u):
return None # no reaction term here
mesh = cartesian_mesh([32, 32], quad_order=3)
model = AbstractPhysicalConservativeModel.from_pde(
Poisson2D(), dirichlet=lambda x: jnp.zeros(1)
)
# TPFA: flux through a face = (u_right - u_left) / distance, times A.
scheme = EllipticFVScheme(
model,
VariablesFV(mesh, projection="average"),
TPFADiffusionFlux(),
assemble_first_order=False,
assemble_reaction=False,
)
scheme = EllipticFVScheme.solve(scheme)
# RMS error against x(1-x)y(1-y): 3.7e-05 at 32x32 cells.
2. Flux classes
Every finite volume flux is an AbstractFVFlux, which
has up to four methods, one per combination of interior/boundary
face and first-order (advective)/second-order (diffusive) term. A
scheme only calls the methods required by its problem. For
instance, TPFADiffusionFlux above only overrides the
two diffusive methods, since the Poisson problem has no advection.
Conversely, for a hyperbolic conservation law,
RusanovFlux only overrides the two advective methods.
It only requires a bound on the local wave speed to be stable and
monotone.
class AbstractFVFlux(ScimbaPytree):
"""A finite volume flux implements up to four methods on a face."""
def first_order(self, model, u_left, u_right, context):
"""Advective flux density leaving the left cell (a Riemann solver)."""
return jnp.zeros_like(u_left)
def second_order(self, model, u_left, u_right, context):
"""Diffusive flux density leaving the left cell (e.g. two-point flux)."""
return jnp.zeros_like(u_left)
def boundary_first_order(self, model, u_inside, boundary_condition, context):
"""Same as first_order on a boundary face, with context.point instead of u_right."""
return jnp.zeros_like(u_inside)
def boundary_second_order(self, model, u_inside, boundary_condition, context):
"""Diffusive counterpart of boundary_first_order."""
return jnp.zeros_like(u_inside)
# TPFADiffusionFlux only overrides the diffusive methods. RusanovFlux only overrides
# first_order (local Lax-Friedrichs flux, with a bound on the wave speed).
from scimba_jax.linear_approximation.finite_volume.hyperbolic import RusanovFlux
flux = RusanovFlux(wave_speed=lambda _model, u, _context: jnp.abs(u[0]))
# {F(uL)+F(uR)}/2 - alpha/2 (uR - uL), alpha = max wave speed on the face.
3. Time-dependent problems
\(\partial_t u + \partial_x\!\left(\dfrac{u^2}{2}\right) = 0\)
TimeDiscreteFVscheme wraps a FiniteVolumeScheme
in the same way that TimeDiscreteFEscheme wraps an
elliptic FEM problem: the spatial scheme gives the right-hand side,
and a Butcher tableau defines the time stepping. For hyperbolic
problems with shocks, such as the Burgers equation, the usual choice
is an explicit tableau under a CFL condition. Indeed, these problems
are not stiff, so an implicit scheme would not allow a much larger
time step.
from scimba_jax.linear_approximation.finite_volume import (
FiniteVolumeScheme,
TimeDiscreteFVscheme,
)
from scimba_jax.linear_approximation.finite_volume.hyperbolic import RusanovFlux
from scimba_jax.linear_approximation.meshes.cartesian_mesh import cartesian_mesh
from scimba_jax.linear_approximation.variables.variables_fv import VariablesFV
from scimba_jax.nonlinear_approximation.model_class.funcparam_scalar import (
ParamScalarFunction,
)
from scimba_jax.physical_models.abstract_conservative_pde import AbstractConservativePDE
from scimba_jax.physical_models.abstract_physical_conservative_model import (
AbstractPhysicalConservativeModel,
)
from scimba_jax.physical_models.weak_boundary_conditions import Dirichlet
from scimba_jax.time_discrete.butcher_tableau import build_explicit_euler_tableau
# Burgers: du/dt + d(u^2/2)/dx = 0, with a Riemann problem (u_L=1, u_R=-0.25) that
# forms a shock. FiniteVolumeScheme gives the right-hand side of the time scheme.
class BurgersPDE(AbstractConservativePDE):
def __init__(self):
super().__init__(dim=1)
def construct_F(self, u):
return ParamScalarFunction(u.dims, lambda sp, x: 0.5 * u(sp, x) ** 2, f_type=u.f_type)
def construct_A(self, u):
return None
def construct_R(self, u):
return None
model = AbstractPhysicalConservativeModel(BurgersPDE())
model.add_boundary_condition("west", Dirichlet(lambda _x: jnp.array([1.0])))
model.add_boundary_condition("east", Dirichlet(lambda _x: jnp.array([-0.25])))
flux = RusanovFlux(wave_speed=lambda _model, u, _context: jnp.abs(u[0]))
spatial_scheme = FiniteVolumeScheme(
model,
VariablesFV(cartesian_mesh([40], quad_order=2)),
flux,
assemble_second_order=False,
assemble_reaction=False,
assemble_source=False,
)
scheme = TimeDiscreteFVscheme(spatial_scheme, build_explicit_euler_tableau(), dt=0.4 / 40)
initial = scheme.initialize(
lambda x: jnp.where(x[0] < 0.5, jnp.array([1.0]), jnp.array([-0.25]))
)
final, history = scheme.solve(initial, t0=0.0, nt=20)
# Explicit Euler with dt <= CFL h / max wave speed. 40 cells, CFL=0.4, t=0.2:
# the mass is exactly conserved (0.46875), without overshoot.
Since finite volumes do not use basis functions, the Basis functions page does not apply to them: the mesh alone defines the discretization. A nonlinear diffusion coefficient is handled in the same way as for FEM and DG (see Nonlinear solvers).