Source code for pytes.compat.advection

"""
A second-order, limited upwind scheme for the advection of the heat transfer fluid.

Upstream: pybamm-team/PyBaMM#3526 and PR #3979 (upwinding in finite volumes), issue #7 here.
PyBaMM's :func:`pybamm.upwind` is first order, which smears a temperature front moving
through the flow path.

Feature: :class:`LimitedFiniteVolume` discretises :func:`pybamm.upwind` with a MUSCL
reconstruction and the van Albada limiter, on uniform or non-uniform meshes, the default
scheme for the fluid (:func:`fluid_spatial_method`, F-052). The fluid's equations are
unchanged, as the scheme only changes the value of the fluid on each cell face, so energy is
still conserved exactly.

Remove when: PyBaMM's finite volumes offer a limited upwind scheme. Then select it in
:func:`fluid_spatial_method`, checking that the inflow and outflow values are unchanged.
"""

import numpy as np
import pybamm
from scipy.sparse import coo_matrix

#: Boundary values on the flow path from the end cells (constant extrapolation, ignoring
#: boundary conditions), as :func:`pytes.compat.upwind_inlet_value` needs
FLUID_OPTIONS = {
    "extrapolation": {
        "order": {"gradient": "quadratic", "value": "constant"},
        "use bcs": False,
    }
}


[docs] class LimitedFiniteVolume(pybamm.FiniteVolume): """ Finite volumes with a second-order, limited upwind scheme (MUSCL with the van Albada limiter) for :func:`pybamm.upwind`. First-order upwinding takes the value on a face from the cell upstream of it. This scheme adds a limited gradient: on the face ``j + 1/2`` downstream of cell ``j``, ``h_{j+1/2} = h_j + g_j (z_{j+1/2} - z_j)``, with ``g_j`` the van Albada average of the backward and forward gradients, ``a = (h_j - h_{j-1}) / (z_j - z_{j-1})``, ``b = (h_{j+1} - h_j) / (z_{j+1} - z_j)``, ``g_j = (a + b) max(a b, 0) / (a^2 + b^2 + eps^2)``, so it works on non-uniform meshes. The limited gradient is zero at a local extremum, so no new extremum appears; it is continuous, but its derivative jumps at extrema, as any limiter that prevents oscillations must. ``eps`` keeps it defined where both gradients vanish: ``regularisation`` times the variable's scale over the domain length. At the ends the scheme is first order, as PyBaMM's: the inflow face takes the inflow value (the ghost node of :func:`pybamm.upwind`), and the outflow face takes the value in the last cell, so the outflow value is the boundary value with constant extrapolation (see :func:`fluid_spatial_method`). For the gradients, these two values sit on the inflow and outflow faces: the last cell, a first-order donor, holds the value it advects out. A steady linear profile is then reconstructed exactly. Parameters ---------- options : dict, optional Options of :class:`pybamm.FiniteVolume`. regularisation : float, optional Relative size of ``eps`` (default 1e-6). """ def __init__(self, options=None, regularisation=1e-6): super().__init__(options) self.regularisation = regularisation
[docs] def upwind_or_downwind(self, symbol, discretised_symbol, bcs, direction): """ The values on the cell faces, upwinded with a limited gradient (see the class docstring). Only upwinding is implemented. Parameters ---------- symbol : pybamm.Symbol The advected variable. discretised_symbol : pybamm.Symbol The discretised variable. bcs : dict The boundary conditions; a Dirichlet condition on the left gives the inflow. direction : str "upwind". Raises ------ NotImplementedError For downwinding. """ if direction != "upwind": raise NotImplementedError( "LimitedFiniteVolume only implements upwinding, not downwinding" ) # First-order face values: the inflow value, then the value in each cell faces = super().upwind_or_downwind(symbol, discretised_symbol, bcs, direction) submesh = self.mesh[symbol.domain] n = submesh.npts edges, nodes = submesh.edges, submesh.nodes repeats = self._get_auxiliary_domain_repeats(symbol.domains) # Where the values sit: the inflow face, the cell centres, and the outflow face # for the last cell (see the class docstring) positions = np.concatenate([[edges[0]], nodes[:-1], [edges[-1]]]) spacing = np.diff(positions) # Rows 1 to n - 1 are the interior faces, downstream of cells 1 to n - 1 (the # rows of the inflow and outflow faces are zero, so they stay first order) rows = np.arange(1, n) def matrix(columns, values): sub = coo_matrix((values, (rows, columns)), shape=(n + 1, n + 1)).tocsr() return pybamm.Matrix(self._block_diagonal(sub, repeats)) # Backward and forward gradients at each interior face backward = ( matrix(rows, 1 / spacing[rows - 1]) @ faces - matrix(rows - 1, 1 / spacing[rows - 1]) @ faces ) forward = ( matrix(rows + 1, 1 / spacing[rows]) @ faces - matrix(rows, 1 / spacing[rows]) @ faces ) scale = symbol.scale if isinstance(symbol, pybamm.Variable) else 1 scale = scale.evaluate() if isinstance(scale, pybamm.Symbol) else scale eps = self.regularisation * np.abs(scale) / (edges[-1] - edges[0]) gradient = ( (backward + forward) * pybamm.maximum(backward * forward, 0) / (backward**2 + forward**2 + eps**2) ) # Distance from each cell centre to the face downstream of it distance = np.zeros(n + 1) distance[rows] = edges[rows] - nodes[rows - 1] distance = pybamm.Vector(np.tile(distance, repeats)) return faces + distance * gradient
[docs] def fluid_spatial_method(scheme="second order", regularisation=1e-6): """ The spatial method for the fluid's flow path: finite volumes whose boundary values are the values in the end cells (constant extrapolation, ignoring boundary conditions), as :func:`pytes.compat.upwind_inlet_value` needs, so the outlet value is the value advected out of the domain. Parameters ---------- scheme : str, optional "second order" (default), :class:`LimitedFiniteVolume` (F-052); or "first order", PyBaMM's own upwinding, e.g. ``pybamm.Simulation(model, spatial_methods={** model.default_spatial_methods, "flow path": fluid_spatial_method("first order")})``. regularisation : float, optional See :class:`LimitedFiniteVolume`. Returns ------- pybamm.FiniteVolume Raises ------ ValueError If the scheme is not recognised. """ if scheme == "second order": return LimitedFiniteVolume(FLUID_OPTIONS, regularisation=regularisation) if scheme == "first order": return pybamm.FiniteVolume(FLUID_OPTIONS) raise ValueError( f"Scheme '{scheme}' not recognised; it must be 'second order' or 'first order'" )