Skip to content

Commit f1c5f30

Browse files
Refactor gmshio to use correct naming of subentities (#3732)
* Refactor gmshio to use loops and 'correct' names for codimensional entities. Ref M. Scroggs paper on permutations, Table 1 * Ruff format * Come on mypy! * Optional * Add missing create connectivity * Change connectivity creation * Create demo that tags peaks, ridges, facets and cells in a grid * Add documentation * Revert new connectivity * Generalize check * Renaming as suggested by M Co-authored-by: Michal Habera <[email protected]> * Improve documentation and naming --------- Co-authored-by: Michal Habera <[email protected]>
1 parent 1473fe4 commit f1c5f30

File tree

2 files changed

+121
-157
lines changed

2 files changed

+121
-157
lines changed

python/demo/demo_gmsh.py

Lines changed: 19 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636

3737
# +
3838
def gmsh_sphere(model: gmsh.model, name: str) -> gmsh.model:
39-
"""Create a Gmsh model of a sphere.
39+
"""Create a Gmsh model of a sphere and tag sub entitites
40+
from all co-dimensions (peaks, ridges, facets and cells).
4041
4142
Args:
4243
model: Gmsh model to add the mesh to.
@@ -53,9 +54,15 @@ def gmsh_sphere(model: gmsh.model, name: str) -> gmsh.model:
5354
# Synchronize OpenCascade representation with gmsh model
5455
model.occ.synchronize()
5556

56-
# Add physical marker for cells. It is important to call this
57-
# function after OpenCascade synchronization
58-
model.add_physical_group(dim=3, tags=[sphere])
57+
# Add physical tag for sphere
58+
model.add_physical_group(dim=3, tags=[sphere], tag=1)
59+
60+
# Embed all sub-entities from the GMSH model into the sphere and tag them
61+
for dim in [0, 1, 2]:
62+
entities = model.getEntities(dim)
63+
entity_ids = [entity[1] for entity in entities]
64+
model.mesh.embed(dim, entity_ids, 3, sphere)
65+
model.add_physical_group(dim=dim, tags=entity_ids, tag=dim)
5966

6067
# Generate the mesh
6168
model.mesh.generate(dim=3)
@@ -167,10 +174,10 @@ def create_mesh(comm: MPI.Comm, model: gmsh.model, name: str, filename: str, mod
167174
mesh_data.cell_tags.name = f"{name}_cells"
168175
if mesh_data.facet_tags is not None:
169176
mesh_data.facet_tags.name = f"{name}_facets"
170-
if mesh_data.edge_tags is not None:
171-
mesh_data.edge_tags.name = f"{name}_edges"
172-
if mesh_data.vertex_tags is not None:
173-
mesh_data.vertex_tags.name = f"{name}_vertices"
177+
if mesh_data.ridge_tags is not None:
178+
mesh_data.ridge_tags.name = f"{name}_ridges"
179+
if mesh_data.peak_tags is not None:
180+
mesh_data.peak_tags.name = f"{name}_peaks"
174181
with XDMFFile(mesh_data.mesh.comm, filename, mode) as file:
175182
mesh_data.mesh.topology.create_connectivity(2, 3)
176183
mesh_data.mesh.topology.create_connectivity(1, 3)
@@ -188,15 +195,15 @@ def create_mesh(comm: MPI.Comm, model: gmsh.model, name: str, filename: str, mod
188195
mesh_data.mesh.geometry,
189196
geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{name}']/Geometry",
190197
)
191-
if mesh_data.edge_tags is not None:
198+
if mesh_data.ridge_tags is not None:
192199
file.write_meshtags(
193-
mesh_data.edge_tags,
200+
mesh_data.ridge_tags,
194201
mesh_data.mesh.geometry,
195202
geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{name}']/Geometry",
196203
)
197-
if mesh_data.vertex_tags is not None:
204+
if mesh_data.peak_tags is not None:
198205
file.write_meshtags(
199-
mesh_data.vertex_tags,
206+
mesh_data.peak_tags,
200207
mesh_data.mesh.geometry,
201208
geometry_xpath=f"/Xdmf/Domain/Grid[@Name='{name}']/Geometry",
202209
)

python/dolfinx/io/gmshio.py

Lines changed: 102 additions & 145 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2022 Jørgen S. Dokken
1+
# Copyright (C) 2022-2025 Jørgen S. Dokken, Henrik N. T. Finsberg and Paul T. Kühner
22
#
33
# This file is part of DOLFINx (https://www.fenicsproject.org)
44
#
@@ -78,9 +78,9 @@ class MeshData(typing.NamedTuple):
7878
Args:
7979
mesh: Mesh.
8080
cell_tags: MeshTags for cells.
81-
facet_tags: MeshTags for facets.
82-
edge_tags: MeshTags for edges.
83-
vertex_tags: MeshTags for vertices.
81+
facet_tags: MeshTags for facets (codim 1).
82+
ridge_tags: MeshTags for ridges (codim 2).
83+
peak_tags: MeshTags for peaks (codim 3).
8484
physical_groups: Physical groups in the mesh, where the key
8585
is the physical name and the value is a tuple with the
8686
dimension and tag.
@@ -89,8 +89,8 @@ class MeshData(typing.NamedTuple):
8989
mesh: Mesh
9090
cell_tags: typing.Optional[MeshTags]
9191
facet_tags: typing.Optional[MeshTags]
92-
edge_tags: typing.Optional[MeshTags]
93-
vertex_tags: typing.Optional[MeshTags]
92+
ridge_tags: typing.Optional[MeshTags]
93+
peak_tags: typing.Optional[MeshTags]
9494
physical_groups: dict[str, tuple[int, int]]
9595

9696

@@ -184,7 +184,13 @@ def extract_topology_and_markers(
184184
# one cell-type,
185185
# i.e. facets of prisms and pyramid meshes are not supported
186186
(entity_types, entity_tags, entity_topologies) = model.mesh.getElements(dim, tag=entity)
187-
assert len(entity_types) == 1
187+
188+
if len(entity_types) > 1:
189+
raise RuntimeError(
190+
f"Unsupported mesh with multiple cell types {entity_types} for entity {entity}"
191+
)
192+
elif len(entity_types) == 0:
193+
continue
188194

189195
# Determine number of local nodes per element to create the
190196
# topology of the elements
@@ -277,8 +283,10 @@ def model_to_mesh(
277283
distribution of cells across MPI ranks.
278284
279285
Returns:
280-
MeshData with mesh, cell tags, facet tags, edge tags,
281-
vertex tags and physical groups.
286+
MeshData with mesh and tags of corresponding entities by codimension.
287+
Codimension 0 is the cell tags, codimension 1 is the facet tags,
288+
codimension 2 is the ridge tags and codimension 3 is the peak tags
289+
as well as a lookup table from the physical groups by name to integer.
282290
283291
Note:
284292
For performance, this function should only be called once for
@@ -292,158 +300,107 @@ def model_to_mesh(
292300
x = extract_geometry(model)
293301
topologies, physical_groups = extract_topology_and_markers(model)
294302

295-
# Extract Gmsh cell id, dimension of cell and number of nodes to
296-
# cell for each
297-
num_cell_types = len(topologies.keys())
298-
cell_information = dict()
299-
cell_dimensions = np.zeros(num_cell_types, dtype=np.int32)
303+
# Extract Gmsh entity (cell) id, topological dimension and number of nodes
304+
# which is used to create an appropriate coordinate element, and seperate
305+
# higher topological entities from lower topological entities (e.g. facets,
306+
# ridges and peaks).
307+
num_unique_entities = len(topologies.keys())
308+
element_ids = np.zeros(num_unique_entities, dtype=np.int32)
309+
entity_tdim = np.zeros(num_unique_entities, dtype=np.int32)
310+
num_nodes_per_element = np.zeros(num_unique_entities, dtype=np.int32)
300311
for i, element in enumerate(topologies.keys()):
301312
_, dim, _, num_nodes, _, _ = model.mesh.getElementProperties(element)
302-
cell_information[i] = {"id": element, "dim": dim, "num_nodes": num_nodes}
303-
cell_dimensions[i] = dim
304-
305-
# Sort elements by ascending dimension
306-
perm_sort = np.argsort(cell_dimensions)
307-
308-
# Broadcast cell type data and geometric dimension
309-
cell_id = cell_information[perm_sort[-1]]["id"]
310-
tdim = cell_information[perm_sort[-1]]["dim"]
311-
num_nodes = cell_information[perm_sort[-1]]["num_nodes"]
312-
cell_id, num_nodes = comm.bcast([cell_id, num_nodes], root=rank)
313-
314-
# Check for facet, edge and vertex data and broadcast relevant info if True
315-
has_facet_data = (tdim - 1) in cell_dimensions
316-
has_edge_data = (tdim - 2) in cell_dimensions
317-
has_vertex_data = (tdim - 3) in cell_dimensions
318-
319-
has_facet_data = comm.bcast(has_facet_data, root=rank)
320-
if has_facet_data:
321-
num_facet_nodes = comm.bcast(cell_information[perm_sort[-2]]["num_nodes"], root=rank)
322-
gmsh_facet_id = cell_information[perm_sort[-2]]["id"]
323-
marked_facets = np.asarray(topologies[gmsh_facet_id]["topology"], dtype=np.int64)
324-
facet_values = np.asarray(topologies[gmsh_facet_id]["cell_data"], dtype=np.int32)
325-
326-
has_edge_data = comm.bcast(has_edge_data, root=rank)
327-
if has_edge_data:
328-
num_edge_nodes = comm.bcast(cell_information[perm_sort[-3]]["num_nodes"], root=rank)
329-
gmsh_edge_id = cell_information[perm_sort[-3]]["id"]
330-
marked_edges = np.asarray(topologies[gmsh_edge_id]["topology"], dtype=np.int64)
331-
edge_values = np.asarray(topologies[gmsh_edge_id]["cell_data"], dtype=np.int32)
332-
333-
has_vertex_data = comm.bcast(has_vertex_data, root=rank)
334-
if has_vertex_data:
335-
num_vertex_nodes = comm.bcast(cell_information[perm_sort[-4]]["num_nodes"], root=rank)
336-
gmsh_vertex_id = cell_information[perm_sort[-4]]["id"]
337-
marked_vertices = np.asarray(topologies[gmsh_vertex_id]["topology"], dtype=np.int64)
338-
vertex_values = np.asarray(topologies[gmsh_vertex_id]["cell_data"], dtype=np.int32)
339-
340-
cells = np.asarray(topologies[cell_id]["topology"], dtype=np.int64)
341-
cell_values = np.asarray(topologies[cell_id]["cell_data"], dtype=np.int32)
342-
physical_groups = comm.bcast(physical_groups, root=rank)
343-
else:
344-
cell_id, num_nodes = comm.bcast([None, None], root=rank)
345-
cells, x = np.empty([0, num_nodes], dtype=np.int32), np.empty([0, gdim], dtype=dtype)
346-
cell_values = np.empty((0,), dtype=np.int32)
347-
348-
has_facet_data = comm.bcast(None, root=rank)
349-
if has_facet_data:
350-
num_facet_nodes = comm.bcast(None, root=rank)
351-
marked_facets = np.empty((0, num_facet_nodes), dtype=np.int32)
352-
facet_values = np.empty((0,), dtype=np.int32)
353-
354-
has_edge_data = comm.bcast(None, root=rank)
355-
if has_edge_data:
356-
num_edge_nodes = comm.bcast(None, root=rank)
357-
marked_edges = np.empty((0, num_edge_nodes), dtype=np.int32)
358-
edge_values = np.empty((0,), dtype=np.int32)
359-
360-
has_vertex_data = comm.bcast(None, root=rank)
361-
if has_vertex_data:
362-
num_vertex_nodes = comm.bcast(None, root=rank)
363-
marked_vertices = np.empty((0, num_vertex_nodes), dtype=np.int32)
364-
vertex_values = np.empty((0,), dtype=np.int32)
365-
366-
physical_groups = comm.bcast(None, root=rank)
313+
element_ids[i] = element
314+
entity_tdim[i] = dim
315+
num_nodes_per_element[i] = num_nodes
367316

368-
# Create distributed mesh
369-
ufl_domain = ufl_mesh(cell_id, gdim, dtype=dtype)
317+
# Broadcast information to all other ranks
318+
entity_tdim, element_ids, num_nodes_per_element = comm.bcast(
319+
(entity_tdim, element_ids, num_nodes_per_element), root=rank
320+
)
321+
else:
322+
entity_tdim, element_ids, num_nodes_per_element = comm.bcast((None, None, None), root=rank)
323+
324+
# Sort elements by descending dimension
325+
assert len(np.unique(entity_tdim)) == len(entity_tdim)
326+
perm_sort = np.argsort(entity_tdim)[::-1]
327+
328+
# Extract position of the highest topological entity and its topological dimension
329+
cell_position = perm_sort[0]
330+
tdim = int(entity_tdim[cell_position])
331+
332+
# Extract entity -> node connectivity for all cells and sub-entities marked in the GMSH model
333+
meshtags: dict[int, tuple[npt.NDArray[np.int32], npt.NDArray[np.int32]]] = {}
334+
for position in perm_sort:
335+
codim = tdim - entity_tdim[position]
336+
if comm.rank == rank:
337+
gmsh_entity_id = element_ids[position]
338+
marked_entities = np.asarray(topologies[gmsh_entity_id]["topology"], dtype=np.int64)
339+
entity_values = np.asarray(topologies[gmsh_entity_id]["cell_data"], dtype=np.int32)
340+
meshtags[codim] = (marked_entities, entity_values)
341+
else:
342+
# Any other process than input rank does not have any entities
343+
marked_entities = np.empty((0, num_nodes_per_element[position]), dtype=np.int32)
344+
entity_values = np.empty((0,), dtype=np.int32)
345+
meshtags[codim] = (marked_entities, entity_values)
346+
347+
# Create a UFL Mesh object for the GMSH element with the highest topoligcal dimension
348+
ufl_domain = ufl_mesh(element_ids[cell_position], gdim, dtype=dtype)
349+
350+
# Get cell->node connectivity and permute to FEniCS ordering
351+
num_nodes = num_nodes_per_element[cell_position]
370352
gmsh_cell_perm = cell_perm_array(_cpp.mesh.to_type(str(ufl_domain.ufl_cell())), num_nodes)
371-
cells = cells[:, gmsh_cell_perm].copy()
372-
mesh = create_mesh(comm, cells, x[:, :gdim].astype(dtype, copy=False), ufl_domain, partitioner)
353+
cell_connectivity = meshtags[0][0][:, gmsh_cell_perm].copy()
373354

374-
# Create MeshTags for cells
375-
local_entities, local_values = distribute_entity_data(
376-
mesh, mesh.topology.dim, cells, cell_values
355+
# Create a distributed mesh, where mesh nodes are only destributed from the input rank
356+
if comm.rank != rank:
357+
x = np.empty([0, gdim], dtype=dtype) # No nodes on other than root rank
358+
mesh = create_mesh(
359+
comm, cell_connectivity, x[:, :gdim].astype(dtype, copy=False), ufl_domain, partitioner
377360
)
378-
mesh.topology.create_connectivity(mesh.topology.dim, 0)
379-
adj = adjacencylist(local_entities)
380-
ct = meshtags_from_entities(
381-
mesh, mesh.topology.dim, adj, local_values.astype(np.int32, copy=False)
361+
assert tdim == mesh.topology.dim, (
362+
f"{mesh.topology.dim=} does not match Gmsh model dimension {tdim}"
382363
)
383-
ct.name = "Cell tags"
384364

385-
# Create MeshTags for facets
365+
# Create MeshTags for all sub entities
386366
topology = mesh.topology
387-
tdim = topology.dim
388-
if has_facet_data:
389-
# Permute facets from MSH to DOLFINx ordering
390-
# FIXME: This does not work for prism meshes
391-
if topology.cell_type == CellType.prism or topology.cell_type == CellType.pyramid:
392-
raise RuntimeError(f"Unsupported cell type {topology.cell_type}")
393-
394-
facet_type = _cpp.mesh.cell_entity_type(
395-
_cpp.mesh.to_type(str(ufl_domain.ufl_cell())), tdim - 1, 0
396-
)
397-
gmsh_facet_perm = cell_perm_array(facet_type, num_facet_nodes)
398-
marked_facets = marked_facets[:, gmsh_facet_perm]
399-
400-
local_entities, local_values = distribute_entity_data(
401-
mesh, tdim - 1, marked_facets, facet_values
402-
)
403-
mesh.topology.create_connectivity(topology.dim - 1, tdim)
404-
adj = adjacencylist(local_entities)
405-
ft = meshtags_from_entities(mesh, tdim - 1, adj, local_values.astype(np.int32, copy=False))
406-
ft.name = "Facet tags"
407-
else:
408-
ft = None
409-
410-
if has_edge_data:
411-
# Permute edges from MSH to DOLFINx ordering
412-
edge_type = _cpp.mesh.cell_entity_type(
413-
_cpp.mesh.to_type(str(ufl_domain.ufl_cell())), tdim - 2, 0
414-
)
415-
gmsh_edge_perm = cell_perm_array(edge_type, num_edge_nodes)
416-
marked_edges = marked_edges[:, gmsh_edge_perm]
417-
367+
codim_to_name = {0: "cell", 1: "facet", 2: "ridge", 3: "peak"}
368+
dolfinx_meshtags: dict[str, typing.Optional[MeshTags]] = {}
369+
for codim in [0, 1, 2, 3]:
370+
key = f"{codim_to_name[codim]}_tags"
371+
if (
372+
codim == 1 and topology.cell_type == CellType.prism
373+
) or topology.cell_type == CellType.pyramid:
374+
raise RuntimeError(f"Unsupported facet tag for type {topology.cell_type}")
375+
376+
meshtag_data = meshtags.get(codim, None)
377+
if meshtag_data is None:
378+
dolfinx_meshtags[key] = None
379+
continue
380+
381+
# Distribute entity data [[e0_v0, e0_v1, ...], [e1_v0, e1_v1, ...], ...]
382+
# which is made in global input indices to local indices on the
383+
# owning process
384+
(marked_entities, entity_values) = meshtag_data
418385
local_entities, local_values = distribute_entity_data(
419-
mesh, tdim - 2, marked_edges, edge_values
386+
mesh, tdim - codim, marked_entities, entity_values
420387
)
421-
mesh.topology.create_connectivity(topology.dim - 2, tdim)
388+
# Create MeshTags object from the local entities
389+
mesh.topology.create_connectivity(tdim - codim, tdim)
422390
adj = adjacencylist(local_entities)
423-
et = meshtags_from_entities(mesh, tdim - 2, adj, local_values.astype(np.int32, copy=False))
424-
et.name = "Edge tags"
425-
else:
426-
et = None
427-
428-
if has_vertex_data:
429-
# Permute vertices from MSH to DOLFINx ordering
430-
vertex_type = _cpp.mesh.cell_entity_type(
431-
_cpp.mesh.to_type(str(ufl_domain.ufl_cell())), tdim - 3, 0
391+
et = meshtags_from_entities(
392+
mesh, tdim - codim, adj, local_values.astype(np.int32, copy=False)
432393
)
433-
gmsh_vertex_perm = cell_perm_array(vertex_type, num_vertex_nodes)
434-
marked_vertices = marked_vertices[:, gmsh_vertex_perm]
394+
et.name = key
395+
dolfinx_meshtags[key] = et
435396

436-
local_entities, local_values = distribute_entity_data(
437-
mesh, tdim - 3, marked_vertices, vertex_values
438-
)
439-
mesh.topology.create_connectivity(topology.dim - 3, tdim)
440-
adj = adjacencylist(local_entities)
441-
vt = meshtags_from_entities(mesh, tdim - 3, adj, local_values.astype(np.int32, copy=False))
442-
vt.name = "Vertex tags"
397+
# Broadcast physical groups (string to integer mapping) to all ranks
398+
if comm.rank == rank:
399+
physical_groups = comm.bcast(physical_groups, root=rank)
443400
else:
444-
vt = None
401+
physical_groups = comm.bcast(None, root=rank)
445402

446-
return MeshData(mesh, ct, ft, et, vt, physical_groups)
403+
return MeshData(mesh, physical_groups=physical_groups, **dolfinx_meshtags)
447404

448405

449406
def read_from_msh(

0 commit comments

Comments
 (0)