Iterate over all facets on mesh surface to obtain average values for each facet

I have a UnitCubeMesh(2,2,2) and I wish to average the dof spatial coordinates for each facet on the outer surface of the mesh in order to get the coordinates of the center of each facet. From there I also wish to take the three vertices of the triangular facets and determine the normal vector via a cross produce (ie for three vertices (a, b and c, the normal n could be calculated by n = (a-b)x(a-c) where the subtraction is element-wise.)

What I would like to know is, is there a way to be able to average the dof coordinates for each facet on the outer surface of the cubic mesh. Then be able to find the desired normal vectors on each facet via the vertices as described earlier?

Note that this cannot be done via FacetNormal as this produces normal vectors that point away from the center of the mesh (see image).

My current code can produce the dof coordinates and produces the normal vectors via FacetNormal, but this obviously should be changed.

from dolfin import *
import numpy as np
from mpi4py import MPI
import matplotlib.pyplot as plt

mesh = UnitCubeMesh(2,2,2)
Space = FunctionSpace(mesh,'P', 1)
Temperature = project(Expression("x[0]+0.1*x[1] + 0.1*x[2]", degree=3), Space) # Temperature solution across mesh
x_Space = Space.tabulate_dof_coordinates().reshape((-1,mesh.geometry().dim()))
T_values = abs(Temperature.vector().get_local().reshape((-1,1))) # obtain the local temperature values at the correponding dof coordinates 

# OLD METHOD OF OBTAINING NORMAL VECTORS
n = FacetNormal(mesh)
V = VectorFunctionSpace(mesh, "CG", 1)
u = TrialFunction(V)
v = TestFunction(V)
a = inner(u,v)*ds
l = inner(n, v)*ds
A = assemble(a, keep_diagonal=True)
L = assemble(l)

num_dofs_per_component = int(V.dim()/V.num_sub_spaces())
num_sub_spaces = V.num_sub_spaces()
vector = np.zeros((num_sub_spaces, num_dofs_per_component))

A.ident_zeros()
nh = Function(V)

solve(A, nh.vector(), L)
x = V.sub(0).collapse().tabulate_dof_coordinates()
for i in range(num_sub_spaces):
    vector[i] = nh.sub(i, deepcopy=True).vector().get_local()

vector = vector.T
for coord, vec, temp in zip(x_Space, vector, T_values):
    print(coord, vec, temp)

File("nh.pvd") << nh

filename = "output.txt"
data = zip(x_Space, vector, T_values)

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter3D(x_Space[:,0],x_Space[:,1],x_Space[:,2])
ax.quiver(x_Space[:,0],x_Space[:,1],x_Space[:,2], vector[:,0],vector[:,1],vector[:,2], length=0.1, normalize=True)

ax.set_xlim(-0.1,1.1)
ax.set_ylim(-0.1,1.1)
ax.set_zlim(-0.1,1.1)
plt.savefig('sourceplot.pdf', bbox_inches='tight', format='pdf')
plt.savefig('sourceplot.png', dpi=300, bbox_inches='tight', format='png')

The plot below shows the current location of the dof coordinates that I would like to average over for each facet and the incorrect normal vectors output by FacetNormal

Hi @CoE2845,

I think the FacetNormals are averaged over all facets that each node is part of. So only for nodes that are not on the boundary of the facet, the normals are the facet normals. If you increase the number of cells of your mesh, more and more of your arrows should truly be normal to the each facet, and only normals on edges and at vertices are not perpendicular to the box.

Maybe you can adapt some of the code from How to get average facet normals for each node? to fit your needs.

Note that the normal vector of a facet is constant over the entire surface of the facet.

Hmm I see, from looking at the code you linked to, I am unsure how you would define the boundaries of your mesh, within the line:

surfacefacetindices = np.where(boundaries.array() == tag)[0]

If you create your mesh with gmsh, you can define the boundaries there and convert the msh file with dolfin-convert to xml files, including a _facet_region.xml. This file can then be loaded, e.g. via

boundaries = MeshFunction('size_t', mesh, facetxml)

where facetxml is the name of the _facet_region.xml file.

If you create your mesh in FEniCS with e.g. UnitCubeMesh, you have to create the boundaries there, too. The FEniCS documentation should give you an idea on how to mark surfaces as boundaries by giving geometric conditions on what counts as a boundary. See for example Marking subdomains of a mesh.

[Edit: Fixed typos.]

What about tag? I keep obtaining an error where:

File "outputtext_v2.py", line 53, in <module>
surfacefacetindices = np.where(boundaries.array() == tag)[0]
NameError: name 'tag' is not defined

boundaries is just a MeshFunction, that basically tells you the tag (which is an integer) of each facet of the mesh. You can inspect boundaries by printing boundaries.array(). You will see that len(boundaries) equals the number of facets of your mesh, i.e. mesh.num_facets(). The tag is just a number that you assign to the facet. That way, you can select only a subset of the facets.

Note: The same goes for markers when tagging cells instead of facets: Here, len(markers) equals mesh.num_cells(), where markers is a MeshFunction as well (that might be loaded from a _physical_region.xml file, or applied geometrically).

The expression np.where(boundaries.array() == tag) returns the list of indices of boundaries.array(), where the list value is tag. That is convenient because one can then later iterate over those facets and get, for example, their coordinates.

OK, so I would need to initialise the value of tag beforehand? such as simply saying:

tag = 0
surfacefacetindices = np.where(boundaries.array() == tag)[0]

That would select all facets, since the MeshFunction (per default) sets all tags to 0, i.e. boundaries.array() per default contains only zeros.

To filter out only some facets, you would set some values of boundaries to the tag you want, say, 5, then get the surfacefacetindices via

surfacefacetindices = np.where(boundaries.array() == 5)[0]

Take a look at, for example, Sections 4.3.2 and 4.4.4 for details on how to mark domain boundaries.

Ah I see, that makes sense, given that in my original .msh file. I gave the surface of my mesh a tag of ‘2’, meaning that my boundaries.array() is simply made up of 2’s and 0’s.

However, I am still obtaining issues with the three functions you built for your code:

def get_dofcoords_from_dof_index(V, dofs):
	doftab = V.tabulate_dof_coordinates()
	dof2coords = dict()
	for dof in dofs:
		coords = list(doftab[dof])
		if not coords in dof2coords.values():
			dof2coords[dof] = coords
	for dof, coords in dof2coords.items():
		dof2coords[dof] = np.array(coords)
	return dof2coords

def get_facet_index_from_dof_index(V, mesh, facetindices, dimension=2):
	dof2facet = defaultdict(set)
	for facetindex in facetindices:
		dofs = V.dofmap().entity_closure_dofs(mesh, dimension, [facetindex])
		for dof in dofs:
			dof2facet[dof].add(facetindex)
	return dof2facet

def get_average_normal_vector_from_dof(dofs, dof2facet, mesh):
	dof2normal = dict()
	all_facets = np.array(list(facets(mesh)))
	for dof in dofs:
		doffacets = np.array(list(dof2facet[dof]))
	average_normal_vector = np.array([[facet.normal().x(), facet.normal().y(), facet.normal().z()] for facet in all_facets[doffacets]]).mean(axis=0)
	average_normal_vector /= np.linalg.norm(average_normal_vector)
	dof2normal[dof] = average_normal_vector
	return dof2normal

Specifically, there is an issue with get_facet_index_from_dof_index:

  File "outputtext_v2.py", line 58, in <module>
    dofs = V.dofmap().entity_closure_dofs(mesh, dimension, surfacefacetindices)
NameError: name 'V' is not defined

That’s strange, since you seem to have provided V as the first argument of get_facet_index_from_dof_index(), since your error is thrown inside this function (and not when calling it). V should be a dolfin.function.functionspace.FunctionSpace type object, namely the FunctionSpace of the whatever entity you want the dofs for.

Yes, I agree. Quite odd. Here is the code I currently have in case this maybe of any help:

from dolfin import *
import numpy as np
from mpi4py import MPI
import matplotlib.pyplot as plt



data = 'box'
mesh = Mesh()
hdf = HDF5File(mesh.mpi_comm(), data+'.h5', 'r')
hdf.read(mesh, '/mesh', False)
cells = MeshFunction('size_t', mesh, 3)
hdf.read(cells, '/cells')
facets = MeshFunction('size_t', mesh, 2)
hdf.read(facets, '/facets')

boundaries = MeshFunction('size_t', mesh, 'box_facet_region.xml') 
print(boundaries.array())


Space = FunctionSpace(mesh,'P', 1)
Temperature = project(Expression("x[0]+0.1*x[1] + 0.1*x[2]", degree=3), Space) # Temperature solution across mesh
x_Space = Space.tabulate_dof_coordinates().reshape((-1,mesh.geometry().dim()))
T_values = abs(Temperature.vector().get_local().reshape((-1,1))) # obtain the local temperature values at the correponding dof coordinates 
#-------------------------------------
def get_dofcoords_from_dof_index(V, dofs):
	doftab = V.tabulate_dof_coordinates()
	dof2coords = dict()
	for dof in dofs:
		coords = list(doftab[dof])
		if not coords in dof2coords.values():
			dof2coords[dof] = coords
	for dof, coords in dof2coords.items():
		dof2coords[dof] = np.array(coords)
	return dof2coords

def get_facet_index_from_dof_index(V, mesh, facetindices, dimension=2):
	dof2facet = defaultdict(set)
	for facetindex in facetindices:
		dofs = V.dofmap().entity_closure_dofs(mesh, dimension, [facetindex])
		for dof in dofs:
			dof2facet[dof].add(facetindex)
	return dof2facet

def get_average_normal_vector_from_dof(dofs, dof2facet, mesh):
	dof2normal = dict()
	all_facets = np.array(list(facets(mesh)))
	for dof in dofs:
		doffacets = np.array(list(dof2facet[dof]))
	average_normal_vector = np.array([[facet.normal().x(), facet.normal().y(), facet.normal().z()] for facet in all_facets[doffacets]]).mean(axis=0)
	average_normal_vector /= np.linalg.norm(average_normal_vector)
	dof2normal[dof] = average_normal_vector
	return dof2normal

#-------------------------------------
tag = 2
surfacefacetindices = np.where(boundaries.array() == tag)[0]
dofs = V.dofmap().entity_closure_dofs(mesh, dimension, surfacefacetindices)
dof2coords = get_dofcoords_from_dof_index(V, list(set(dofs)))
uniquedofs = dof2coords.keys()
dof2facet = get_facet_index_from_dof_index(V, mesh, surfacefacetindices)
dof2normal = get_average_normal_vector_from_dof(uniquedofs, dof2facet, mesh)

vector = dof2normal
vector = vector.T
print(vector)


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.scatter3D(x_Space[:,0],x_Space[:,1],x_Space[:,2])
ax.quiver(x_Space[:,0],x_Space[:,1],x_Space[:,2], vector[:,0],vector[:,1],vector[:,2], length=0.1, normalize=True)

ax.set_xlim(-0.1,1.1)
ax.set_ylim(-0.1,1.1)
ax.set_zlim(-0.1,1.1)
plt.savefig('sourceplot.pdf', bbox_inches='tight', format='pdf')
plt.savefig('sourceplot.png', dpi=300, bbox_inches='tight', format='png')

As the error message correctly identifies, V is not defined anywhere in your code. This is a type of error that I think you should be able to spot yourself before posting.

I see, that has been added. My apologies for that as Fenics is really not my strong suit. Another question for you, how would you define your facets function compared to what I have done here? Currently, there seems to be an issue with calling get_average_normal_vector_from_dof, specifically I obtain:

  File "outputtext_v2.py", line 62, in <module>
    dof2normal = get_average_normal_vector_from_dof(uniquedofs, dof2facet, mesh)
  File "outputtext_v2.py", line 47, in get_average_normal_vector_from_dof
    all_facets = np.array(list(facets(mesh)))
TypeError: 'dolfin.cpp.mesh.MeshFunctionSizet' object is not callable

It took me a lot of try and error, too.

Your error is caused by a name clash: from dolfin import * imports a function called facets(), a name which you redefined with facets = MeshFunction… to be a MeshFunction. You can fix this by renaming your variable. Then, the dolfin function facets() can be called as expected.

Great! That works as expected! I am now trying to try and output a list of the normal vectors and their corresponding coordinates (which I presume would just be dof2coords). I have this currently:

tag = 2
surfacefacetindices = np.where(boundaries.array() == tag)[0]
dofs = V.dofmap().entity_closure_dofs(mesh, 2, surfacefacetindices)
dof2coords = get_dofcoords_from_dof_index(V, list(set(dofs)))
uniquedofs = dof2coords.keys()
dof2facet = get_facet_index_from_dof_index(V, mesh, surfacefacetindices)
dof2normal = get_average_normal_vector_from_dof(uniquedofs, dof2facet, mesh)

vector = dof2normal
print(vector)

But this simply prints:

{421: array([0.57735027, 0.57735027, 0.57735027])}

Which I presume is the index of the facet followed by the normal vector of that facet. Is there a way to be able to output the coordinates and the normal vectors as a list for all the facets, such that I can create a figure as shown in the original question?

Again, thank you so much for your help so far!

The code snippets above calculate averaged normals at the dofs (averaging is necessary since a dof is part of multiple facets, so I wanted to get the average dof-normal).

You just want the facet normals, which is way easier:
Filter out the facets of interest, as you have already with surfacefacetindices. Then just get the midpoint coordinates and normals from the facets directly:

mesh = ... # you've already got that (mesh.xml or otherwise)
boundaries = ... # you've already got that (*_facet_region.xml or otherwise)
tag = ... # you've already got that (from the mesh file or otherwise)
surfacefacetindices = np.where(boundaries.array() == tag)[0]
surfacefacets = np.array(facets(mesh))[surfacefacetindices]
for f in surfacefacets:
    print('midpoint: {}, normal: {}'.format(f.midpoint().array(), f.normal().array()))

:slight_smile: