Inserting several vectors into assembled global matrix equation

I am not having a lot of luck with the Sherman Morrison approach, so I decided to take a look at an alternative approach. I would like to construct a block matrix equation of the following form
\left\lbrack\begin{array}{cc} \lbrack A\rbrack & \lbrack V\rbrack \\ \lbrack V\rbrack^T & \lbrack 1\rbrack \end{array} \right \rbrack \left\{\begin{array}{c} \{E\} \\ \{C\}\end{array}\right \} = \left\{ \begin{array}{c} \{b\} \\ \{0\}\end{array}\right \}
where \lbrack A\rbrack is the usual matrix assembled from the bilinear form. The square submatrix [1] is the identity matrix. The off diagonal block \lbrack V\rbrack is formed by a set of sparse vectors based on linear forms generated by surface integrals
v_i = \iint\limits_{\partial\Omega_i} \mathbf{v}\cdot \mathbf{h}_i dS
where \mathbf{h} is a known function. The vector is “dense” over the DoFs within the surface \partial\Omega_i, but the surface DoFs generally a small proportion of the total problem DoFs, so the vectors are quite sparse.

I have read through some of the descriptions of using nested matrices in PETSc. Most interesting was this approach. Unfortunately, it looks like the author of the post was inserting complete vectors into a PETSc matrix. For large problems, this will spoil the sparsity of the LU factorization. Is there a way to insert only the non-zero values of the [V] matrix block or dhttps://fenicsproject.discourse.group/t/is-it-possible-to-insert-a-vector-into-an-assembled-matrix/14118oes PETSc automatically ignore “zero” matrix elements? What is the best way to implement this block structure (using the PETSc nested storage, perhaps)? Online documentation seems a bit limited. Any help would be greatly accepted.

PETSc does give you the tools to precisely specify the sparsity pattern of your matrix, but it gets quite involved quite quickly (MatCreateAIJ — PETSc 3.24.0 documentation )

