Ejemplo n.º 1
0
def test_subcell_mapping_2d_simplex_1():
    p = np.array([[0, 1, 1, 0], [0, 0, 1, 1]])
    g = simplex.TriangleGrid(p)

    subcell_topology = fvutils.SubcellTopology(g)

    ccum = np.bincount(subcell_topology.cno,
                       weights=np.ones(subcell_topology.cno.size))
    assert np.all(ccum == 6)

    ncum = np.bincount(subcell_topology.nno,
                       weights=np.ones(subcell_topology.nno.size))
    assert ncum[0] == 2
    assert ncum[1] == 4
    assert ncum[2] == 2
    assert ncum[3] == 4

    fcum = np.bincount(subcell_topology.fno,
                       weights=np.ones(subcell_topology.fno.size))
    assert np.sum(fcum == 4) == 1
    assert np.sum(fcum == 2) == 4

    subfcum = np.bincount(subcell_topology.subfno,
                          weights=np.ones(subcell_topology.subfno.size))
    assert np.sum(subfcum == 2) == 2
    assert np.sum(subfcum == 1) == 8
Ejemplo n.º 2
0
def test_subcell_topology_2d_cart_1():
    x = np.ones(2)
    g = structured.CartGrid(x)

    subcell_topology = fvutils.SubcellTopology(g)

    assert np.all(subcell_topology.cno == 0)

    ncum = np.bincount(subcell_topology.nno,
                       weights=np.ones(subcell_topology.nno.size))
    assert np.all(ncum == 2)

    fcum = np.bincount(subcell_topology.fno,
                       weights=np.ones(subcell_topology.fno.size))
    assert np.all(fcum == 2)

    # There is only one cell, thus only unique subfno
    usubfno = np.unique(subcell_topology.subfno)
    assert usubfno.size == subcell_topology.subfno.size

    assert np.all(np.in1d(subcell_topology.subfno, subcell_topology.subhfno))
