Extract facet markers from netgen mesh

Hi all,

I’m trying to extract a set of tagged facets from a 2D mesh.
I’ve created the following script:

try:
    import netgen.geom2d
except ModuleNotFoundError:
    print("This example requires the netgen-mesher module to be installed.")
    exit(1)
import numpy as np


geo = netgen.geom2d.CSG2d()
poly = netgen.geom2d.Solid2d(
    [
        (0, 0),
        netgen.geom2d.EdgeInfo(bc="bottom"),
        (2, 0),
        netgen.geom2d.EdgeInfo(bc="right"),
        (2, 2),
        netgen.geom2d.EdgeInfo(bc="top"),
        (0, 2),
        netgen.geom2d.EdgeInfo(bc="left"),
    ]
)

geo.Add(poly)
ngmesh = geo.GenerateMesh(maxh=2)

ngmesh.Export("tmp.mesh", "Medit Format")
regions: dict[str : list[int]] = {}
for region in range(len(ngmesh.GetRegionNames(codim=1))):
    if (name := ngmesh.GetBCName(region)) in regions.keys():
        regions[name].append(region + 1)
    else:
        regions[name] = [region + 1]
print(f"{regions=}")

I then in turn try to create a relationship between he edge-info string, and the indices (integer marker of the edges).
I am not able to understand the relationship between them, as regions prints:

regions={'bottom': [1], 'right': [2], 'top': [3], 'left': [4]}

resulting in the mesh


Further inspection of the facets/edges yield

ng_facets = ngmesh.Elements1D()
facet_indices = ng_facets.NumPy()["nodes"].astype(np.int64)
facets = (
    np.array(
        [list(np.trim_zeros(a, "b")) for a in list(facet_indices)],
        dtype=np.int64,
    )
    - 1
)
facet_values = ng_facets.NumPy()["index"].astype(np.int32)
print("Unique facet values:", np.unique(facet_values))

with output

Unique facet values: [1 2 3 4]

but then mapping the facets to its coordinates, reveal a mismatch:

x = ngmesh.Coordinates()

for region, indices in regions.items():
    print(f"{region=}")
    for index in indices:
        facet_positions = np.flatnonzero(facet_values == index)
        print(f"{x[facets[facet_positions]]=}")

returns

region='bottom'
x[facets[facet_positions]]=array([[[0., 0.],
        [2., 0.]]])
region='right'
x[facets[facet_positions]]=array([[[0., 0.],
        [0., 2.]]])
region='top'
x[facets[facet_positions]]=array([[[2., 0.],
        [2., 2.]]])
region='left'
x[facets[facet_positions]]=array([[[2., 2.],
        [0., 2.]]])

where we see that the bottom facet is marked correctly, but that the right facet maps to what should be the left facet, top should be right, and left should be top.

Could anyone shed some light on this mysterious indexing?

The figure of the mesh above is created by converting the mesh to medit and read through meshio to convert to vtk, where the indexing is even stranger (goes from 2->5):
while converting the meshio to Medit and then to vtk, results in the following markers:

import meshio

mesh = meshio.read("tmp.mesh")


def create_mesh(mesh, cell_type, prune_z=False):
    cells = mesh.get_cells_type(cell_type)

    cell_data = mesh.get_cell_data("medit:ref", cell_type)
    points = mesh.points[:, :2] if prune_z else mesh.points
    out_mesh = meshio.Mesh(
        points=points, cells={cell_type: cells}, cell_data={"name_to_read": [cell_data]}
    )
    return out_mesh

triangle_mesh = create_mesh(mesh, "triangle")
line_mesh = create_mesh(mesh, "line")
meshio.write("facets.vtk", line_mesh)
meshio.write("cells.vtk", triangle_mesh)

Hi Jørgen,
I agree, there is an inconsistency, which should be fixed. From

for el in ngmesh.Elements1D():
    print (el.edgenr, el.index, el.vertices)
    print (ngmesh.Elements1D().NumPy()["index"])

I get

1 1 [1, 2]
2 4 [1, 3]
3 2 [2, 4]
4 3 [4, 3]
[1 2 3 4]

el.index is the number you want to see, but you get ‘el.edgenr’.
Every 1D element has two numbers, one for the label index, and one for the geometric entity - and numpy[“index”] seems to return the other one.

However, the highly recommended and only actively developed Netgen geometry format is OpenCascade. And there, index and edgenr contain the same index value, so you should not run into that problem.

best, Joachim