Maybe just setting the NNZ (number of non-zeros) already gives you the performance you need? I’ve played around with that quite a while ago (Assembly of FEniCS submatrices in large PETSc matrix: preallocation - #6 by Stein)

The PETSc matrix call M.getInfo() is quite useful for checking whether PETSc is actually doing what you intend.

For more targeted help you’d have to supply a MWE.

2 Likes

Thanks @Stein ! I took your advice and used MatCreateAIJ. The assembly worked well to build a nested matrix. However, the solve stage does not work. (Here is an example.)

# Example to test multi-mode waveguide absorbing boundary condition
# for a simple rectangular waveguide using nested PETSc matrices
import sys
import numpy as np
from mpi4py import MPI as M
from petsc4py import PETSc as P
import gmsh
import ufl as U
import basix.ufl
from dolfinx.mesh import locate_entities_boundary, meshtags
from dolfinx.io import gmshio
from dolfinx.fem import petsc
from dolfinx import fem
from dolfinx import io
import time

eta0 = 377.0 # free space wave impedance

# The TE modes in rectangular WG for excitation and mode vectors in absorbing BC
class ModesTE:
    def __init__(self, beta, k, a, b, m, n):
        self.beta = beta
        self.k = k
        self.a = a
        self.b = b
        self.m = m
        self.n = n
    def eval_H(self, x):
        ZTE = self.k * eta0 / self.beta
        if((self.m != 0) and (self.n != 0)):
            B = np.sqrt(4.0 / (ZTE * self.a * self.b))
        elif self.m == 0:
            B = np.sqrt(2.0 / (ZTE * self.a * self.b))
        else:
            B = np.sqrt(2.0 / (ZTE * self.a * self.b))
        hx = B * np.sin((np.pi * self.m / self.a) * x[0]) * np.cos((np.pi * self.n / self.b) * x[1])
        hy = B * np.cos((np.pi * self.m / self.a) * x[0]) * np.sin((np.pi * self.n / self.b) * x[1])
        hz = np.full_like(hx, 0.0+0j, dtype=np.complex128)
        return(hx, hy, hz)

    def eval_E(self, x):
        ZTE = self.k * eta0 / self.beta
        if((self.m != 0) and (self.n != 0)):
            B = np.sqrt(4.0 * ZTE / (self.a * self.b))
        elif self.m == 0:
            B = np.sqrt(2.0 * ZTE / (self.a * self.b))
        else:
            B = np.sqrt(2.0 * ZTE / (self.a * self.b))
        ey = -B * np.sin((np.pi * self.m / self.a) * x[0]) * np.cos((np.pi * self.n / self.b) * x[1])
        ex = B * np.cos((np.pi * self.m / self.a) * x[0]) * np.sin((np.pi * self.n / self.b) * x[1])
        ez = np.full_like(hx, 0.0+0j, dtype=np.complex128)
        return(ex, ey, ez)

eps = 1.0e-4
a = 22.86 #WG width
b = 10.16 #WG height
l = 25.8 # WG length
f = 10.5 #frequency of operation GHz
c = 300.0 #speed of light
lm = 8.20

comm = M.COMM_WORLD
mpiRank = comm.rank
modelRank = 0 # where GMSH runs
k0 = 2.0 * np.pi * f / c #wavenumber wrt frequency

if mpiRank == modelRank:
    gmsh.initialize()
    gmsh.option.setNumber('General.Terminal', 1)
    gmsh.model.add('Waveguide')
    gmsh.model.occ.addBox(0, 0, 0, a, b, l, 1)
    gmsh.model.occ.synchronize()
    pt = gmsh.model.getEntities(dim=0)
    gmsh.model.mesh.setSize(pt, lm)

    gmsh.option.setNumber('Mesh.MeshSizeMin', 0.01)
    gmsh.option.setNumber('Mesh.MeshSizeMax', 8.2)
    gmsh.option.setNumber('Mesh.Algorithm', 6) #1=Mesh Adapt, 2=Auto, 3=Initial mesh only, 5=Delaunay, 6=Frontal-Delaunay
    gmsh.option.setNumber('Mesh.MinimumCirclePoints', 30)
    gmsh.option.setNumber('Mesh.CharacteristicLengthFromCurvature', 1)
    gmsh.option.setNumber('Mesh.Tetrahedra', 0)
    OUT = []
    IN = []
    PEC = []
    bb = gmsh.model.getEntities(dim=3)
    pt = gmsh.model.getBoundary(bb, combined=True, oriented=False, recursive=False)
    for bnd in pt:
        CoM = gmsh.model.occ.getCenterOfMass(bnd[0], bnd[1])
        if np.abs(CoM[2]) < eps:
            IN.append(bnd[1])
        elif np.abs(CoM[2] - l) < eps:
            OUT.append(bnd[1])
        else:
            PEC.append(bnd[1])

    gmsh.model.addPhysicalGroup(3, [1], 1) #WG
    gmsh.model.setPhysicalName(3, 1, "Waveguide")
    gmsh.model.addPhysicalGroup(2, PEC, 1)
    gmsh.model.setPhysicalName(2, 1, "PEC")
    gmsh.model.addPhysicalGroup(2, IN, 2)
    gmsh.model.setPhysicalName(2, 2, "INPORT")
    gmsh.model.addPhysicalGroup(2, OUT, 3)
    gmsh.model.setPhysicalName(2, 3, "OUTPORT")

    gmsh.model.mesh.generate(3)


mesh, ct, fm = gmshio.model_to_mesh(gmsh.model, comm, modelRank, gdim=3)
if mpiRank == modelRank:
    gmsh.finalize()

mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim)
elem = basix.ufl.element('Nedelec 1st kind H(curl)', mesh.basix_cell(), degree=2)
V = fem.functionspace(mesh, elem)
u = U.TrialFunction(V)
v = U.TestFunction(V)
normal = U.FacetNormal(mesh)
ds = U.Measure("ds", domain=mesh, subdomain_data = fm)
# Dirichlet BCs on waveguide walls only in-ports and out ports have Neumann condition.
facets = fm.find(1)
ubc = fem.Function(V)
ubc.interpolate(lambda x: np.array([0*x[0], 0*x[1], 0*x[2]], dtype=np.complex128))
dofs = fem.locate_dofs_topological(V, mesh.topology.dim-1, facets)
bc = fem.dirichletbc(ubc, dofs)
# Build rhs - Excitation at in port.
uh = fem.Function(V, dtype=np.complex128)
kz = np.sqrt(k0 * k0 - (np.pi / a)**2)
f = ModesTE(kz, k0, a, b, 1, 0)
uh.interpolate(f.eval_H)
finc = 2.0j * k0 * eta0 * U.inner(U.cross(normal, uh), v) * ds(2)
uh.name = "H_inc"
# Generate mode inner product for projection operator overi in and out port surfaces.
ua = fem.Function(V, dtype=np.complex128)
kz = np.sqrt(k0 * k0 - (np.pi / a)**2)
Zm = eta0 * k0 / kz
f = ModesTE(kz, k0, a, b, 1, 0) #Mode 1 only
ua.interpolate(f.eval_H)
# Generate linear forms for mode functions on ports
um1a = fem.form(U.inner(ua, U.cross(normal, v)) * ds(2)) #input plane
um1b = fem.form(U.inner(ua, U.cross(normal, v)) * ds(3)) #output plane
P1 = mesh.comm.allreduce(fem.assemble_scalar(fem.form(U.inner(ua, ua) * ds(2))) * Zm , op=M.SUM)
P2 = mesh.comm.allreduce(fem.assemble_scalar(fem.form(U.inner(ua, ua) * ds(3))) * Zm , op=M.SUM)
print(P1, P2)
t_start = time.perf_counter()

