Source code for cashocs.geometry.mesh_testing
# Copyright (C) 2020-2026 Fraunhofer ITWM and Sebastian Blauth
#
# This file is part of cashocs.
#
# cashocs is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# cashocs is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cashocs. If not, see <https://www.gnu.org/licenses/>.
"""Testing of mesh quality."""
from __future__ import annotations
import collections
from typing import TYPE_CHECKING
import fenics
import numpy as np
try:
import ufl_legacy as ufl
except ImportError:
import ufl
from cashocs import _utils
from cashocs import log
if TYPE_CHECKING:
from cashocs import _typing
[docs]
class APrioriMeshTester:
"""A class for testing the mesh before it is modified."""
def __init__(self, mesh: fenics.Mesh) -> None:
"""Initializes the mesh tester.
Args:
mesh: The mesh that is to be tested.
"""
self.mesh = mesh
self.comm = self.mesh.mpi_comm()
dg_function_space = fenics.FunctionSpace(self.mesh, "DG", 0)
vector_cg_space = fenics.VectorFunctionSpace(self.mesh, "CG", 1)
dx = ufl.Measure("dx", domain=mesh)
self.transformation_container = fenics.Function(vector_cg_space)
self.determinant_function = fenics.Function(dg_function_space)
# pylint: disable=invalid-name
self.A_prior = (
fenics.TrialFunction(dg_function_space)
* fenics.TestFunction(dg_function_space)
* dx
)
self.l_prior = (
ufl.det(
ufl.Identity(self.mesh.geometric_dimension())
+ ufl.grad(self.transformation_container)
)
* fenics.TestFunction(dg_function_space)
* dx
)
self.options_prior: _typing.KspOption = {
"ksp_type": "preonly",
"pc_type": "jacobi",
"pc_jacobi_type": "diagonal",
"ksp_rtol": 1e-16,
"ksp_atol": 1e-20,
"ksp_max_it": 1000,
}
[docs]
@log.profile_execution_time("testing the volume change restrictions")
def test(self, transformation: fenics.Function, volume_change: float) -> bool:
r"""Check the quality of the transformation before the actual mesh is moved.
Checks the quality of the transformation. The criterion is that
.. math:: \det(I + D \texttt{transformation})
should neither be too large nor too small in order to achieve the best
transformations.
Args:
transformation: The transformation for the mesh.
volume_change: The allowed factor that each element is allowed to change in
volume.
Returns:
A boolean that indicates whether the desired transformation is feasible.
"""
self.transformation_container.vector().vec().aypx(
0.0, transformation.vector().vec()
)
self.transformation_container.vector().apply("")
_utils.assemble_and_solve_linear(
self.A_prior,
self.l_prior,
self.determinant_function,
ksp_options=self.options_prior,
)
min_det = float(self.determinant_function.vector().vec().min()[1])
max_det = float(self.determinant_function.vector().vec().max()[1])
result = bool((min_det >= 1 / volume_change) and (max_det <= volume_change))
if result:
log.debug(
"Accepting the deformation as all mesh elements satisfy the volume "
"change restrictions. There are no inverted cells are in the mesh."
)
else:
log.debug(
"Rejecting the deformation as the mesh does not satisfy the volume "
"change restrictions."
)
if min_det < 0.0:
log.debug("There are inverted cells in the mesh.")
return result
[docs]
class IntersectionTester:
"""A class for testing the mesh after it has been modified."""
def __init__(self, mesh: fenics.Mesh) -> None:
"""Initializes the posterior mesh tester.
Args:
mesh: The mesh that is to be tested.
"""
self.mesh = mesh
self.comm = self.mesh.mpi_comm()
cells = self.mesh.cells()
vertex_ghost_offset = self.mesh.topology().ghost_offset(0)
cell_ghost_offset = self.mesh.topology().ghost_offset(
self.mesh.topology().dim()
)
flat_cells = cells[:cell_ghost_offset].flatten().tolist()
self.cell_counter: collections.Counter = collections.Counter(flat_cells)
self.occurrences = np.array(
[self.cell_counter[i] for i in range(vertex_ghost_offset)]
)
[docs]
@log.profile_execution_time("testing for self-intersecting mesh cells")
def test(self) -> bool:
"""Checks the quality of the transformation after the actual mesh is moved.
Checks whether the mesh is a valid finite element mesh
after it has been moved, i.e., if there are no overlapping
or self intersecting elements.
Returns:
True if the test is successful, False otherwise.
Notes:
fenics itself does not check whether the used mesh is a valid finite
element mesh, so this check has to be done manually.
"""
self_intersections = False
collisions = CollisionCounter.compute_collisions(self.mesh)
if not (collisions == self.occurrences).all():
self_intersections = True
list_self_intersections = self.comm.allgather(self_intersections)
if any(list_self_intersections):
log.debug(
"Rejecting the deformation as the mesh has self-intersecting elements "
"after the update."
)
result = False
else:
log.debug(
"Accepting the deformation as the mesh does not have self-intersecting "
"elements after the update."
)
result = True
return result
[docs]
class CollisionCounter:
"""Class for testing, whether a given mesh is a valid FEM mesh."""
_cpp_code = """
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <pybind11/stl.h>
namespace py = pybind11;
#include <dolfin/mesh/Mesh.h>
#include <dolfin/mesh/Vertex.h>
#include <dolfin/geometry/BoundingBoxTree.h>
#include <dolfin/geometry/Point.h>
using namespace dolfin;
Eigen::VectorXi
compute_collisions(std::shared_ptr<const Mesh> mesh)
{
int num_vertices;
std::vector<unsigned int> colliding_cells;
num_vertices = mesh->num_vertices();
Eigen::VectorXi collisions(num_vertices);
int i = 0;
for (VertexIterator v(*mesh); !v.end(); ++v)
{
colliding_cells = mesh->bounding_box_tree()->compute_entity_collisions(
v->point()
);
collisions[i] = colliding_cells.size();
++i;
}
return collisions;
}
PYBIND11_MODULE(SIGNATURE, m)
{
m.def("compute_collisions", &compute_collisions);
}
"""
_cpp_object = fenics.compile_cpp_code(_cpp_code)
def __init__(self) -> None:
"""Initializes self."""
pass
[docs]
@classmethod
def compute_collisions(cls, mesh: fenics.Mesh) -> np.ndarray:
"""Computes the cells which (potentially) contain self intersections.
Args:
mesh: A FEM mesh.
Returns:
An array of cell indices, where ``array[i]`` contains the indices of all
cells that vertex ``i`` collides with.
"""
vertex_ghost_offset = mesh.topology().ghost_offset(0)
collisions: np.ndarray = cls._cpp_object.compute_collisions(mesh)
collisions = collisions[:vertex_ghost_offset]
return collisions