Source code for ogstools.meshlib.gmsh_converter
# Copyright (c) 2012-2025, OpenGeoSys Community (http://www.opengeosys.org)
# Distributed under a Modified BSD License.
# See accompanying file LICENSE.txt or
# http://www.opengeosys.org/project/license
#
# Original author: Dominik Kern (TU Bergakademie Freiberg)
# Author: Florian Zill
import logging
from pathlib import Path
import meshio
import numpy as np
import pyvista as pv
from meshio._vtk_common import meshio_to_vtk_type
from .mesh import Mesh
logging.basicConfig() # Important, initializes root logger
logger = logging.getLogger(__name__)
[docs]
def meshes_from_gmsh(
filename: Path,
dim: int | list[int] = 0,
reindex: bool = True,
log: bool = True,
) -> dict[str, Mesh]:
"""
Generates pyvista unstructured grids from a gmsh mesh (.msh).
Extracts domain-, boundary- and physical group-submeshes.
:param filename: Gmsh mesh file (.msh) as input data
:param dim: Spatial dimension (1, 2 or 3), trying automatic detection,
if not given. If multiple dimensions are provided, all elements
of these dimensions are embedded in the resulting domain mesh.
:param reindex: Physical groups / regions / Material IDs to be
renumbered consecutively beginning with zero.
:param log: If False, silence log messages
:returns: A dictionary of names and corresponding meshes
"""
logger.setLevel(logging.INFO if log else logging.ERROR)
if isinstance(dim, list) and len(dim) > 3:
msg = "Specify at most 3 dim values."
raise ValueError(msg)
filename = Path(filename)
if not filename.is_file():
raise FileNotFoundError
meshes: dict[str, meshio.Mesh] = {}
mesh: meshio.Mesh = meshio.read(str(filename))
# Workaround for pyvista 0.45
# pv.from_meshio operates on mesh_cells, which
# from 0.45 from_meshio expects (wrongly?) that physical groups are disjunct data sets
# Workaround: cell_sets is temporary removed from data for from_meshio
read_cells = mesh.cell_sets.copy()
mesh.cell_sets = None
pv_mesh: pv.UnstructuredGrid = pv.from_meshio(mesh).clean()
mesh.cell_sets = read_cells
# Code without workaround:
# pv_mesh = pv.read(str(filename))
if "gmsh:physical" not in pv_mesh.cell_data:
pv_mesh.cell_data["gmsh:physical"] = np.zeros(pv_mesh.number_of_cells)
pv_cell_dims = np.asarray([cell.dimension for cell in pv_mesh.cell])
if dim == 0:
dim = [np.max(pv_cell_dims)]
logger.info("Detected domain dimension of %d", dim[0])
elif isinstance(dim, int):
dim = [dim]
domain_mesh = Mesh(
pv_mesh.extract_cells([cell.dimension in dim for cell in pv_mesh.cell])
)
mat_ids = domain_mesh.cell_data.pop(
"gmsh:physical", np.zeros(domain_mesh.number_of_cells)
)
unique_mat_ids = np.unique(mat_ids)
logger.info("Found material IDs: %s", unique_mat_ids)
domain_mesh.clear_cell_data()
domain_mesh.clear_point_data()
domain_mesh.clear_field_data()
domain_mesh.cell_data["MaterialIDs"] = np.int32(mat_ids)
if reindex:
domain_mesh.reindex_material_ids()
logger.info("Renumbered to: %s", np.unique(domain_mesh["MaterialIDs"]))
meshes["domain"] = domain_mesh
logger.info("%s: %s", "domain", domain_mesh)
for name, (group_index, group_dim) in mesh.field_data.items():
# skip iteration for domain mesh
if name == filename.stem:
continue
# for old gmsh versions
if mesh.cell_sets == {}:
subdomain = pv_mesh.extract_cells(
pv_cell_dims == group_dim
).threshold([group_index, group_index], "gmsh:physical")
# for recent gmsh versions (allows cells belonging to multiple groups)
else:
group_cells = np.full(pv_mesh.number_of_cells, False)
for type_name, type_subset in mesh.cell_sets_dict[name].items():
celltype = meshio_to_vtk_type[type_name]
type_index = np.nonzero(pv_mesh.celltypes == celltype)[0]
group_cells[type_index[type_subset]] = True
subdomain = pv_mesh.extract_cells(group_cells)
# Workaround for gmsh python api bug
# (physical group with tag 0 doesn't produce correct msh file)
if (
subdomain.number_of_cells == 0
and group_index == 1
and 0 in pv_mesh["gmsh:physical"]
):
subdomain = pv_mesh.threshold([0, 0], "gmsh:physical")
if subdomain.number_of_cells == 0:
msg = f"Unexpectedly got an empty mesh in physical group: {name}."
raise RuntimeError(msg)
sub_cell_dims = np.array([cell.dimension for cell in subdomain.cell])
if not np.all(sub_cell_dims == sub_cell_dims[0]):
msg = (
f"Subdomain should only contain cells of dim {group_dim}, but"
f"{name} contains cells of dim {np.unique(sub_cell_dims)}."
)
raise AssertionError(msg)
# rename vtk fields to OGS conventions and change to correct type
subdomain["bulk_elem_ids"] = np.uint64(
subdomain.cell_data.pop("vtkOriginalCellIds")
)
subdomain["bulk_node_ids"] = np.uint64(
subdomain.point_data.pop("vtkOriginalPointIds")
)
# remove unnecessary data
for cell_data_key in ["gmsh:physical", "gmsh:geometrical"]:
if cell_data_key in subdomain.cell_data:
subdomain.cell_data.remove(cell_data_key)
if "gmsh:dim_tags" in subdomain.point_data:
subdomain.point_data.remove("gmsh:dim_tags")
subdomain.field_data.clear()
meshes[f"physical_group_{name}"] = Mesh(subdomain)
logger.info("%s: %s", f"physical_group_{name}", subdomain)
logger.info("Conversion complete.")
return meshes