# Weak form
L = (U.inner(U.curl(u), U.curl(v)) - k0 * k0 * U.inner(u, v)) * U.dx
Rf = fem.form(finc)
Df = fem.form(L)

num_dofs_local = V.dofmap.index_map.size_local * V.dofmap.index_map_bs
num_dofs_global = V.dofmap.index_map.size_global * V.dofmap.index_map_bs
print(f"Number of dofs (owned) by rank {mpiRank}: {num_dofs_local}")
if mpiRank == modelRank:
    print(f"Number of dofs global: {num_dofs_global}")

#Define PETSc vectors 
def VecCreate(linear_form, bilin_form, BCs):
    vec = petsc.create_vector(linear_form)
    with vec.localForm() as lb:
        lb.set(0.0)
    vec.set(0)
    petsc.assemble_vector(vec, linear_form)
#    petsc.apply_lifting(vec, [bilin_form], [[BCs]])
    vec.ghostUpdate(addv=P.InsertMode.ADD_VALUES, mode=P.ScatterMode.REVERSE)
    petsc.set_bc(vec, [BCs])
    vec.ghostUpdate(addv=P.InsertMode.INSERT, mode=P.ScatterMode.FORWARD)
    return vec

A = petsc.assemble_matrix(Df, bcs=[bc])
A.assemble()
b = VecCreate(Rf, Df, bc)
u1 = VecCreate(um1a, Df, bc)
u2 = VecCreate(um1b, Df, bc)

sol = fem.Function(V)

U = P.Mat().createAIJ(size=(b.size, 2))
U.setUp()
print("Vector size ={0}".format(b.getSize()), flush=True)
print("MPI process ownership ranges" + str(b.getOwnershipRanges()), flush=True)
iz = b.getOwnershipRanges()
nnb = 0
for ii, index in enumerate(iz):
    if ii != 0:
        if mpiRank == ii-1:
           for jj in range(iz[ii-1], index):
               aa = b.getValue(jj)
               if(np.abs(aa) > 0.01*eps):
                  print("Proc ={0}, index={1}, Entry={2}".format(ii, jj, aa), flush=True)
                  nnb += 1
print(mpiRank, nnb, flush=True)
print("Vector size ={0}".format(u1.getSize()), flush=True)
print("MPI process ownership ranges" + str(u1.getOwnershipRanges()), flush=True)
iz = u1.getOwnershipRanges()
nnu1 = 0
for ii, index in enumerate(iz):
    if ii != 0:
        if mpiRank == ii-1:
           for jj in range(iz[ii-1], index):
               aa = u1.getValue(jj)
               if(np.abs(aa) > 0.001*eps):
                  U.setValue(jj, 0, 1j * kz * aa, addv=P.InsertMode.INSERT_VALUES)
                  print("Proc ={0}, index={1}, Entry={2}".format(ii, jj, 1j * kz * aa), flush=True)
                  nnu1 += 1