Ejemplo n.º 3
0
def biot(g, constit, bound, faces=None, eta=0, inverter='numba'):
    """
    Discretization of poro-elasticity by the MPSA-W method.

    Implementation needs (in addition to those mentioned in mpsa function):
        1) Fields for non-zero boundary conditions. Should be simple.
        2) Split return value grad_p into forces and a divergence operator, so
           that we can compute Biot forces on a face.

    Parameters:
        g (core.grids.grid): grid to be discretized
        k (core.constit.second_order_tensor) permeability tensor
        constit (core.bc.bc) class for boundary values
        faces (np.ndarray) faces to be considered. Intended for partial
            discretization, may change in the future
        eta Location of pressure continuity point. Should be 1/3 for simplex
            grids, 0 otherwise. On boundary faces with Dirichlet conditions,
            eta=0 will be enforced.
        inverter (string) Block inverter to be used, either numba (default),
            cython or python. See fvutils.invert_diagonal_blocks for details.

    Returns:
        scipy.sparse.csr_matrix (shape num_faces * dim, num_cells * dim): stres
            discretization, in the form of mapping from cell displacement to
            face stresses.
        scipy.sparse.csr_matrix (shape num_faces * dim, num_faces * dim):
            discretization of boundary conditions. Interpreted as istresses
            induced by the boundary condition (both Dirichlet and Neumann). For
            Neumann, this will be the prescribed stress over the boundary face,
            and possibly stress on faces having nodes on the boundary. For
            Dirichlet, the values will be stresses induced by the prescribed
            displacement.  Incorporation as a right hand side in linear system
            by multiplication with divergence operator.
        scipy.sparse.csr_matrix (shape num_cells * dim, num_cells): Forces from
            the pressure gradient (I*p-term), represented as body forces.
            TODO: Should rather be represented as forces on faces.
        scipy.sparse.csr_matrix (shape num_cells, num_cells * dim): Trace of
            strain matrix, cell-wise.
        scipy.sparse.csr_matrix (shape num_cells x num_cells): Stabilization
            term.

    Example:
        # Set up a Cartesian grid
        g = structured.CartGrid([5, 5])
        c = fourth_order_tensor.FourthOrderTensor(g.dim, np.ones(g.num_cells))
        k = second_order_tensor.SecondOrderTensor(g.dim, np.ones(g.num_cells))

        # Dirirchlet boundary conditions for mechanics
        bound_faces = g.get_boundary_faces().ravel()
        bnd = bc.BoundaryCondition(g, bound_faces, ['dir'] * bound_faces.size)

        # Use no boundary conditions for flow, will default to homogeneous
        # Neumann.

        # Discretization
        stress, bound_stress, grad_p, div_d, stabilization = biot(g, c, bnd)
        flux, bound_flux = mpfa(g, k, None)

        # Source in the middle of the domain
        q_mech = np.zeros(g.num_cells * g.dim)

        # Divergence operator for the grid
        div_mech = fvutils.vector_divergence(g)
        div_flow = fvutils.scalar_divergence(g)
        a_mech = div_mech * stress
        a_flow = div_flow * flux

        a_biot = sps.bmat([[a_mech, grad_p], [div_d, a_flow +
                                                       stabilization]])

        # Zero boundary conditions by default.

        # Injection in the middle of the domain
        rhs = np.zeros(g.num_cells * (g.dim + 1))
        rhs[g.num_cells * g.dim + np.ceil(g.num_cells / 2)] = 1
        x = sps.linalg.spsolve(A, rhs)

        u_x = x[0:g.num_cells * g.dim: g.dim]
        u_y = x[1:g.num_cells * g.dim: g.dim]
        p = x[g.num_cells * gdim:]

    """

    # The grid coordinates are always three-dimensional, even if the grid is
    # really 2D. This means that there is not a 1-1 relation between the number
    # of coordinates of a point / vector and the real dimension. This again
    # violates some assumptions tacitly made in the discretization (in
    # particular that the number of faces of a cell that meets in a vertex
    # equals the grid dimension, and that this can be used to construct an
    # index of local variables in the discretization). These issues should be
    # possible to overcome, but for the moment, we simply force 2D grids to be
    # proper 2D.
    if g.dim == 2:
        g = g.copy()
        g.cell_centers = np.delete(g.cell_centers, (2), axis=0)
        g.face_centers = np.delete(g.face_centers, (2), axis=0)
        g.face_normals = np.delete(g.face_normals, (2), axis=0)
        g.nodes = np.delete(g.nodes, (2), axis=0)

        constit.c = np.delete(constit.c, (2, 5, 6, 7, 8), axis=0)
        constit.c = np.delete(constit.c, (2, 5, 6, 7, 8), axis=1)
    nd = g.dim

    # Define subcell topology
    subcell_topology = fvutils.SubcellTopology(g)
    # Obtain mappings to exclude boundary faces
    bound_exclusion = fvutils.ExcludeBoundaries(subcell_topology, bound, nd)

    num_subhfno = subcell_topology.subhfno.size

    num_nodes = np.diff(g.face_nodes.indptr)
    sgn = g.cell_faces[subcell_topology.fno, subcell_topology.cno].A

    def build_rhs_normals_single_dimension(dim):
        val = g.face_normals[dim, subcell_topology.fno] \
            * sgn / num_nodes[subcell_topology.fno]
        mat = sps.coo_matrix((val.squeeze(), (subcell_topology.subfno,
                                              subcell_topology.cno)),
                             shape=(subcell_topology.num_subfno,
                                    subcell_topology.num_cno))
        return mat

    rhs_normals = build_rhs_normals_single_dimension(0)
    for iter1 in range(1, nd):
        this_dim = build_rhs_normals_single_dimension(iter1)
        rhs_normals = sps.vstack([rhs_normals, this_dim])

    rhs_normals = bound_exclusion.exclude_dirichlet_nd(rhs_normals)

    num_dir_subface = (bound_exclusion.exclude_neu.shape[1] -
                       bound_exclusion.exclude_neu.shape[0]) * nd
    rhs_normals_displ_var = sps.coo_matrix((nd * subcell_topology.num_subfno
                                            - num_dir_subface,
                                            subcell_topology.num_cno))

    # Why minus?
    rhs_normals = -sps.vstack([rhs_normals, rhs_normals_displ_var])
    del rhs_normals_displ_var

    # Call core part of MPSA
    hook, igrad, rhs_cells, \
        cell_node_blocks, hook_normal = __mpsa_elasticity(g, constit,
                                                          subcell_topology,
                                                          bound_exclusion, eta, inverter)

    # Output should be on face-level (not sub-face)
    hf2f = _map_hf_2_f(subcell_topology.fno_unique,
                       subcell_topology.subfno_unique, nd)

    # Stress discretization
    stress = hf2f * hook * igrad * rhs_cells

    # Right hand side for boundary discretization
    rhs_bound = _create_bound_rhs(bound, bound_exclusion, subcell_topology, g)
    # Discretization of boundary values
    bound_stress = hf2f * hook * igrad * rhs_bound

    del hook, rhs_bound

    # Face-wise gradient operator. Used for the term grad_p in Biot's
    # equations.
    rows = __expand_indices_nd(subcell_topology.cno, nd)
    cols = np.arange(num_subhfno * nd)
    vals = np.tile(sgn, (nd, 1)).ravel('F')
    div_gradp = sps.coo_matrix((vals, (rows, cols)),
                               shape=(subcell_topology.num_cno * nd,
                                      num_subhfno * nd)).tocsr()

    del rows, cols, vals

    # Normal vectors, used for computing pressure gradient terms in
    # Biot's equations. These are mappings from cells to their faces,
    # and are most easily computed prior to elimination of subfaces (below)
    # ind_face = np.argsort(np.tile(subcell_topology.subhfno, nd))
    # hook_normal = sps.coo_matrix((np.ones(num_subhfno * nd),
    #                               (np.arange(num_subhfno*nd), ind_face)),
    # shape=(nd*num_subhfno, ind_face.size)).tocsr()

    grad_p = div_gradp * hook_normal * igrad * rhs_normals
    # assert np.allclose(grad_p.sum(axis=0), np.zeros(g.num_cells))

    del hook_normal, div_gradp

    num_cell_nodes = g.num_cell_nodes()
    cell_vol = g.cell_volumes / num_cell_nodes

    if nd == 2:
        trace = np.array([0, 3])
    elif nd == 3:
        trace = np.array([0, 4, 8])
    row, col = np.meshgrid(np.arange(cell_node_blocks.shape[1]), trace)
    incr = np.cumsum(nd**2 * np.ones(cell_node_blocks.shape[1])) - nd**2
    col += incr.astype('int32')
    val = np.tile(cell_vol[cell_node_blocks[0]], (nd, 1))
    vector_2_scalar = sps.coo_matrix((val.ravel('F'),
                                      (row.ravel('F'),
                                       col.ravel('F')))).tocsr()
    del row, col, val
    div_op = sps.coo_matrix((np.ones(cell_node_blocks.shape[1]),
                             (cell_node_blocks[0], np.arange(
                                 cell_node_blocks.shape[1])))).tocsr()
    div = div_op * vector_2_scalar
    del div_op, vector_2_scalar

    div_d = div * igrad * rhs_cells
    del rhs_cells

    stabilization = div * igrad * rhs_normals

    return stress, bound_stress, grad_p, div_d, stabilization
