Maxwell equation producing odd results --- fact check?

Hi all, I’m new to FEniCS, and as a first working example I wanted to simulate the reflection of a plane wave off a flat surface. I begin with a box-shaped Mesh with MeshTags indicating the reflective surface at y = 0, and the remaining boundaries. I define a TM plane wave function E_b coming in at a 45-degree angle. I want FEniCS to compute the ‘scattered’ function E_s, which I expect to be a plane wave travelling in the perpendicular direction.

With E = E_b + E_s, I use the double-curl equation
\qquad \nabla \times \nabla \times E - k^2 E = 0 \text{.}
At y = 0, I impose the boundary condition
\qquad n \times E = 0 \text{,}
while at the remaining boundaries, I impose the first-order absorbing boundary condition
\qquad n \times (\nabla \times E) + j k n \times (E \times n) = 0 \text{.}
I dump these equations straight into the code, as

a = ufl.inner(ufl.curl(ufl.curl(E_s)) - k**2 * E_s, v) * dx \
    + ufl.inner(ufl.cross(n, E_s), v) * ds_surface \
    + ufl.inner(ufl.cross(n, ufl.curl(E_s)) + 1j * k * ufl.cross(n, ufl.cross(E_s, n)), v) * ds_boundary

L = - ufl.inner(ufl.cross(n, E_b), v) * ds_surface

where

V = FunctionSpace(mesh, FiniteElement("N1curl", mesh.ufl_cell(), 3))
E_s = TrialFunction(V)
v = TestFunction(V)
dx = Measure("dx", my_mesh)
ds = Measure("ds", my_mesh, subdomain_data = facet_tags)
ds_surface = ds(1)
ds_boundary = ds(2)
n = FacetNormal(my_mesh)

I solve a == L as a LinearProblem, and the computation goes through without error, however the output of E_s is not remotely what I’m expecting.

Since I’m new to FEniCS (this is my first code snippet), I expect that I’m making a basic mistake in my setup. Does anyone have any suggestions?

Edit: Full code for completeness. I believe I’m using the dolfinx/lab:stable Docker image for running the code. The Git commit has for dolfinx is 24f86a9ce57df6978070dbee22b3eae8bb77235f.

import numpy as np
import pyvista as pv
import ufl
from dolfinx import fem, io, mesh, plot
from mpi4py import MPI
from petsc4py import PETSc

# Create box mesh
my_mesh = mesh.create_box(
    MPI.COMM_WORLD,
    [[0, 0, 0], [1, 1, 1]],
    [10, 10, 1] # z-direction is irrelevant for now
)

boundaries = [(1, lambda x: np.isclose(x[1], 0)), # Reflective boundary
              (2, lambda x: np.isclose(x[0], 1)) or np.isclose(x[0], 0) or np.isclose(x[1], 1) or np.isclose(x[2], 0) or np.isclose(x[2], 1)]

# Create tags for both boundaries
facet_indices, facet_markers = [], []
fdim = my_mesh.topology.dim - 1
for (marker, locator) in boundaries:
    facets = mesh.locate_entities(my_mesh, fdim, locator)
    facet_indices.append(facets)
    facet_markers.append(np.full_like(facets, marker))
facet_indices = np.hstack(facet_indices).astype(np.int32)
facet_markers = np.hstack(facet_markers).astype(np.int32)
sorted_facets = np.argsort(facet_indices)
facet_tags = mesh.meshtags(my_mesh, fdim, facet_indices[sorted_facets], facet_markers[sorted_facets])

class BackgroundElectricField:
    def __init__(self, theta: float, n_bkg: float, k0: complex):
        self.theta = theta  # Angle with respect to the x-axis!
        self.n_bkg = n_bkg  # Background refractive index
        self.kx = self.n_bkg * k0 * np.cos(self.theta) # x wavevector
        self.ky = self.n_bkg * k0 * np.sin(self.theta)

    def eval(self, x):
        phi = self.kx * x[0] + self.ky * x[1]
        ax, ay = np.sin(self.theta), np.cos(self.theta)
        return (-ax * np.exp(1j * phi), ay * np.exp(1j * phi), np.zeros(x[2].shape))

curl_el = ufl.FiniteElement("N1curl", my_mesh.ufl_cell(), 3) # ufl_cell or basix_cell depending on version
V = fem.FunctionSpace(my_mesh, curl_el)

theta = 1.75 * np.pi  # Angle of incidence of the background field --- I think this is pointing to the down & right
n_bkg = 1.33  # Background refractive index
wavelength = 0.3 # Wavelength of the background field.
k = 2 * np.pi / wavelength

f = BackgroundElectricField(theta, n_bkg, k)
E_b = fem.Function(V)
E_b.interpolate(f.eval)

E_s = ufl.TrialFunction(V)
v = ufl.TestFunction(V)

dx = ufl.Measure("dx", my_mesh)
ds = ufl.Measure("ds", my_mesh, subdomain_data = facet_tags)
ds_surface = ds(1)
ds_boundary = ds(2)

n = ufl.FacetNormal(my_mesh)

# Maxwell equations
a = ufl.inner(ufl.curl(ufl.curl(E_s)) - k**2 * E_s, v) * dx \
    + ufl.inner(ufl.cross(n, E_s), v) * ds_surface \
    + ufl.inner(ufl.cross(n, ufl.curl(E_s)) + 1j * k * ufl.cross(n, ufl.cross(E_s, n)), v) * ds_boundary
L = ufl.inner(ufl.cross(n, E_b), v) * ds_surface

# Solve problem
problem = fem.petsc.LinearProblem(a, L, bcs=[], petsc_options={"ksp_type": "preonly", "pc_type": "lu"})
E_s = problem.solve()

# Plot results
V_dg = fem.VectorFunctionSpace(my_mesh, ("Discontinuous Lagrange", 3))
E_dg = fem.Function(V_dg)
E_dg.interpolate(E_s)

pv.start_xvfb()
plotter = pv.Plotter()
plotter.set_position([0, 0, 0])
topology, cell_types, geometry = plot.create_vtk_mesh(V_dg)
grid = pv.UnstructuredGrid(topology, cell_types, geometry)
E_values = np.zeros((geometry.shape[0], 3), dtype = np.float64)
E_values[:, : my_mesh.topology.dim] = E_dg.x.array.reshape(geometry.shape[0], my_mesh.topology.dim).real # Discard imaginary parts for plotting
grid.point_data["E_sh"] = E_values
plotter = pv.Plotter()
plotter.add_mesh(grid.copy(), show_edges=True)
plotter.view_xy()
plotter.link_views()
plotter.show(jupyter_backend = 'panel')

You should add your complete code, including how you are creating the post processing, such that one can replicate the results you are observing.

Apologies, I didn’t want to make my post too long. Will do!

You could also have a look at @jpdean’s Maxwell code: GitHub - jpdean/maxwell