print(mpiRank, nnu1, flush=True)
print("Vector size ={0}".format(u2.getSize()), flush=True)
print("MPI process ownership ranges" + str(u2.getOwnershipRanges()), flush=True)
iz = u2.getOwnershipRanges()
nnu2 = 0
for ii, index in enumerate(iz):
    if ii != 0:
        if mpiRank == ii-1:
           for jj in range(iz[ii-1], index):
               aa = u2.getValue(jj)
               if(np.abs(aa) > 0.001*eps):
                  U.setValue(jj, 1, 1j * kz * aa, addv=P.InsertMode.INSERT_VALUES)
                  print("Proc ={0}, index={1}, Entry={2}".format(ii, jj, 1j * kz * aa), flush=True)
                  nnu2 += 1
print(mpiRank, nnu2, flush=True)
U.assemble()
Ut = U.transpose()
# Identity block
Id = P.Mat().createAIJ(size=(2, 2))
Id.setUp()
Id.setValue(0, 0, 1.0, addv=P.InsertMode.INSERT_VALUES)
Id.setValue(1, 1, 1.0, addv=P.InsertMode.INSERT_VALUES)
Id.assemble()

A_new = P.Mat()
A_new.createNest([[A, U], [Ut, Id]], comm = comm)
A_new.setUp()
A_new.assemble()

b_0 = P.Vec().create(comm=comm)
b_0.setSizes(2)
b_0.setUp()
b_0.assemble()
print("***********Last line!*************")
x_0 = P.Vec().create(comm=comm)
x_0.setSizes(2)
x_0.setUp()
x_0.assemble()

b_new = P.Vec()
b_new.createNest([b, b_0], comm = comm)
b_new.setUp()
b_new.assemble()

x_new = P.Vec()
x_new.createNest([sol.x.petsc_vec, x_0], comm=comm)
x_new.setUp()
petsc_options = {
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver": "mumps",
        "pc_factor_nonzeros_along_diagonal": 1e-8,
        "ksp_error_if_not_converged": False,
        "ksp_monitor": None }
opts = P.Options()
for key, value in petsc_options.items():
    opts[key] = value

solver = P.KSP().create(mesh.comm)
solver.setOperators(A_new)
solver.setFromOptions()
solver.setUp()
t_stop = time.perf_counter()

solver.solve(b_new, x_new)

print(A_new.sizes)


sys.exit(0)

I get this error:

petsc4py.PETSc.Error: error code 92
[2] KSPSetUp() line 406 in ./src/ksp/ksp/interface/itfunc.c
[2] PCSetUp() line 1017 in ./src/ksp/pc/interface/precon.c
[2] PCSetUp_LU() line 82 in ./src/ksp/pc/impls/factor/lu/lu.c
[2] MatGetFactor() line 4756 in ./src/mat/interface/matrix.c
[2] See https://petsc.org/release/overview/linear_solve_table/ for possible LU and Cholesky solvers
[2] Could not locate a solver type for factorization type LU and matrix type nest.
    solver.setUp()

I have seen from other posts that MUMPS LU solver does not work for nested matrices.
This leads me to a couple of questions.

  1. Is there a way to convert the nested form into a form suitable for LU factorization solvers?
  2. Would it be easier to avoid the nested construction? What type of matrix is suitable for direct methods? How would I go about creating a monolithic sparse matrix for use with an LU solver?

Unfortunately, I cannot find much documentation on the topic. Most assumes that iterative solvers are used (Navier Stokes p-v formulations, etc.) I need to manually insert rows/columns in the PETSc matrix that do not correspond to FEM degrees of freedom, but represent external constraints.

I think the keyword here should be pc_factor_mat_solver_type and is the reason your code doesn’t work atm. Only mumps works with LU with nest matrices atm.

2 Likes

Thanks @Dokken. I changed the line in the options block, but PETSc complained again:

[1] KSPSetUp() line 406 in ./src/ksp/ksp/interface/itfunc.c
[1] PCSetUp() line 1017 in ./src/ksp/pc/interface/precon.c
[1] PCSetUp_LU() line 82 in ./src/ksp/pc/impls/factor/lu/lu.c
[1] MatGetFactor() line 4759 in ./src/mat/interface/matrix.c
[1] See https://petsc.org/release/overview/linear_solve_table/ for possible LU and Cholesky solvers
[1] MatSolverType mumps does not support matrix type nest

It says MUMPS does not support nested matrices. I am clearly missing something here, since the PETSc docs say MUMPS supports nested matrices.

