FacetNormal vector components and director vector

Here is an example where one can visualize the FacetNormal properly:

from mpi4py import MPI
import dolfinx
import basix.ufl
import scifem
import ufl


def move_to_facet_quadrature(ufl_expr, mesh, sub_facets, scheme="default", degree=6):
    fdim = mesh.topology.dim - 1
    # Create submesh
    bndry_mesh, entity_map, _, _ = dolfinx.mesh.create_submesh(mesh, fdim, sub_facets)
    # Create quadrature space on submesh
    q_el = basix.ufl.quadrature_element(
        bndry_mesh.basix_cell(), ufl_expr.ufl_shape, scheme, degree
    )
    Q = dolfinx.fem.functionspace(bndry_mesh, q_el)

    # Compute where to evaluate expression per submesh cell
    integration_entities = dolfinx.fem.compute_integration_domains(
        dolfinx.fem.IntegralType.exterior_facet, mesh.topology, entity_map, fdim
    )
    compiled_expr = dolfinx.fem.Expression(ufl_expr, Q.element.interpolation_points())

    # Evaluate expression
    q = dolfinx.fem.Function(Q)
    q.x.array[:] = compiled_expr.eval(mesh, integration_entities).reshape(-1)
    return q


# Facet expression evaluations

mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)
n = ufl.FacetNormal(mesh)
mesh.topology.create_connectivity(1, 2)
exterior_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
n_h = move_to_facet_quadrature(n, mesh, exterior_facets, degree=2)

with dolfinx.io.XDMFFile(mesh.comm, "mesh.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
with scifem.xdmf.XDMFFile("approximation.xdmf", [n_h]) as xdmf:
    xdmf.write(0.0)


Taking the tangential projection one gets

(i.e. with code)

from mpi4py import MPI
import dolfinx
import basix.ufl
import scifem
import ufl


def move_to_facet_quadrature(ufl_expr, mesh, sub_facets, scheme="default", degree=6):
    fdim = mesh.topology.dim - 1
    # Create submesh
    bndry_mesh, entity_map, _, _ = dolfinx.mesh.create_submesh(mesh, fdim, sub_facets)
    # Create quadrature space on submesh
    q_el = basix.ufl.quadrature_element(
        bndry_mesh.basix_cell(), ufl_expr.ufl_shape, scheme, degree
    )
    Q = dolfinx.fem.functionspace(bndry_mesh, q_el)

    # Compute where to evaluate expression per submesh cell
    integration_entities = dolfinx.fem.compute_integration_domains(
        dolfinx.fem.IntegralType.exterior_facet, mesh.topology, entity_map, fdim
    )
    compiled_expr = dolfinx.fem.Expression(ufl_expr, Q.element.interpolation_points())

    # Evaluate expression
    q = dolfinx.fem.Function(Q)
    q.x.array[:] = compiled_expr.eval(mesh, integration_entities).reshape(-1)
    return q


# Facet expression evaluations

mesh = dolfinx.mesh.create_unit_square(MPI.COMM_WORLD, 10, 10)

V = dolfinx.fem.functionspace(mesh, ("Lagrange", 1, (2,)))
u = dolfinx.fem.Function(V)
u.interpolate(lambda x: (x[0], x[1]))
n = ufl.FacetNormal(mesh)


def tangential_projection(u: ufl.Coefficient, n: ufl.FacetNormal) -> ufl.Coefficient:
    return (ufl.Identity(u.ufl_shape[0]) - ufl.outer(n, n)) * u


u_t = tangential_projection(u, n)

mesh.topology.create_connectivity(1, 2)
exterior_facets = dolfinx.mesh.exterior_facet_indices(mesh.topology)
n_h = move_to_facet_quadrature(u_t, mesh, exterior_facets, degree=2)

with dolfinx.io.XDMFFile(mesh.comm, "mesh.xdmf", "w") as xdmf:
    xdmf.write_mesh(mesh)
with scifem.xdmf.XDMFFile("approximation.xdmf", [n_h]) as xdmf:
    xdmf.write(0.0)