Ejemplo n.º 4
0
def mpsa(g, constit, bound, faces=None, eta=0, inverter='numba'):
    """
    Discretize the vector elliptic equation by the multi-point flux
    approximation method, specifically the weakly symmetric MPSA-W method.

    The method computes stresses over faces in terms of displacments in
    adjacent cells (defined as all cells sharing at least one vertex with the
    face).  This corresponds to the MPSA-W method, see

    Keilegavlen, Nordbotten: Finite volume methods for elasticity with weak
        symmetry, arxiv: 1512.01042

    Implementation needs:
        1) The local linear systems should be scaled with the elastic moduli
        and the local grid size, so that we avoid rounding errors accumulating
        under grid refinement / convergence tests.
        2) It should be possible to do a partial update of the discretization
        stensil (say, if we introduce an internal boundary, or modify the
        permeability field).
        3) For large grids, the current implementation will run into memory
        issues, due to the construction of a block diagonal matrix. This can be
        overcome by splitting the discretization into several partial updates.
        4) It probably makes sense to create a wrapper class to store the
        discretization, interface to linear solvers etc.
    Right now, there are concrete plans for 2) - 4).

    Parameters:
        g (core.grids.grid): grid to be discretized
        k (core.constit.second_order_tensor) permeability tensor
        constit (core.bc.bc) class for boundary values
        faces (np.ndarray) faces to be considered. Intended for partial
            discretization, may change in the future
        eta Location of pressure continuity point. Should be 1/3 for simplex
            grids, 0 otherwise. On boundary faces with Dirichlet conditions,
            eta=0 will be enforced.
        inverter (string) Block inverter to be used, either numba (default),
            cython or python. See fvutils.invert_diagonal_blocks for details.

    Returns:
        scipy.sparse.csr_matrix (shape num_faces, num_cells): stress
            discretization, in the form of mapping from cell displacement to
            face stresses.
            NOTE: The cell displacements are ordered cellwise (first u_x_1,
            u_y_1, u_x_2 etc)
        scipy.sparse.csr_matrix (shape num_faces, num_faces): discretization of
            boundary conditions. Interpreted as istresses induced by the boundary
            condition (both Dirichlet and Neumann). For Neumann, this will be
            the prescribed stress over the boundary face, and possibly stress
            on faces having nodes on the boundary. For Dirichlet, the values
            will be stresses induced by the prescribed displacement.
            Incorporation as a right hand side in linear system by
            multiplication with divergence operator.
            NOTE: The stresses are ordered facewise (first s_x_1, s_y_1 etc)

    Example:
        # Set up a Cartesian grid
        g = structured.CartGrid([5, 5])
        c = fourth_order_tensor.FourthOrderTensor(g.dim, np.ones(g.num_cells))

        # Dirirchlet boundary conditions
        bound_faces = g.get_boundary_faces().ravel()
        bnd = bc.BoundaryCondition(g, bound_faces, ['dir'] * bound_faces.size)

        # Discretization
        stress, bound_stress = mpsa(g, c, bnd)

        # Source in the middle of the domain
        q = np.zeros(g.num_cells * g.dim)
        q[12 * g.dim] = 1

        # Divergence operator for the grid
        div = fvutils.vector_divergence(g)

        # Discretization matrix
        A = div * stress

        # Assign boundary values to all faces on the bounary
        bound_vals = np.zeros(g.num_faces * g.dim)
        bound_vals[bound_faces] = np.arange(bound_faces.size * g.dim)

        # Assemble the right hand side and solve
        rhs = q + div * bound_stress * bound_vals
        x = sps.linalg.spsolve(A, rhs)
        s = stress * x - bound_stress * bound_vals

    """

    """
    Implementation details:

    The displacement is discretized as a linear function on sub-cells (see
    reference paper). In this implementation, the displacement is represented by
    its cell center value and the sub-cell gradients.

    The method will give continuous stresses over the faces, and displacement
    continuity for certain points (controlled by the parameter eta). This can
    be expressed as a linear system on the form

        (i)   A * grad_u            = 0
        (ii)  B * grad_u + C * u_cc = 0
        (iii) 0            D * u_cc = I

    Here, the first equation represents stress continuity, and involves only
    the displacement gradients (grad_u). The second equation gives displacement
    continuity over cell faces, thus B will contain distances between cell
    centers and the face continuity points, while C consists of +- 1 (depending
    on which side the cell is relative to the face normal vector). The third
    equation enforces the displacement to be unity in one cell at a time. Thus
    (i)-(iii) can be inverted to express the displacement gradients as in terms
    of the cell center variables, that is, we can compute the basis functions
    on the sub-cells. Because of the method construction (again see reference
    paper), the basis function of a cell c will be non-zero on all sub-cells
    sharing a vertex with c. Finally, the fluxes as functions of cell center
    values are computed by insertion into Hook's law (which is essentially half
    of A from (i), that is, only consider contribution from one side of the
    face.

    Boundary values can be incorporated with appropriate modifications -
    Neumann conditions will have a non-zero right hand side for (i), while
    Dirichlet gives a right hand side for (ii).

    """

    # The grid coordinates are always three-dimensional, even if the grid is
    # really 2D. This means that there is not a 1-1 relation between the number
    # of coordinates of a point / vector and the real dimension. This again
    # violates some assumptions tacitly made in the discretization (in
    # particular that the number of faces of a cell that meets in a vertex
    # equals the grid dimension, and that this can be used to construct an
    # index of local variables in the discretization). These issues should be
    # possible to overcome, but for the moment, we simply force 2D grids to be
    # proper 2D.
    if g.dim == 2:
        g = g.copy()
        g.cell_centers = np.delete(g.cell_centers, (2), axis=0)
        g.face_centers = np.delete(g.face_centers, (2), axis=0)
        g.face_normals = np.delete(g.face_normals, (2), axis=0)
        g.nodes = np.delete(g.nodes, (2), axis=0)

        # TODO: Need to copy constit here, but first implement a deep copy.
        constit.c = np.delete(constit.c, (2, 5, 6, 7, 8), axis=0)
        constit.c = np.delete(constit.c, (2, 5, 6, 7, 8), axis=1)

    nd = g.dim

    # Define subcell topology
    subcell_topology = fvutils.SubcellTopology(g)
    # Obtain mappings to exclude boundary faces
    bound_exclusion = fvutils.ExcludeBoundaries(subcell_topology, bound, nd)
    # Most of the work is done by submethod for elasticity (which is common for
    # elasticity and poro-elasticity).
    hook, igrad, rhs_cells, _, _ = __mpsa_elasticity(g, constit,
                                                     subcell_topology,
                                                     bound_exclusion, eta,
                                                     inverter)

    hook_igrad = hook * igrad
    del hook, igrad

    # Output should be on face-level (not sub-face)
    hf2f = _map_hf_2_f(subcell_topology.fno_unique,
                       subcell_topology.subfno_unique, nd)

    # Stress discretization
    stress = hf2f * hook_igrad * rhs_cells

    # Right hand side for boundary discretization
    rhs_bound = _create_bound_rhs(bound, bound_exclusion, subcell_topology, g)
    # Discretization of boundary values
    rhs_bound_temp = rhs_bound.copy()
    bound_stress = hf2f * hook_igrad * rhs_bound
    stress, bound_stress = _zero_neu_rows(stress, bound_stress, bound)

    return stress, bound_stress