EDIT:
I checked my PETSc4py version and it is v3.15. I use it with Dolfinx 0.9.0. Could this be a problem?
I also add a very short code, to reduce the possibility of some external misconfiguration, that illustrates the same problem.

import sys
import numpy as np
from mpi4py import MPI as M
from petsc4py import PETSc as P

comm = M.COMM_WORLD
mpiRank = comm.rank

N = 100 # Matrix order
BW = 6 # matrix bandwidth fill

U = P.Mat().createAIJ(size=(N, N))
V = P.Mat().createAIJ(size=(N, N))

U.setUp()
V.setUp()

rnd = np.random.default_rng(seed=109987091)

for i in range(N):
    for k in range(-BW, BW):
        j = i + k
        if (j >= 0) and (j < N):
            aa = rnd.standard_normal(1)
            U.setValue(i, j, aa, addv=P.InsertMode.INSERT_VALUES)
            V.setValue(i, j, aa, addv=P.InsertMode.INSERT_VALUES)

U.assemble()
V.assemble()

print(U.getOwnershipRanges(), V.getOwnershipRanges)

A_new = P.Mat()
A_new.createNest([[U, None], [None, V]], comm = comm)
A_new.setUp()


print(A_new.getOwnershipRanges())

petsc_options = {
        "ksp_type": "preonly",
        "pc_type": "lu",
        "pc_factor_mat_solver_type": "mumps",
        "pc_factor_nonzeros_along_diagonal": 1e-8,
        "ksp_error_if_not_converged": True,
        "ksp_monitor": None }
opts = P.Options()
for key, value in petsc_options.items():
    opts[key] = value

solver = P.KSP().create(comm)
solver.setOperators(A_new)
solver.setFromOptions()
solver.setUp()

sys.exit(0)

You are using a quite old version of PETSC (from 2021) where this is not supported. Please upgrade PETSc.

Thanks @Dokken. I had a look at the PETSc versions and it looks like this functionality is in PETSc versions 3.20 and later. I am doing an OS upgrade on my desktop cluster to Ubuntu 24.04 and the apt installer wants to install PETSc version 3.19 with dolfinx :confounded_face: . Since I am not a particularly good computer administrator, what is the best way to force the installation of a newer version of petsc4py (3.20 or newer) ? Even pip wants to install v. 3.19.

EDIT: Taking a look at the Spack installer, which looks like it gives more control over packages and permits installation in user directory.

Following Dokken’s suggestion, I did a reinstall of Fenicsx 0.9.0 with PETSc 3.24.1 via spack, and the installation proceeded without problems. The short code that I posted for testing PETSc nested matrices with MUMPS now works! Yay! Progress!

I have a new problem (which is surely a package configuration problem) when trying to run Fenicsx. Code that previously ran now produces the following error.

Traceback (most recent call last):
  File "/home/bill/Cluster/Fenics2020/WaveguideBC/Rect1.py", line 10, in <module>
  File "/home/bill/spack/var/spack/environments/fenicsx-env/.spack-env/view/lib/python3.12/site-packages/dolfinx/__init__.py", line 20, in <module>
  File "/home/bill/spack/var/spack/environments/fenicsx-env/.spack-env/view/lib/python3.12/site-packages/dolfinx/__init__.py", line 20, in <module>
    from dolfinx.io import gmshio
    assert dolfinx.common.has_petsc4py
    assert dolfinx.common.has_petsc4py
    from dolfinx.io import gmshio
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bill/spack/var/spack/environments/fenicsx-env/.spack-env/view/lib/python3.12/site-packages/dolfinx/__init__.py", line 20, in <module>

AssertionError

Can someone with Spack experience tell me what this means? Is there a missing package? The package petsc4py exists and the complex version is present.

EDIT: Found out that petsc4py was not linked to Dolfinx0.9.0 module. Now this works.

Hello again! I decided to move my questions from the spack installation thread back to this discussion.

The generation of matrix blocks using the Scifem real function spaces looks like the way to go for building these types of matrices. I am now trying to implement this method into a simulation that uses a GMSH generated mesh and I have come up against another roadblock. In the code:

# Dolfin 0.9.0, PETSc 3.24.1
import sys
import numpy as np
from mpi4py import MPI as M
from petsc4py import PETSc as P
import gmsh
import ufl as U
import basix.ufl
from dolfinx.io import gmshio
from dolfinx.fem import petsc
from dolfinx import mesh as msh
from dolfinx import fem
from dolfinx import io
import scifem
import time

