Source code for ogstools.meshlib.gmsh_converter

# Copyright (c) 2012-2024, 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 .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)) pv_mesh = pv.from_meshio(mesh).clean() 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) group_ids = np.unique(pv_mesh["gmsh:physical"]) 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: # Gmsh may store element of the same physical id in different # blocks. To mark all those elements correctly for extraction in the # pyvista mesh, we need to add an offset of the running total of # counted cells, as we don't have these blocks in the pyvista mesh. group_offsets = {index: 0 for index in group_ids} group_cells = np.full(pv_mesh.number_of_cells, False) for index, cell_ids in enumerate(mesh.cell_sets[name]): if len(cell_ids) == 0: continue # assert all in this array are the same group_id = mesh.cell_data["gmsh:physical"][index][0] select = np.argwhere( (pv_mesh["gmsh:physical"] == group_id) & (pv_cell_dims == group_dim) )[:, 0] group_cells[select[cell_ids + group_offsets[group_id]]] = True group_offsets[group_id] += len( mesh.cell_data["gmsh:physical"][index] ) 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 = "Unexpectedly got an empty mesh." 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