Static condensation of HDG

I am solving a Poisson problem using hybridized discontinuous Galerkin and want to perform static condensation of the local solves DoFs.

The main problem is

-\nabla \cdot \nabla u =0

with u_{\Gamma_{left}}=0, u_{\Gamma_{right}}=1, and \nabla u \cdot \pmb{n}|_{\partial \Omega \setminus (\Gamma_{left}\bigcup\Gamma_{right}})=0.

Where \pmb{x} are the local solutions and \pmb{y} is the global solution, I’m solving:

\begin{bmatrix} \pmb{A} & \pmb{B}\\ \pmb{C} & \pmb{D} \end{bmatrix} \begin{pmatrix} \pmb{x}\\ \pmb{y} \end{pmatrix} = \begin{pmatrix} \pmb{0}\\ \pmb{f_y} \end{pmatrix}

and want to have

\pmb{K_y} = \pmb{D} - \pmb{C}\pmb{A}^{-1}\pmb{B}
\pmb{b_y} = \pmb{f_y}

so that I can solve \pmb{K_y}\pmb{y}=\pmb{b_y}.

In the below MWE, I’m unclear what goes into dolfinx.fem.form_cpp_class especially since I’m working with submeshes. As a result, I’m getting an error that

Traceback (most recent call last):
  File "/home/lmolel/miniforge3/envs/fenicsx-env/lib/python3.13/site-packages/numba/np/linalg.py", line 841, in _check_finite_matrix
    raise np.linalg.LinAlgError(
numpy.linalg.LinAlgError: Array must not contain infs or NaNs.
inf

Here’s the MWE

#!/usr/bin/env python3
import argparse
import json
import os
import shutil
import sys
import time

sys.path.append("../")

import gmsh
import matplotlib.pyplot as plt
import matspy
import numpy as np
import scifem
import scipy
import ufl

from dolfinx import cpp, fem, io, log, mesh
from dolfinx import default_real_type, default_scalar_type

from dolfinx.graph import partitioner_scotch
from dolfinx.jit import ffcx_jit

import dolfinx.fem.petsc as petsc
import numba.core.typing.cffi_utils as cffi_support

from mpi4py import MPI
from petsc4py import PETSc
from petsc4py.PETSc import ScalarType
from ufl import div, dot, grad, inner
import cffi
import numba
from ffcx.codegeneration.utils import empty_void_pointer
from ffcx.codegeneration.utils import numba_ufcx_kernel_signature as ufcx_signature


Print = PETSc.Sys.Print
dtype = default_scalar_type
rtype = default_real_type

_type_to_offset_index = {fem.IntegralType.cell: 0, fem.IntegralType.exterior_facet: 1}

ffi = cffi.FFI()


left = 1
right = 2


def compute_cell_boundary_facets(msh):
    """Compute the integration entities for integrals around the
    boundaries of all cells in msh.

    Parameters:
        msh: The mesh.

    Returns:
        Facets to integrate over, identified by ``(cell, local facet
        index)`` pairs.
    """
    tdim = msh.topology.dim
    fdim = tdim - 1
    n_f = cpp.mesh.cell_num_entities(msh.topology.cell_type, fdim)
    n_c = msh.topology.index_map(tdim).size_local
    return np.vstack((np.repeat(np.arange(n_c), n_f), np.tile(np.arange(n_f), n_c))).T.flatten()


def create_mesh(LX, LY):
    gmsh.initialize()
    gmsh.model.add('ssb')
    # gmsh.option.setNumber("Mesh.CharacteristicLengthMax", 0.01)

    points = [
        (0, 0, 0),
        (LX, 0, 0),
        (LX, LY, 0),
        (0, LY, 0),
        ]
    gpoints = [gmsh.model.occ.addPoint(*p) for p in points]
    lines = [gmsh.model.occ.addLine(gpoints[i], gpoints[i+1]) for i in range(-1, len(gpoints)-1)]

    curve_loop = gmsh.model.occ.addCurveLoop(lines)
    surf = gmsh.model.occ.addPlaneSurface([curve_loop])
    gmsh.model.occ.synchronize()
    gmsh.model.addPhysicalGroup(1, [1], left, "Left")
    gmsh.model.addPhysicalGroup(1, [4], right, "Right")

    gmsh.model.addPhysicalGroup(2, [surf], 1, "domain")
    gmsh.model.occ.synchronize()
    gmsh.model.mesh.generate(2)
    gmsh.write("mesh.msh")
    gmsh.finalize()

if __name__ == '__main__':
    comm = MPI.COMM_WORLD
    if comm.rank == 0:
        create_mesh(1, 1)
    partitioner = mesh.create_cell_partitioner(partitioner_scotch(), mesh.GhostMode.shared_facet)
    domain, ct, ft = io.gmsh.read_from_msh("mesh.msh", comm, partitioner=partitioner)[:3]
    tdim = domain.topology.dim
    fdim = tdim - 1
    domain.topology.create_entities(fdim)
    domain.topology.create_connectivity(tdim, fdim)
    domain.topology.create_connectivity(fdim, tdim)
    domain.topology.create_connectivity(fdim, fdim)
    ct_imap = domain.topology.index_map(tdim)
    num_entities_local = ct_imap.size_local + ct_imap.num_ghosts
    # tag internal facets as 0
    ft_imap = domain.topology.index_map(fdim)
    num_facets_local = ft_imap.size_local + ft_imap.num_ghosts

    # facets
    num_facets_local = (
        domain.topology.index_map(fdim).size_local + domain.topology.index_map(fdim).num_ghosts
    )
    facets = np.arange(num_facets_local, dtype=np.int32)
    values = np.full_like(facets, 0, dtype=np.int32)
    values[ft.find(left)] = left
    values[ft.find(right)] = right

    ft = mesh.meshtags(domain, fdim, facets, values)

    domain.topology.create_connectivity(fdim, tdim)
    f_to_c = domain.topology.connectivity(fdim, tdim)

    x = ufl.SpatialCoordinate(domain)
    dx = ufl.Measure('dx', domain=domain, subdomain_data=ct)

    # facet mesh
    facet_mesh, facet_mesh_emap = mesh.create_submesh(domain, fdim, ft.indices)[:2]
    entity_maps = [facet_mesh_emap]
    k = 1
    V = fem.functionspace(domain, ("DG", k))
    Vbar = fem.functionspace(facet_mesh, ("DG", k))

    W = ufl.MixedFunctionSpace(V, Vbar)
    u, ubar = ufl.TrialFunctions(W)
    v, vbar = ufl.TestFunctions(W)

    h = ufl.CellDiameter(domain)
    n = ufl.FacetNormal(domain)

    gamma = 16 * k ** 2 / h

    cell_boundaries = 1  # A tag

    cell_boundary_facets = compute_cell_boundary_facets(domain)

    # Create the measure
    ds_c = ufl.Measure("ds", subdomain_data=[(cell_boundaries, cell_boundary_facets)], domain=domain)

    f_to_c = domain.topology.connectivity(fdim, tdim)
    c_to_f = domain.topology.connectivity(tdim, fdim)

    a = (
    ufl.inner(ufl.grad(u), ufl.grad(v)) * dx
    - ufl.inner((u - ubar), ufl.dot(ufl.grad(v), n)) * ds_c(cell_boundaries)
    - ufl.inner(ufl.dot(ufl.grad(u), n), (v - vbar)) * ds_c(cell_boundaries)
    + gamma * ufl.inner((u - ubar), v - vbar) * ds_c(cell_boundaries)
    )
    a_blocks = ufl.extract_blocks(a)
    a00 = a_blocks[0][0]
    a01 = a_blocks[0][1]
    a10 = a_blocks[1][0]
    a11 = a_blocks[1][1]


    facet_mesh.topology.create_connectivity(fdim, fdim)
    left_boundary_facets = facet_mesh_emap.sub_topology_to_topology(
        ft.find(left), inverse=True
    )
    right_boundary_facets = facet_mesh_emap.sub_topology_to_topology(
        ft.find(right), inverse=True
    )

    # Get the dofs and apply the boundary condition
    facet_mesh.topology.create_connectivity(fdim, fdim)
    left_dofs = fem.locate_dofs_topological(Vbar, fdim, left_boundary_facets)
    right_dofs = fem.locate_dofs_topological(Vbar, fdim, right_boundary_facets)
    left_bc = fem.dirichletbc(dtype(0.0), left_dofs, Vbar)
    right_bc = fem.dirichletbc(dtype(1.0), right_dofs, Vbar)
    bcs = [right_bc]
    ufcx00, _, _ = ffcx_jit(comm, a00, form_compiler_options={"scalar_type": dtype})
    kernel00 = getattr(ufcx00.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

    ufcx01, _, _ = ffcx_jit(comm, a01, form_compiler_options={"scalar_type": dtype})
    kernel01 = getattr(ufcx01.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

    ufcx10, _, _ = ffcx_jit(comm, a10, form_compiler_options={"scalar_type": dtype})
    kernel10 = getattr(ufcx10.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

    ufcx11, _, _ = ffcx_jit(comm, a11, form_compiler_options={"scalar_type": dtype})
    kernel11 = getattr(ufcx11.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

    Vsize = V.element.space_dimension
    Vbarsize = Vbar.element.space_dimension

    @numba.cfunc(ufcx_signature(dtype, rtype), nopython=True)
    def tabulate_A(A_, w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
        """Element kernel that applies static condensation."""

        # Prepare target condensed local element tensor
        A = numba.carray(A_, (Vbarsize, Vbarsize), dtype=dtype)

        # Tabulate all sub blocks locally
        A00 = np.zeros((Vsize, Vsize), dtype=dtype)
        kernel00(
            ffi.from_buffer(A00),
            w_,
            c_,
            coords_,
            entity_local_index,
            permutation,
            empty_void_pointer(),
        )

        A01 = np.zeros((Vsize, Vbarsize), dtype=dtype)
        kernel01(
            ffi.from_buffer(A01),
            w_,
            c_,
            coords_,
            entity_local_index,
            permutation,
            empty_void_pointer(),
        )

        A10 = np.zeros((Vbarsize, Vsize), dtype=dtype)
        kernel10(
            ffi.from_buffer(A10),
            w_,
            c_,
            coords_,
            entity_local_index,
            permutation,
            empty_void_pointer(),
        )

        A11 = np.zeros((Vbarsize, Vbarsize), dtype=dtype)
        kernel11(
            ffi.from_buffer(A11),
            w_,
            c_,
            coords_,
            entity_local_index,
            permutation,
            empty_void_pointer(),
        )

        # A = A11 - A10 * A00^{-1} * A01
        A[:, :] = A11 - A10 @ np.linalg.solve(A00, A01)

    formtype = fem.form_cpp_class(dtype)
    facets = np.arange(domain.topology.index_map(fdim).size_local)
    cells = np.arange(domain.topology.index_map(domain.topology.dim).size_local)
    integrals = {fem.IntegralType.exterior_facet: [(0, tabulate_A.address, facets, np.array([], dtype=np.int8))]}
    a_cond = fem.Form(
        formtype([Vbar._cpp_object, Vbar._cpp_object], integrals, [], [], False, [], mesh=facet_mesh._cpp_object)
    )
    A_cond = fem.petsc.assemble_matrix(a_cond, bcs=bcs)
    A_cond.assemble()
    Print(A_cond)
    b = fem.petsc.create_vector([Vbar])
    fem.petsc.apply_lifting(b, [a_cond], bcs=[[right_bc]])
    b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
    right_bc.set(b)
    uc = fem.Function(Vbar, name="from_condensation")
    solver = PETSc.KSP().create(A_cond.getComm())
    solver.setType("gmres")
    solver.getPC().setType("lu")
    solver.setOperators(A_cond)
    solver.solve(b, uc.x.petsc_vec)
    Print(uc.x.petsc_vec.norm(0))
    solver.destroy()

Hi,

I was also trying to do elementwise static condensation for HDG. I leave here my attempt.

I am based on the examples demo_hdg.py, demo_static-condensation.py and the repository of jpdean dolfinx_hdg (from 3 years ago) dolfinx_hdg/python/demo/hdg_poisson.py at navier_stokes · jpdean/dolfinx_hdg · GitHub

I have not adapted the codes from jpdean to the v0.10.0, and I was merely trying to see if the new version was capable of performing elementwise static condensation, is it the case?

For the compilation of the form, I provide IntegralType.cell and it is inside each tabulation that there is a loop for the facets.

However, the matrix is not assembled correctly. Doing A_cond.view() it shows that it only assembles part of the matrix, and the remaining is not right either (the sparsity of the matrix is wrong).

# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.13.6
# ---

# # HDG scheme for the Poisson equation
#
# ```{admonition} Download sources
# :class: download
# * {download}`Python script <./demo_hdg.py>`
# * {download}`Jupyter notebook <./demo_hdg.ipynb>`
# ```
# This demo illustrates how to:
#
# - Solve Poisson's equation using an HDG scheme.
# - Defining custom integration domains
# - Create a submesh over all facets of the mesh
# - Use `ufl.MixedFunctionSpace` to defined blocked problems.
# - Assemble mixed systems with multiple, related meshes

# +
from mpi4py import MPI
from petsc4py import PETSc

import numpy as np
import numba
import dolfinx
import ufl
from dolfinx import fem, mesh
from dolfinx.cpp.mesh import cell_num_entities
from dolfinx.fem import extract_function_spaces
from dolfinx.fem.petsc import (
    apply_lifting,
    assemble_matrix,
    assemble_vector,
    assign,
    create_vector,
    set_bc,
)
import cffi
from ffcx.codegeneration.utils import empty_void_pointer
from ffcx.codegeneration.utils import numba_ufcx_kernel_signature as ufcx_signature

# -


# We start by creating two convenience functions: One to compute
# the L2 norm of a UFL expression, and one to define the integration
# domains for the facets of all cells in a mesh.


def norm_L2(v: ufl.core.expr.Expr, measure: ufl.Measure = ufl.dx) -> np.inexact:
    """Convenience function to compute the L2 norm of a UFL expression."""
    compiled_form = fem.form(ufl.inner(v, v) * measure)
    comm = compiled_form.mesh.comm
    return np.sqrt(comm.allreduce(fem.assemble_scalar(compiled_form), op=MPI.SUM))


# In DOLFINx, we represent integration domains over entities of
# codimension > 0 as a tuple `(cell_idx, local_entity_idx)`,
# where `cell_idx` is the index of the cell in the mesh
# (local to process), and `local_entity_idx` is the local index
# of the sub entity (local to cell). For the HDG scheme, we will
# integrate over the facets of each cell (for internal facets,
# there will be repeat entries, from the viewpoint of the connected cells).


def compute_cell_boundary_facets(msh: dolfinx.mesh.Mesh) -> np.ndarray:
    """Compute the integration entities for integrals around the
    boundaries of all cells in msh.

    Parameters:
        msh: The mesh.

    Returns:
        Facets to integrate over, identified by ``(cell, local facet
        index)`` pairs.
    """
    tdim = msh.topology.dim
    fdim = tdim - 1
    n_f = cell_num_entities(msh.topology.cell_type, fdim)
    n_c = msh.topology.index_map(tdim).size_local
    return np.vstack((np.repeat(np.arange(n_c), n_f), np.tile(np.arange(n_f), n_c))).T.flatten()


def u_e(x):
    """Exact solution."""
    u_e = 1
    for i in range(tdim):
        u_e *= ufl.sin(ufl.pi * x[i])
    return u_e


comm = MPI.COMM_WORLD
rank = comm.rank
dtype = PETSc.ScalarType

# Create the mesh

n = 2  # Number of elements in each direction
msh = mesh.create_unit_cube(comm, n, n, n, ghost_mode=mesh.GhostMode.none)

# We need to create a broken Lagrange space defined over the facets of
# the mesh. To do so, we require a sub-mesh of the all facets. We begin
# by creating a list of all of the facets in the mesh

tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_entities(fdim)
facet_imap = msh.topology.index_map(fdim)
num_facets = facet_imap.size_local + facet_imap.num_ghosts
facets = np.arange(num_facets, dtype=np.int32)

# The submesh is created with {py:func}`dolfinx.mesh.create_submesh`,
# which takes in the mesh to extract entities from, the topological
# dimension of the entities, and the set of entities to create the
# submesh (indices local to process).
# ```{admonition} Note
# Despite all facets being present in the submesh, the entity map
# isn't necessarily the identity in parallel
# ```

facet_mesh, facet_mesh_emap = mesh.create_submesh(msh, fdim, facets)[:2]

tdim = msh.topology.dim
fdim = tdim - 1
msh.topology.create_connectivity(fdim, tdim)
facet_cell = msh.topology.connectivity(fdim, tdim)
interior_facets = []
for f in range(msh.topology.index_map(fdim).size_local):
    if len(facet_cell.links(f)) == 2:  # Facette intérieure
        interior_facets.append(f)
interior_facets = np.array(interior_facets, dtype=np.int32)
# Define function spaces

k = 1  # Polynomial order
V = fem.functionspace(msh, ("Discontinuous Lagrange", k))
Vbar = fem.functionspace(facet_mesh, ("Discontinuous Lagrange", k))
V_ele_space_dim = V.element.space_dimension
Vbar_ele_space_dim = Vbar.element.space_dimension

# Trial and test functions in mixed space, we use {py:class}`
# ufl.MixedFunctionSpace`
# to create a single function space object we can extract {py:func}`
# ufl.TrialFunctions`
# and {py:func}`ufl.TestFunctions` from.

W = ufl.MixedFunctionSpace(V, Vbar)
u, ubar = ufl.TrialFunctions(W)
v, vbar = ufl.TestFunctions(W)

# ## Define integration measures

# We define the integration measure over cells as we would do in any
# other UFL form.

dx_c = ufl.Measure("dx", domain=msh)

# For the cell boundaries, we need to define an integration measure to
# integrate around the boundary of each cell. The integration entities
# can be computed using the following convenience function.

cell_boundary_facets = compute_cell_boundary_facets(msh)
cell_boundaries = 1  # A tag

# We pass the integration domains into the `ufl.Measure` through the
# `subdomain_data` keyword argument.
ds_c = ufl.Measure("ds", subdomain_data=[(cell_boundaries, cell_boundary_facets)], domain=msh)

# Create a cell integral measure over the facet mesh

dx_f = ufl.Measure("dx", domain=facet_mesh)

# ## Variational formulation

# +
h = ufl.CellDiameter(msh)
n = ufl.FacetNormal(msh)
gamma = 16.0 * k**2 / h  # Scaled penalty parameter

x = ufl.SpatialCoordinate(msh)
c = 1.0 + 0.1 * ufl.sin(ufl.pi * x[0]) * ufl.sin(ufl.pi * x[1])
a = (
    ufl.inner(c * ufl.grad(u), ufl.grad(v)) * dx_c
    - ufl.inner(c * (u - ubar), ufl.dot(ufl.grad(v), n)) * ds_c(cell_boundaries)
    - ufl.inner(ufl.dot(ufl.grad(u), n), c * (v - vbar)) * ds_c(cell_boundaries)
    + gamma * ufl.inner(c * (u - ubar), v - vbar) * ds_c(cell_boundaries)
)


f = -ufl.div(c * ufl.grad(u_e(x)))  # Manufacture a source term
L = ufl.inner(f, v) * dx_c
L += ufl.inner(fem.Constant(facet_mesh, dtype(0.0)), vbar) * dx_f
# -

# We extract the blocks and we set the ufcx kernels
rtype = dolfinx.default_real_type
dtype = dolfinx.default_scalar_type

a_ufl_blocked = ufl.extract_blocks(a)
print(a_ufl_blocked[0][1])

jit_options = {
                    "cffi_extra_compile_args": ["-O2"],
                    "cache_dir": ".cache",
                    "cffi_libraries": ["m"],
                    "cffi_debug": True,
                    'cffi_verbose':True
                }

ufcx_form_00, _, _ = dolfinx.jit.ffcx_jit(msh.comm, a_ufl_blocked[0][0], form_compiler_options={"scalar_type": dtype}, jit_options=jit_options)
kernel_00_cell = getattr(ufcx_form_00.form_integrals[0],
                            f"tabulate_tensor_{np.dtype(dtype).name}")
kernel_00_facet = getattr(ufcx_form_00.form_integrals[1],
                            f"tabulate_tensor_{np.dtype(dtype).name}")

ufcx_form_01, _, _ = dolfinx.jit.ffcx_jit(msh.comm, a_ufl_blocked[0][1], form_compiler_options={"scalar_type": dtype}, jit_options=jit_options)
kernel_01 = getattr(ufcx_form_01.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

ufcx_form_10, _, _ = dolfinx.jit.ffcx_jit(msh.comm, a_ufl_blocked[1][0], form_compiler_options={"scalar_type": dtype}, jit_options=jit_options)
kernel_10 = getattr(ufcx_form_10.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

ufcx_form_11, _, _ = dolfinx.jit.ffcx_jit(msh.comm, a_ufl_blocked[1][1], form_compiler_options={"scalar_type": dtype}, jit_options=jit_options)
kernel_11 = getattr(ufcx_form_11.form_integrals[0],f"tabulate_tensor_{np.dtype(dtype).name}")

print(((ufcx_form_01.form_integrals[0])))

# # RHS forms
L_ufl_blocked = ufl.extract_blocks(L)

ufcx_form_0, _, _ = dolfinx.jit.ffcx_jit(msh.comm, L_ufl_blocked[0], form_compiler_options={"scalar_type": dtype})
kernel_0 = getattr(ufcx_form_0.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

# FIXME: Which should be the Integraltype of this one? dx_f measure
ufcx_form_1, _, _ = dolfinx.jit.ffcx_jit(msh.comm, L_ufl_blocked[1], form_compiler_options={"scalar_type": dtype})
kernel_1 = getattr(ufcx_form_1.form_integrals[0], f"tabulate_tensor_{np.dtype(dtype).name}")

ffi = cffi.FFI()
# FIXME See if there is a better way to pass null
null64 = np.zeros(0, dtype=np.float64)
null32 = np.zeros(0, dtype=np.int32)
null8 = np.zeros((1), dtype=np.uint8)

# Our bilinear form involves two domains (`msh` and `facet_mesh`). The
# mesh passed to the measure is called the "integration domain". For
# each additional mesh in our form, we must pass an
# {py:class}`EntityMap<dolfinx.mesh.EntityMap` object
# that relates entities in that mesh to entities in the integration
# domain. In this case, the only other mesh is `facet_mesh`, so we pass
# `facet_mesh_emap`.

Usize = Vbar.element.space_dimension
Ssize = V.element.space_dimension
num_cell_facets = msh.ufl_cell().num_facets
print("Usize", Usize)
print("Ssize", Ssize)
@numba.njit(fastmath=True)
def compute_A00(w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
    A00 = np.zeros((Ssize, Ssize), dtype=dtype)
    kernel_00_cell(
        ffi.from_buffer(A00),
        w_,
        c_,
        coords_,
        entity_local_index,
        permutation,
        empty_void_pointer(),
    )
    entity_local_index = np.zeros((1), dtype=np.int32)
    for local_f in range(num_cell_facets):
        entity_local_index[0] = local_f
        
        kernel_00_facet(
            ffi.from_buffer(A00),
            w_,
            c_,
            coords_,
            ffi.from_buffer(entity_local_index),
            ffi.from_buffer(null8),
            empty_void_pointer(),
        )
        
    return A00

@numba.njit(fastmath=True)
def compute_A10(w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
    A10 = np.zeros((num_cell_facets * Usize, Ssize), dtype=dtype)
    A10_f = np.zeros((Usize, Ssize), dtype=dtype)
    entity_local_index = np.zeros((1), dtype=np.int32)
    for local_f in range(num_cell_facets):
        entity_local_index[0] = local_f
        A10_f.fill(0.0)

        kernel_10(
        ffi.from_buffer(A10_f),
        w_,
        c_,
        coords_,
        ffi.from_buffer(entity_local_index),
        ffi.from_buffer(null8),
        empty_void_pointer(),
        )
        # Insert in correct location
        start = local_f * Usize
        end = start + Usize
        A10[start:end, :] += A10_f[:, :]
    return A10

@numba.njit(fastmath=True)
def compute_A01(w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
    A01 = np.zeros((Ssize, num_cell_facets * Usize), dtype=dtype)
    A01_f = np.zeros((Ssize, Usize), dtype=dtype)
    entity_local_index = np.zeros((1), dtype=np.int32)
    for local_f in range(num_cell_facets):
        entity_local_index[0] = local_f
        A01_f.fill(0.0)
        
        kernel_01(
        ffi.from_buffer(A01_f),
        w_,
        c_,
        coords_,
        ffi.from_buffer(entity_local_index),
        ffi.from_buffer(null8),
        empty_void_pointer(),
        )
        
        # Insert in correct location
        start = local_f * Usize
        end = start + Usize
        A01[:, start:end] += A01_f[:, :]
    return A01

@numba.njit(fastmath=True)
def compute_A11(w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
    A11 = np.zeros((num_cell_facets * Usize, num_cell_facets * Usize), dtype=dtype)
    A11_f = np.zeros((Usize, Usize), dtype=dtype)
    entity_local_index = np.zeros((1), dtype=np.int32)
    for local_f in range(num_cell_facets):
        entity_local_index[0] = local_f
        kernel_11(
        ffi.from_buffer(A11_f),
        w_,
        c_,
        coords_,
        ffi.from_buffer(entity_local_index),
        ffi.from_buffer(null8),
        empty_void_pointer(),
        )
        
        # Insert in correct location
        start = local_f * Usize
        end = start + Usize
        A11[start:end, start:end] += A11_f[:, :]
    return A11

@numba.njit(fastmath=True)
def compute_b1(w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
    b_1 = np.zeros(Usize * num_cell_facets, dtype=PETSc.ScalarType)
    b_1_f = np.zeros(Usize, dtype=PETSc.ScalarType)
    entity_local_index = np.zeros((1), dtype=np.int32)
    for local_f in range(num_cell_facets):
        entity_local_index[0] = local_f
        kernel_11(
        ffi.from_buffer(b_1_f),
        w_,
        c_,
        coords_,
        ffi.from_buffer(entity_local_index),
        ffi.from_buffer(null8),
        empty_void_pointer(),
        )
        
        # Insert in correct location
        start = local_f * Usize
        end = start + Usize
        b_1[start:end] += b_1_f[:]
    return b_1

@numba.cfunc(ufcx_signature(dtype, rtype), nopython=True, fastmath=True)
def tabulate_tensor_L(b_,  w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
        b_local = numba.carray(b_, num_cell_facets * Usize,
                               dtype=PETSc.ScalarType)
        b_0 = np.zeros(Ssize, dtype=PETSc.ScalarType)
        kernel_0(
                ffi.from_buffer(b_0),
                w_,
                c_,
                coords_,
                entity_local_index,
                permutation,
                empty_void_pointer(),
            )
        A00 = compute_A00(w_, c_, coords_, entity_local_index, permutation=permutation)
        A10 = compute_A10(w_, c_, coords_, entity_local_index, permutation=permutation)
        b_local -= A10 @ np.linalg.solve(A00, b_0)
        b_1 = compute_b1(w_, c_, coords_, entity_local_index, permutation=permutation)
        b_local += b_1

@numba.cfunc(ufcx_signature(dtype, rtype), nopython=True, fastmath=True)
def backsub(x_, w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
        x = numba.carray(x_, Ssize, dtype=PETSc.ScalarType)
        u_bar = numba.carray(w_, Usize * num_cell_facets,
                             dtype=PETSc.ScalarType)
        b_0 = np.zeros(Ssize, dtype=PETSc.ScalarType)
        # TODO Pass nullptr for last two parameters
        kernel_0(
                ffi.from_buffer(b_0),
                w_,
                c_,
                coords_,
                entity_local_index,
                permutation,
                empty_void_pointer(),
            )
        A00 = compute_A00(w_, c_, coords_, entity_local_index, permutation=permutation)
        A01 = compute_A01(w_, c_, coords_, entity_local_index, permutation=permutation)
        x += np.linalg.solve(A00, b_0 - A01 @ u_bar)

@numba.cfunc(ufcx_signature(dtype, rtype), nopython=True, fastmath=True)  # type: ignore
def tabulate_A_funcs(A_, w_, c_, coords_, entity_local_index, permutation=ffi.NULL, custom_data=None):
     # Prepare target condensed local element tensor
    A_local = numba.carray(A_, (num_cell_facets  * Usize, num_cell_facets * Usize), dtype=dtype)
    
    A00 = compute_A00(w_, c_, coords_, entity_local_index, permutation=permutation)
    A01 = compute_A01(w_, c_, coords_, entity_local_index, permutation=permutation)
    A10 = compute_A10(w_, c_, coords_, entity_local_index, permutation=permutation)
    A11 = compute_A11(w_, c_, coords_, entity_local_index, permutation=permutation)
    # A[:, :] = A11[:, :]
    A_local[:, :] = A11 - A10 @ np.linalg.solve(A00, A01)
    

formtype = dolfinx.fem.form_cpp_class(dtype)  # type: ignore
cells = np.arange(msh.topology.index_map(msh.topology.dim).size_local)
facets = np.arange(msh.topology.index_map(fdim).size_local)
print("Facets", np.shape(facets))
print("cell_boundary_facets", np.shape(cell_boundary_facets))
# integrals = {dolfinx.fem.IntegralType.cell: [(0, tabulate_A.address, cells, np.array([], dtype=np.int8))]}
integrals = {dolfinx.fem.IntegralType.cell: [(0, tabulate_A_funcs.address, cells, np.array([], dtype=np.int8))]}
form_type_compiled = formtype([Vbar._cpp_object, Vbar._cpp_object], integrals, [], [], False, mesh=msh._cpp_object, entity_maps=[facet_mesh_emap._cpp_object])
a_cond = dolfinx.fem.Form(form_type_compiled)

# Next, we pass the compiled kernel to the standard {py:func}`
# assemble_matrix <dolfinx.fem.petsc.assemble_matrix>` function to assemble
# to the global condensed stiffness matrix. We also assemble the right-hand
# side vector using {py:func}`assemble_vector
# <dolfinx.fem.petsc.assemble_vector>` and apply the boundary conditions by
# {py:func}`applying lifting <dolfinx.fem.petsc.apply_lifting>` and
# {py:meth}`set bc<dolfinx.fem.DirichletBC.set>`.

# Apply Dirichlet boundary conditions. We begin by locating the boundary
# facets of msh.

msh_boundary_facets = mesh.exterior_facet_indices(msh.topology)

# Since the boundary condition is enforced in the facet space, we need
# to get the corresponding facets in `facet_mesh` using the entity map

facet_mesh_boundary_facets = facet_mesh_emap.sub_topology_to_topology(
    msh_boundary_facets, inverse=True
)

# Get the dofs and apply the boundary condition

num_dofs_local = (Vbar.dofmap.index_map.size_local) * Vbar.dofmap.index_map_bs
num_dofs_global = Vbar.dofmap.index_map.size_global * Vbar.dofmap.index_map_bs

print(f"Number of dofs (owned) by rank {MPI.COMM_WORLD.rank}: {num_dofs_local}")
if MPI.COMM_WORLD.rank ==0:
    print(f"Number of dofs global: {num_dofs_global}")

facet_mesh.topology.create_connectivity(fdim, fdim)
dofs = fem.locate_dofs_topological(Vbar, fdim, facet_mesh_boundary_facets)
bc = fem.dirichletbc(dtype(0.0), dofs, Vbar)
print("Assembling")
A_cond = assemble_matrix(a_cond, bcs=[bc])
A_cond.assemble()
A_cond.view()
print("Assembled")


integrals_L = {dolfinx.fem.IntegralType.cell:  [(0, tabulate_tensor_L.address, cells, np.array([], dtype=np.int8))]}
L_kernel = dolfinx.fem.Form(formtype([Vbar._cpp_object], integrals_L, [], [], False, mesh=msh._cpp_object, entity_maps=[facet_mesh_emap._cpp_object]))
print("Assembling vector")
b = assemble_vector(L_kernel)
print("Assembled vector")
apply_lifting(b, [a_cond], bcs=[[bc]])
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)  # type: ignore
bc.set(b)
print("boundary condition set")

use_direct_solver = True

if use_direct_solver:
    ksp = PETSc.KSP().create(msh.comm)
    ksp.setOperators(A_cond)
    ksp.setType("preonly")
    ksp.getPC().setType("lu")
    ksp.getPC().setFactorSolverType("mumps")
else:
    opts = PETSc.Options()
    # Solver (Use conjugate gradient)
    opts['ksp_type'] = 'cg'
    opts['ksp_rtol'] = '1.0e-8'
    opts['ksp_view'] = None
    opts['ksp_monitor'] = None
    opts['pc_type'] = 'hypre'
    opts['pc_hypre_type'] = 'boomeramg'
    opts['pc_hypre_boomeramg_strong_threshold'] = 0.85
    opts['pc_hypre_boomeramg_agg_nl'] = 1
    # opts['pc_hypre_boomeramg_agg_num_paths'] = 2       
    PETSc.Options().view()

    ksp = PETSc.KSP().create(msh.comm)
    ksp.setOperators(A_cond)
    ksp.setFromOptions()
    
ubar = dolfinx.fem.Function(Vbar, name="u_from_condensation")
solver = PETSc.KSP().create(A_cond.getComm())  # type: ignore
solver.setOperators(A_cond)
solver.solve(b, ubar.x.petsc_vec)
solver.destroy()



integrals_backsub = {dolfinx.fem.IntegralType.cell:  [(0, backsub.address, cells, np.array([], dtype=np.int8))]}
u_form = dolfinx.fem.Form(formtype([V._cpp_object], integrals_backsub, [ubar._cpp_object], [], False, None, entity_maps=[facet_mesh_emap._cpp_object]))
coeffs = dolfinx.fem.pack_coefficients(u_form)
u = dolfinx.fem.Function(V)
dolfinx.fem.assemble_vector(u.x.array, u_form, coeffs=coeffs)
u.vector.ghostUpdate(addv=PETSc.InsertMode.ADD,
                        mode=PETSc.ScatterMode.REVERSE)

Thanks!

1 Like

Correct me if I am wrong, but the static condensation demo is elementwise Static condensation of linear elasticity — DOLFINx 0.10.0.0 documentation. I believe the approach for a problem involving both mesh and mesh skeleton, like the HDG problem, will borrow elements of Dean’s solution such as the separate compilations of the \pmb{A}_{ij} submatrices. I will incorporate Dean’s approach and update.

You are right the example of static condensation is elementwise, the only difference indeed is that for HDG as you correctly state, there is the mesh and the mesh skeleton.

Hi all,

I’ve been meaning to update that code for a long time now, but so far I haven’t managed to find time. Some parts should be significantly simpler now we have mixed-domain support. Ideally we want to use the standard DOLFINx assemblers to avoid having to write our own custom ones. The main problem is how to iterate over cells but add contributions to the facet space in the standard assemblers. This might be possible using a custom mat_set function, but I haven’t looked into it in detail yet. I’ll have a think and try to get back to you as soon as I can.

Thanks,

Joe

2 Likes