I skimmed over the discourse but could not pick the most proper and easiest way of getting normal vector field projected on a CG-1 space. All I want is;
I managed to get it working; Here is the function that takes function space and returns the facet normal field.
# Copyright (C) 2023 Jørgen S. Dokken and Halvor Herlyng
#
# SPDX-License-Identifier: MIT
import dolfinx as dfx
from petsc4py import PETSc
import ufl
from mpi4py import MPI
from dolfinx.fem.petsc import assemble_matrix, assemble_vector, apply_lifting, set_bc
from dolfinx.cpp.fem.petsc import insert_diagonal
def facet_vector_approximation(V: dfx.fem.FunctionSpace,
mt: dfx.mesh.MeshTags = None,
jit_options: dict = None,
form_compiler_options: dict = None) -> dfx.fem.Function:
"""
Approximate the facet normal or tangent on a surface by projecting it into the function space for a set of facets.
Args:
V: The function space to project into. Needs to be defined on discontinuous elements.
mt: The `dolfinx.mesh.MeshTags` containing facet markers.
mt_id: The id for the facets in `mt` we want to represent the normal or tangent on.
tangent: To approximate the tangent of the facets set this flag to `True`.
jit_options: Parameters used in CFFI JIT compilation of C code generated by FFCx.
See https://github.com/FEniCS/dolfinx/blob/main/python/dolfinx/jit.py#L22-L37
for all available parameters. Takes priority over all other parameter values.
form_compiler_options: Parameters used in FFCx compilation of this form. Run `ffcx - -help` at
the commandline to see all available options. Takes priority over all
other parameter values, except for `scalar_type` which is determined by
DOLFINx.
"""
# if not V.element.basix_element.discontinuous:
# raise RuntimeError("Discontinuous elements are required for a proper representation of the facet vectors.")
jit_options = jit_options if jit_options is not None else {}
form_compiler_options = form_compiler_options if form_compiler_options is not None else {}
comm = V.mesh.comm # MPI Communicator
n = ufl.FacetNormal(V.mesh) # UFL representation of mesh facet normal
nh = dfx.fem.Function(V) # Function for the facet vector approximation
u, v = ufl.TrialFunction(V), ufl.TestFunction(V) # Trial and test functions
#
# Define integral measure to create exterior facet
ds = ufl.ds(domain=V.mesh , subdomain_data=mt)
a = ufl.inner(u, v) * ds
L = ufl.inner(n, v) * ds
# Find all boundary dofs, which are the dofs where we want to solve for the facet vector approximation.
# Start by assembling test functions integrated over the boundary integral measure.
ones = dfx.fem.Constant(V.mesh, dfx.default_scalar_type((1,) * V.mesh.geometry.dim)) # A vector of ones
local_val = dfx.fem.form(ufl.inner(ones, ufl.TestFunction(V))*ds)
local_vec = dfx.fem.assemble_vector(local_val)
# For the dofs that do not lie on the boundary of the mesh the assembled vector has value zero.
# Extract these dofs and use them to deactivate the corresponding block in the linear system we will solve.
bdry_dofs_zero_val = np.flatnonzero(np.isclose(local_vec.array, 0))
deac_blocks = np.unique(bdry_dofs_zero_val // V.dofmap.bs).astype(np.int32)
# Create sparsity pattern by manipulating the blocks to be deactivated and set
# a zero Dirichlet boundary condition for these dofs.
bilinear_form = dfx.fem.form(a, jit_options=jit_options,
form_compiler_options=form_compiler_options)
pattern = dfx.fem.create_sparsity_pattern(bilinear_form)
pattern.insert_diagonal(deac_blocks)
pattern.finalize()
u_0 = dfx.fem.Function(V)
u_0.vector.set(0)
bc_deac = dfx.fem.dirichletbc(u_0, deac_blocks)
# Create the matrix
A = dfx.cpp.la.petsc.create_matrix(comm, pattern)
A.zeroEntries()
# Assemble the matrix with all entries
form_coeffs = dfx.cpp.fem.pack_coefficients(bilinear_form._cpp_object)
form_consts = dfx.cpp.fem.pack_constants(bilinear_form._cpp_object)
assemble_matrix(A, bilinear_form, constants=form_consts, coeffs=form_coeffs, bcs=[bc_deac])
# Insert the diagonal with the deactivated blocks.
if bilinear_form.function_spaces[0] is bilinear_form.function_spaces[1]:
A.assemblyBegin(PETSc.Mat.AssemblyType.FLUSH)
A.assemblyEnd(PETSc.Mat.AssemblyType.FLUSH)
insert_diagonal(A=A, V=bilinear_form.function_spaces[0], bcs=[bc_deac._cpp_object], diagonal=1.0)
A.assemble()
# Assemble the linear form and the right-hand side vector.
linear_form = dfx.fem.form(L, jit_options=jit_options,
form_compiler_options=form_compiler_options)
b = assemble_vector(linear_form)
# Apply lifting to the right-hand side vector and set boundary conditions.
apply_lifting(b, [bilinear_form], [[bc_deac]])
b.ghostUpdate(addv=PETSc.InsertMode.ADD_VALUES, mode=PETSc.ScatterMode.REVERSE)
set_bc(b, [bc_deac])
# Setup a linear solver using the Conjugate Gradient method.
solver = PETSc.KSP().create(MPI.COMM_WORLD)
solver.setType("cg")
solver.rtol = 1e-12
solver.setOperators(A)
# Solve the linear system and perform ghost update.
solver.solve(b, nh.vector)
nh.vector.ghostUpdate(addv=PETSc.InsertMode.INSERT, mode=PETSc.ScatterMode.FORWARD)
# print(np.unique(nh.vector[:]))
# Normalize the vectors to get the unit facet normal/tangent vector.
nh_norm = ufl.sqrt(ufl.inner(nh, nh)) # Norm of facet vector
if V.mesh.geometry.dim == 1:
nh_norm_vec = ufl.as_vector((nh[0]/nh_norm))
elif V.mesh.geometry.dim == 2:
nh_norm_vec = ufl.as_vector((nh[0]/nh_norm, nh[1]/nh_norm))
elif V.mesh.geometry.dim == 3:
nh_norm_vec = ufl.as_vector((nh[0]/nh_norm, nh[1]/nh_norm, nh[2]/nh_norm))
nh_normalized = dfx.fem.Expression(nh_norm_vec, V.element.interpolation_points())
n_out = dfx.fem.Function(V)
n_out.interpolate(nh_normalized)
return n_out