Ejemplo n.º 5
0
def mpfa(g, k, bnd, faces=None, eta=0, inverter='numba'):
    """
    Discretize the scalar elliptic equation by the multi-point flux
    approximation method.

    The method computes fluxes over faces in terms of pressures in adjacent
    cells (defined as all cells sharing at least one vertex with the face).
    This corresponds to the MPFA-O method, see

    Aavatsmark (2002): An introduction to the MPFA-O method on
            quadrilateral grids, Comp. Geosci. for details.


    Implementation needs:
        1) The local linear systems should be scaled with the permeability and
        the local grid size, so that we avoid rounding errors accumulating
        under grid refinement / convergence tests.
        2) It should be possible to do a partial update of the discretization
        stensil (say, if we introduce an internal boundary, or modify the
        permeability field).
        3) For large grids, the current implementation will run into memory
        issues, due to the construction of a block diagonal matrix. This can be
        overcome by splitting the discretization into several partial updates.
        4) It probably makes sense to create a wrapper class to store the
        discretization, interface to linear solvers etc.
    Right now, there are concrete plans for 2) - 4).

    Parameters:
        g (core.grids.grid): grid to be discretized
        k (core.constit.second_order_tensor) permeability tensor
        bnd (core.bc.bc) class for boundary values
        faces (np.ndarray) faces to be considered. Intended for partial
            discretization, may change in the future
        eta Location of pressure continuity point. Should be 1/3 for simplex
            grids, 0 otherwise. On boundary faces with Dirichlet conditions,
            eta=0 will be enforced.
        inverter (string) Block inverter to be used, either numba (default),
            cython or python. See fvutils.invert_diagonal_blocks for details.

    Returns:
        scipy.sparse.csr_matrix (shape num_faces, num_cells): flux
            discretization, in the form of mapping from cell pressures to face
            fluxes.
        scipy.sparse.csr_matrix (shape num_faces, num_faces): discretization of
            boundary conditions. Interpreted as fluxes induced by the boundary
            condition (both Dirichlet and Neumann). For Neumann, this will be
            the prescribed flux over the boundary face, and possibly fluxes
            over faces having nodes on the boundary. For Dirichlet, the values
            will be fluxes induced by the prescribed pressure. Incorporation as
            a right hand side in linear system by multiplication with
            divergence operator.

    Example:
        # Set up a Cartesian grid
        g = structured.CartGrid([5, 5])
        k = second_order_tensor.SecondOrderTensor(g.dim, np.ones(g.num_cells))

        # Dirirchlet boundary conditions
        bound_faces = g.get_boundary_faces().ravel()
        bnd = bc.BoundaryCondition(g, bound_faces, ['dir'] * bound_faces.size)

        # Discretization
        flux, bound_flux = mpfa(g, k, bnd)

        # Source in the middle of the domain
        q = np.zeros(g.num_cells)
        q[12] = 1

        # Divergence operator for the grid
        div = fvutils.scalar_divergence(g)

        # Discretization matrix
        A = div * flux

        # Assign boundary values to all faces on the bounary
        bound_vals = np.zeros(g.num_faces)
        bound_vals[bound_faces] = np.arange(bound_faces.size)

        # Assemble the right hand side and solve
        rhs = q + div * bound_flux * bound_vals
        x = sps.linalg.spsolve(A, rhs)
        f = flux * x - bound_flux * bound_vals

    """
    """
    Method properties and implementation details.

    The pressure is discretized as a linear function on sub-cells (see
    reference paper). In this implementation, the pressure is represented by
    its cell center value and the sub-cell gradients (this is in contrast to
    most papers, which use auxiliary pressures on the faces; the current
    formulation is equivalent, but somewhat easier to implement).

    The method will give continuous fluxes over the faces, and pressure
    continuity for certain points (controlled by the parameter eta). This can
    be expressed as a linear system on the form

        (i)   A * grad_p            = 0
        (ii)  B * grad_p + C * p_cc = 0
        (iii) 0            D * p_cc = I

    Here, the first equation represents flux continuity, and involves only the
    pressure gradients (grad_p). The second equation gives pressure continuity
    over cell faces, thus B will contain distances between cell centers and the
    face continuity points, while C consists of +- 1 (depending on which side
    the cell is relative to the face normal vector). The third equation
    enforces the pressure to be unity in one cell at a time. Thus (i)-(iii) can
    be inverted to express the pressure gradients as in terms of the cell
    center variables, that is, we can compute the basis functions on the
    sub-cells. Because of the method construction (again see reference paper),
    the basis function of a cell c will be non-zero on all sub-cells sharing
    a vertex with c. Finally, the fluxes as functions of cell center values are
    computed by insertion into Darcy's law (which is essentially half of A from
    (i), that is, only consider contribution from one side of the face.

    Boundary values can be incorporated with appropriate modifications -
    Neumann conditions will have a non-zero right hand side for (i), while
    Dirichlet gives a right hand side for (ii).
    """

    # The grid coordinates are always three-dimensional, even if the grid is
    # really 2D. This means that there is not a 1-1 relation between the number
    # of coordinates of a point / vector and the real dimension. This again
    # violates some assumptions tacitly made in the discretization (in
    # particular that the number of faces of a cell that meets in a vertex
    # equals the grid dimension, and that this can be used to construct an
    # index of local variables in the discretization). These issues should be
    # possible to overcome, but for the moment, we simply force 2D grids to be
    # proper 2D.
    if g.dim == 2:
        # Make a copy before modifying the grid.
        g = g.copy()
        g.cell_centers = np.delete(g.cell_centers, (2), axis=0)
        g.face_centers = np.delete(g.face_centers, (2), axis=0)
        g.face_normals = np.delete(g.face_normals, (2), axis=0)
        g.nodes = np.delete(g.nodes, (2), axis=0)
        # Same treatment for the permeability tensor
        k = k.copy()
        k.perm = np.delete(k.perm, (2), axis=0)
        k.perm = np.delete(k.perm, (2), axis=1)

    # Define subcell topology, that is, the local numbering of faces, subfaces,
    # sub-cells and nodes. This numbering is used throughout the
    # discretization.
    subcell_topology = fvutils.SubcellTopology(g)

    # Obtain normal_vector * k, pairings of cells and nodes (which together
    # uniquely define sub-cells, and thus index for gradients.
    nk_grad, cell_node_blocks, \
        sub_cell_index = _tensor_vector_prod(g, k, subcell_topology)

    # Distance from cell centers to face centers, this will be the
    # contribution from gradient unknown to equations for pressure continuity
    pr_cont_grad = fvutils.compute_dist_face_cell(g, subcell_topology, eta)

    # Darcy's law
    darcy = -nk_grad[subcell_topology.unique_subfno]

    # Pair fluxes over subfaces, that is, enforce conservation
    nk_grad = subcell_topology.pair_over_subfaces(nk_grad)

    # Contribution from cell center potentials to local systems
    # For pressure continuity, +-1 (Depending on whether the cell is on the
    # positive or negative side of the face.
    # The .A suffix is necessary to get a numpy array, instead of a scipy
    # matrix.
    sgn = g.cell_faces[subcell_topology.fno, subcell_topology.cno].A
    pr_cont_cell = sps.coo_matrix(
        (sgn[0], (subcell_topology.subfno, subcell_topology.cno))).tocsr()
    # The cell centers give zero contribution to flux continuity
    nk_cell = sps.coo_matrix(
        (np.zeros(1), (np.zeros(1), np.zeros(1))),
        shape=(subcell_topology.num_subfno, subcell_topology.num_cno)).tocsr()
    del sgn

    # Mapping from sub-faces to faces
    hf2f = sps.coo_matrix(
        (np.ones(subcell_topology.unique_subfno.size),
         (subcell_topology.fno_unique, subcell_topology.subfno_unique)))

    # Update signs
    sgn_unique = g.cell_faces[subcell_topology.fno_unique,
                              subcell_topology.cno_unique].A.ravel('F')

    # The boundary faces will have either a Dirichlet or Neumann condition, but
    # not both (Robin is not implemented).
    # Obtain mappings to exclude boundary faces.
    bound_exclusion = fvutils.ExcludeBoundaries(subcell_topology, bnd, g.dim)

    # No flux conditions for Dirichlet boundary faces
    nk_grad = bound_exclusion.exclude_dirichlet(nk_grad)
    nk_cell = bound_exclusion.exclude_dirichlet(nk_cell)
    # No pressure condition for Neumann boundary faces
    pr_cont_grad = bound_exclusion.exclude_neumann(pr_cont_grad)
    pr_cont_cell = bound_exclusion.exclude_neumann(pr_cont_cell)

    # So far, the local numbering has been based on the numbering scheme
    # implemented in SubcellTopology (which treats one cell at a time). For
    # efficient inversion (below), it is desirable to get the system over to a
    # block-diagonal structure, with one block centered around each vertex.
    # Obtain the necessary mappings.
    rows2blk_diag, cols2blk_diag, size_of_blocks = _block_diagonal_structure(
        sub_cell_index, cell_node_blocks, subcell_topology.nno_unique,
        bound_exclusion)

    del cell_node_blocks, sub_cell_index

    # System of equations for the subcell gradient variables. On block diagonal
    # form.
    grad_eqs = sps.vstack([nk_grad, pr_cont_grad])

    num_nk_cell = nk_cell.shape[0]
    num_pr_cont_grad = pr_cont_grad.shape[0]
    del nk_grad, pr_cont_grad

    grad = rows2blk_diag * grad_eqs * cols2blk_diag

    del grad_eqs
    darcy_igrad = darcy * cols2blk_diag * fvutils.invert_diagonal_blocks(grad,
                                                                         size_of_blocks,
                                                                         method=inverter) \
        * rows2blk_diag

    del grad, cols2blk_diag, rows2blk_diag, darcy

    flux = hf2f * darcy_igrad * (-sps.vstack([nk_cell, pr_cont_cell]))

    del nk_cell, pr_cont_cell
    ####
    # Boundary conditions
    rhs_bound = _create_bound_rhs(bnd, bound_exclusion, subcell_topology,
                                  sgn_unique, g, num_nk_cell, num_pr_cont_grad)
    # Discretization of boundary values
    bound_flux = hf2f * darcy_igrad * rhs_bound

    return flux, bound_flux