eta0 = 377.0 # free space wave impedance

class ModesTE:
    def __init__(self, beta, k, a, b, m, n):
        self.beta = beta
        self.k = k
        self.a = a
        self.b = b
        self.m = m
        self.n = n
    def eval_H(self, x):
        if((self.m != 0) and (self.n != 0)):
            B = np.sqrt(self.n * self.m * 4.0 / (self.a * self.b))
        elif self.m == 0:
            B = np.sqrt(self.n * 2.0 / (self.a * self.b))
        else:
            B = np.sqrt(self.m * 2.0 / (self.a * self.b))
        hx = B * np.sin((np.pi * self.m / self.a) * x[0]) * np.cos((np.pi * self.n / self.b) * x[1])
        hy = B * np.cos((np.pi * self.m / self.a) * x[0]) * np.sin((np.pi * self.n / self.b) * x[1])
        hz = np.full_like(hx, 0.0+0j, dtype=np.complex128)
        return(hx, hy, hz)
    def eval_E(self, x):
        if((self.m != 0) and (self.n != 0)):
            B = np.sqrt(self.n * self.m * 4.0 / (ZTE * self.a * self.b))
        elif self.m == 0:
            B = np.sqrt(self.n * 2.0 / (ZTE * self.a * self.b))
        else:
            B = np.sqrt(self.m * 2.0 / (ZTE * self.a * self.b))
        ZTE = eta0 * self.k / self.beta
        ey = -ZTE * B * np.sin((np.pi * self.m / self.a) * x[0]) * np.cos((np.pi * self.n / self.b) * x[1])
        ex = ZTE * B * np.cos((np.pi * self.m / self.a) * x[0]) * np.sin((np.pi * self.n / self.b) * x[1])
        ez = np.full_like(hx, 0.0+0j, dtype=np.complex128)
        return(ex, ey, ez)

eps = 1.0e-4
a = 22.86 #WG width
b = 10.16 #WG height
l = 50.8 # WG length
f = 10.5 #frequency of operation GHz
c = 300.0 #speed of light
lm = 6.60

comm = M.COMM_WORLD
mpiRank = comm.rank
modelRank = 0 # where GMSH runs
k0 = 2.0 * np.pi * f / c #wavenumber wrt frequency

if mpiRank == modelRank:
    gmsh.initialize()
    gmsh.option.setNumber('General.Terminal', 1)
    gmsh.model.add('Waveguide')
    gmsh.model.occ.addBox(0, 0, 0, a, b, l, 1)
    gmsh.model.occ.synchronize()
    pt = gmsh.model.getEntities(dim=0)
    gmsh.model.mesh.setSize(pt, lm)

    gmsh.option.setNumber('Mesh.MeshSizeMin', 0.01)
    gmsh.option.setNumber('Mesh.MeshSizeMax', 2.2)
    gmsh.option.setNumber('Mesh.Algorithm', 6) #1=Mesh Adapt, 2=Auto, 3=Initial mesh only, 5=Delaunay, 6=Frontal-Delaunay
    gmsh.option.setNumber('Mesh.MinimumCirclePoints', 30)
    gmsh.option.setNumber('Mesh.CharacteristicLengthFromCurvature', 1)
    gmsh.option.setNumber('Mesh.Tetrahedra', 0)
    OUT = []
    IN = []
    PEC = []
    bb = gmsh.model.getEntities(dim=3)
    pt = gmsh.model.getBoundary(bb, combined=True, oriented=False, recursive=False)
    for bnd in pt:
        CoM = gmsh.model.occ.getCenterOfMass(bnd[0], bnd[1])
        if np.abs(CoM[2]) < eps:
            IN.append(bnd[1])
        elif np.abs(CoM[2] - l) < eps:
            OUT.append(bnd[1])
        else:
            PEC.append(bnd[1])

    gmsh.model.addPhysicalGroup(3, [1], 1) #WG
    gmsh.model.setPhysicalName(3, 1, "Waveguide")
    gmsh.model.addPhysicalGroup(2, PEC, 1)
    gmsh.model.setPhysicalName(2, 1, "PEC")
    gmsh.model.addPhysicalGroup(2, IN, 2)
    gmsh.model.setPhysicalName(2, 2, "INPORT")
    gmsh.model.addPhysicalGroup(2, OUT, 3)
    gmsh.model.setPhysicalName(2, 3, "OUTPORT")

    gmsh.model.mesh.generate(3)