Hi Joachim,
Thanks for your suggested solution.
I’ll iterate over the el.index for now, as I am planning to make an interface between netgen meshes and DOLFINx (and I can’t force people to use the occ backend).

Not sure if it was medit, but we had a similar thing with meshio where the format only supported one index set. So it might be that the volume is nr 1 and then the edges are 2-5

Also have a look at mhochstegers meshio fork as he had some pull requests open to meshio as far as i remember but meshio seems abandoned / currently not maintained

Best
Christopher

No worries, I don’t need meshio for the extraction as long as I have a way to extract the correct indices.
This is my current (unpolished) interface to DOLFINx:

def create_crack_mesh(comm, max_res: float = 0.05):
    geo = netgen.geom2d.CSG2d()
    poly = netgen.geom2d.Solid2d(
        [
            (0, 0),
            netgen.geom2d.EdgeInfo(bc="bottom"),
            (2, 0),
            netgen.geom2d.EdgeInfo(bc="right"),
            (2, 2),
            netgen.geom2d.EdgeInfo(bc="topright"),
            (1.01, 2),
            netgen.geom2d.EdgeInfo(bc="crackright"),
            (1, 1.5),
            netgen.geom2d.EdgeInfo(bc="crackleft"),
            (0.99, 2),
            netgen.geom2d.EdgeInfo(bc="topleft"),
            (0, 2),
            netgen.geom2d.EdgeInfo(bc="left"),
        ]
    )

    disk = netgen.geom2d.Circle((0.3, 0.3), 0.2, bc="hole")
    geo.Add(poly - disk)
    if comm.rank == 0:
        ngmesh = geo.GenerateMesh(maxh=max_res)
        x = ngmesh.Coordinates()
        ng_elements = ngmesh.Elements2D()
        cell_indices = ng_elements.NumPy()["nodes"]
        if Version(np.__version__) >= Version("2.2"):
            cells = np.trim_zeros(cell_indices, "b", axis=1).astype(np.int64) - 1
        else:
            cells = (
                np.array(
                    [list(np.trim_zeros(a, "b")) for a in list(cell_indices)],
                    dtype=np.int64,
                )
                - 1
            )
    else:
        x = np.zeros((0, 2))
        cells = np.zeros((0, 3), dtype=np.int64)

    MPI.COMM_WORLD.barrier()

    ud = ufl.Mesh(basix.ufl.element("Lagrange", "triangle", 1, shape=(2,)))
    linear_mesh = dolfinx.mesh.create_mesh(MPI.COMM_WORLD, cells, x, ud)

    if comm.rank == 0:
        regions: dict[str, list[int]] = {name: [] for name in ngmesh.GetRegionNames(codim=1)}
        for i, name in enumerate(ngmesh.GetRegionNames(codim=1), 1):
            regions[name].append(i)
        ng_facets = ngmesh.Elements1D()
        facet_indices = ng_facets.NumPy()["nodes"].astype(np.int64)
        if Version(np.__version__) >= Version("2.2"):
            facets = np.trim_zeros(facet_indices, "b", axis=1).astype(np.int64) - 1
        else:
            facets = (
                np.array(
                    [list(np.trim_zeros(a, "b")) for a in list(facet_indices)],
                    dtype=np.int64,
                )
                - 1
            )
        # Can't use the vectorized version, due to a bug in ngsolve:
        # https://forum.ngsolve.org/t/extract-facet-markers-from-netgen-mesh/3256
        facet_values = np.array([facet.index for facet in ng_facets], dtype=np.int32)
        regions = comm.bcast(regions, root=0)
    else:
        facets = np.zeros((0, 3), dtype=np.int64)
        facet_values = np.zeros((0,), dtype=np.int32)
        regions = comm.bcast(None, root=0)
    local_entities, local_values = dolfinx.io.gmshio.distribute_entity_data(
        linear_mesh, linear_mesh.topology.dim - 1, facets, facet_values
    )
    linear_mesh.topology.create_connectivity(linear_mesh.topology.dim - 1, 0)
    adj = dolfinx.graph.adjacencylist(local_entities)
    ft = dolfinx.mesh.meshtags_from_entities(
        linear_mesh,
        linear_mesh.topology.dim - 1,
        adj,
        local_values.astype(np.int32, copy=False),
    )
    ft.name = "Facet tags"
    return linear_mesh, ft, regions

It needs some extra conditionals to handle 3D meshes and quads, and I’ll aim to have higher order geometries interfaces as well. I’ll get back to you once I get around this.