#    gmsh.fltk.run()

mesh, ct, fm = gmshio.model_to_mesh(gmsh.model, comm, modelRank, gdim=3)
if mpiRank == modelRank:
    gmsh.finalize()

mesh.topology.create_connectivity(mesh.topology.dim-1, mesh.topology.dim)
#Real function space over boundary tag 2
facetsB = fm.find(2)
Gamma, entity_map1, _, _ = msh.create_submesh(mesh, mesh.topology.dim-1, facetsB)
print(entity_map1)
Z1 = scifem.create_real_functionspace(Gamma, value_shape=(1,))
z1u = U.TrialFunction(Z1)
z1v = U.TestFunction(Z1)

elem = basix.ufl.element('N1curl', mesh.basix_cell(), degree=2)
V = fem.functionspace(mesh, elem)
u = U.TrialFunction(V)
v = U.TestFunction(V)
normal = U.FacetNormal(mesh)
ds = U.Measure("ds", domain=mesh, subdomain_data = fm)
# Dirichlet BCs on boundary tag 1
facets = fm.find(1)
ubc = fem.Function(V)
ubc.interpolate(lambda x: np.array([0*x[0], 0*x[1], 0*x[2]], dtype=np.complex128))
dofs = fem.locate_dofs_topological(V, mesh.topology.dim-1, facets)
bc = fem.dirichletbc(ubc, dofs)
kz = np.sqrt(k0 * k0 - (np.pi / a)**2)
f = ModesTE(kz, k0, a, b, 1, 0)
# Build boundary mode vector
gh = fem.Function(V, dtype=np.complex128)
gh.interpolate(f.eval_H)

t_start = time.perf_counter()
n = U.FacetNormal(mesh)

# Weak form
num_facets_parent = mesh.topology.index_map(mesh.topology.dim-1).size_local + mesh.topology.index_map(mesh.topology.dim-1).num_ghosts
inverse_map = np.full(num_facets_parent, -1, dtype=np.int32)
inverse_map[facetsB] = np.arange(len(facetsB), dtype=np.int32)
L00 = fem.form((U.inner(U.curl(u), U.curl(v)) - k0 * k0 * U.inner(u, v)) * U.dx)
L10 = fem.form(1j * kz * U.inner(U.dot(U.cross(u, gh), n), z1v) * ds(2), entity_maps={Gamma:inverse_map})
L01 = fem.form(1j * kz * U.inner(U.dot(U.cross(v, gh), n), z1u) * ds(2), entity_maps={Gamma:inverse_map})

A = petsc.create_matrix([[L00, L01], [L10, None]], "mpiaij")
petsc.assemble_matrix(A, [[L00, L01], [L10, None]], bcs=[bc])

A.assemble()
sys.exit(0)

I am generating three sub-matrices. L00 is the usual weak form square matrix. L01 and L10 are two forms that use the scifem real space and should each generate an Nx1 and 1xN matrix, respectively. Unfortunately, there is a shape error in building the real space forms. In the interfacing with the GMSH mesh, I am probably missing something in passing the mesh information to the real space definition. This is the error:

  Traceback (most recent call last):
  File "/home/bill/Cluster/Fenics2020/WaveguideBC/Rect2.py", line 154, in <module>
    L10 = fem.form(1j * kz * U.inner(U.dot(u, n), z1v) * ds(2), entity_maps=[entity_map1])
                             ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/bill/spack/var/spack/environments/Fenics-090-complex/.spack-env/view/lib/python3.12/site-packages/ufl/operators.py", line 213, in inner
    return Inner(a, b)
           ^^^^^^^^^^^
  File "/home/bill/spack/var/spack/environments/Fenics-090-complex/.spack-env/view/lib/python3.12/site-packages/ufl/tensoralgebra.py", line 166, in __new__
    raise ValueError(f"Shapes do not match: {ufl_err_str(a)} and {ufl_err_str(b)}")
ValueError: Shapes do not match: <Dot id=125006079896432> and <Argument id=125006079775024>

EDIT 16.55, 2025-11-28: I realized that I posted the wrong code. Updated with the proper entity map lines at the end of the code block.