# Copyright (C) 2021-2025 Université Gustave Eiffel.
# This file is part of the EasyFEA project.
# EasyFEA is distributed under the terms of the GNU General Public License v3, see LICENSE.txt and CREDITS.md for more information.
"""Module providing an interface with meshio (https://pypi.org/project/meshio/)."""
import re
import meshio
from collections import Counter
from typing import Any, Optional, Union
from . import Folder, Display, _types
from ..fem import Mesh, ElemType, GroupElemFactory, _GroupElem
from .PyVista import np, pv
# ----------------------------------------------
# TYPES
# ----------------------------------------------
DICT_ELEMTYPE_TO_MESHIO = {
ElemType.POINT: "vertex",
ElemType.SEG2: "line",
ElemType.SEG3: "line3",
ElemType.SEG4: "line4",
ElemType.SEG5: "line5",
ElemType.TRI3: "triangle",
ElemType.TRI6: "triangle6",
ElemType.TRI10: "triangle10",
ElemType.TRI15: "triangle15",
ElemType.QUAD4: "quad",
ElemType.QUAD8: "quad8",
ElemType.QUAD9: "quad9",
ElemType.TETRA4: "tetra",
ElemType.TETRA10: "tetra10",
ElemType.HEXA8: "hexahedron",
ElemType.HEXA20: "hexahedron20",
ElemType.HEXA27: "hexahedron27",
ElemType.PRISM6: "wedge",
ElemType.PRISM15: "wedge15",
ElemType.PRISM18: "wedge18",
}
"""ElemType: meshioType"""
DICT_MESHIO_TO_ELEMTYPE: dict[str, ElemType] = {
meshio: elemType for elemType, meshio in DICT_ELEMTYPE_TO_MESHIO.items()
}
"""CellType: ElemType"""
DICT_ELEMTYPE_TO_VTK: dict[ElemType, pv.CellType] = {
# (to Pyvista, to Paraview)
# see https://dev.pyvista.org/api/utilities/_autosummary/pyvista.celltype#pyvista.CellType
ElemType.POINT: pv.CellType.VERTEX,
ElemType.SEG2: pv.CellType.LINE,
ElemType.SEG3: pv.CellType.QUADRATIC_EDGE,
ElemType.SEG4: pv.CellType.CUBIC_LINE,
ElemType.SEG5: pv.CellType.HIGHER_ORDER_EDGE,
ElemType.TRI3: pv.CellType.TRIANGLE,
ElemType.TRI6: pv.CellType.QUADRATIC_TRIANGLE,
ElemType.TRI10: pv.CellType.LAGRANGE_TRIANGLE,
ElemType.TRI15: pv.CellType.LAGRANGE_TRIANGLE,
ElemType.QUAD4: pv.CellType.QUAD,
ElemType.QUAD8: pv.CellType.QUADRATIC_QUAD,
ElemType.QUAD9: pv.CellType.BIQUADRATIC_QUAD,
ElemType.TETRA4: pv.CellType.TETRA,
ElemType.TETRA10: pv.CellType.QUADRATIC_TETRA,
ElemType.HEXA8: pv.CellType.HEXAHEDRON,
ElemType.HEXA20: pv.CellType.QUADRATIC_HEXAHEDRON,
ElemType.HEXA27: pv.CellType.TRIQUADRATIC_HEXAHEDRON,
ElemType.PRISM6: pv.CellType.WEDGE,
ElemType.PRISM15: pv.CellType.QUADRATIC_WEDGE,
ElemType.PRISM18: pv.CellType.BIQUADRATIC_QUADRATIC_WEDGE,
}
"""ElemType: CellType"""
DICT_PYVISTA_TO_ELEMTYPE: dict[pv.CellType, ElemType] = {
cellType: elemType for elemType, cellType in DICT_ELEMTYPE_TO_VTK.items()
}
"""CellType: ElemType"""
DICT_ELEMTYPE_TO_ENSIGHT: dict[ElemType, str] = {
# (to Ensight)
ElemType.POINT: "point",
ElemType.SEG2: "bar2",
ElemType.SEG3: "bar3",
# ElemType.SEG4: "bar4", # not supported by Ensight
# ElemType.SEG5: "bar5", # not supported by Ensight
ElemType.TRI3: "tria3",
ElemType.TRI6: "tria6",
# ElemType.TRI10: "tria10", # not supported by Ensight
# ElemType.TRI15: "tria15", # not supported by Ensight
ElemType.QUAD4: "quad4",
ElemType.QUAD8: "quad8",
# ElemType.QUAD9: "quad9", # not supported by Ensight
ElemType.TETRA4: "tetra4",
ElemType.TETRA10: "tetra10",
ElemType.HEXA8: "hexa8",
ElemType.HEXA20: "hexa20",
# ElemType.HEXA27: "hexa27", # not supported by Ensight
ElemType.PRISM6: "wedge6",
ElemType.PRISM15: "wedge15",
# ElemType.PRISM18: "wedge18", # not supported by Ensight
}
"""ElemType: Ensight"""
DICT_ENSIGHT_TO_ELEMTYPE: dict[str, ElemType] = {
ensight: elemType for elemType, ensight in DICT_ELEMTYPE_TO_ENSIGHT.items()
}
"""Ensight: ElemType"""
# ----------------------------------------------
# INDEXES
# ----------------------------------------------
# reorganize the connectivity order
# because some elements in gmsh don't have the same numbering order as in vtk
# pyvista -> https://docs.pyvista.org/version/stable/api/core/_autosummary/pyvista.UnstructuredGrid.celltypes.html
# vtk -> https://vtk.org/doc/nightly/html/vtkCellType_8h_source.html
# https://dev.pyvista.org/api/utilities/_autosummary/pyvista.celltype
# you can search for vtk elements on the internet
DICT_GMSH_TO_VTK_INDEXES: dict[ElemType, list[int]] = {
# https://dev.pyvista.org/api/examples/_autosummary/pyvista.examples.cells.quadratichexahedron#pyvista.examples.cells.QuadraticHexahedron
# fmt: off
ElemType.HEXA20: [
0, 1, 2, 3, 4, 5, 6, 7, # vertices
8, 11, 13, 9, 16, 18, 19, 17, 10, 12, 14, 15 # edges
],
# https://dev.pyvista.org/api/examples/_autosummary/pyvista.examples.cells.triquadratichexahedron#pyvista.examples.cells.TriQuadraticHexahedron
ElemType.HEXA27: [
0, 1, 2, 3, 4, 5, 6, 7, # vertices
8, 11, 13, 9, 16, 18, 19, 17, 10, 12, 14, 15, # edges
22, 23, 21, 24, 20, 25, # faces
26 # volumes
],
ElemType.PRISM15: [
0, 1, 2, 3, 4, 5, # vertices
6, 9, 7, 12, 14, 13, 8, 10, 11 # edges
],
ElemType.PRISM18: [
0, 1, 2, 3, 4, 5, # vertices
6, 9, 7, 12, 14, 13, 8, 10, 11, # edges
15, 17, 16 # faces
],
# nodes 8 and 9 are switch
ElemType.TETRA10: [
0, 1, 2, 3, # vertices
4, 5, 6, 7, 9, 8 # faces
],
# fmt: on
}
"""ElemType: list[int]"""
DICT_VTK_TO_GMSH_INDEXES: dict[pv.CellType, list[int]] = {
DICT_ELEMTYPE_TO_VTK[elemType]: [indexes.index(i) for i in range(len(indexes))]
for elemType, indexes in DICT_GMSH_TO_VTK_INDEXES.items()
}
"""CellType: list[int]"""
# https://ansyshelp.ansys.com/public/account/secured?returnurl=%2F%2F%2F%2F%2FViews%2FSecured%2Fcorp%2Fv242%2Fen%2Fensight_um%2FUM-C9xmlidEnSightGoldCaseFileFormat.html
DICT_GMSH_TO_ENSIGHT_INDEXES: dict[ElemType, list[int]] = {
# fmt: off
ElemType.SEG3: [0, 2, 1],
# nodes 8 and 9 are switch
ElemType.TETRA10: [
0, 1, 2, 3, # vertices
4, 5, 6, 7, 9, 8 # edges
],
ElemType.PRISM15: [
0, 1, 2, 3, 4, 5, # vertices
6, 8, 12, 7, 13, 14, 9, 11, 10 # edges
],
ElemType.HEXA20: [
0, 1, 2, 3, 4, 5, 6, 7, # vertices
8, 11, 16, 9, 17, 10, 18, 19, 12, 15, 13, 14 # edges
],
# fmt: on
}
"""ElemType: list[int]"""
DICT_ENSIGHT_TO_GMSH_INDEXES: dict[str, list[int]] = {
DICT_ELEMTYPE_TO_ENSIGHT[elemType]: [indexes.index(i) for i in range(len(indexes))]
for elemType, indexes in DICT_GMSH_TO_ENSIGHT_INDEXES.items()
}
"""Ensight: list[int]"""
# ----------------------------------------------
# Tools
# ----------------------------------------------
[docs]
def Surface_reconstruction(mesh: Mesh) -> Mesh:
"""Reconstructs the missing surfaces in a mesh."""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
if mesh.dim != 3:
# No need to reconstruct elements for 0D, 1D or 2D meshes
return mesh
useMixedElements = mesh.elemType.startswith(("PRISM"))
# get coordinates without orphan nodes
coordinates = mesh.groupElem.coordGlob[mesh.nodes]
# get group elem data
groupElem = mesh.groupElem
connectivity = groupElem.connect
# get faces to access nodes in connectivity
faces = groupElem.faces
Nface = faces.shape[0]
allConnect: list[_types.IntArray] = []
allIds: list[tuple[int]] = []
# loop over each indices
for face in faces:
# get connect for the idx
connect = connectivity[:, face]
allConnect.extend(connect.copy())
# Ensure that generated IDs (tuples in this case) are unique
connect = np.sort(connect, axis=1)
# add unique ids
if useMixedElements:
allIds.extend([tuple(nodes) for nodes in connect])
else:
allIds.extend(list(map(tuple, connect)))
# make sure all nodes are imported
assert len(allConnect) == groupElem.Ne * Nface
# counts the number of repetitions of each identifier
counts = Counter(allIds)
# get unique nodes in all created nodes
uniqueNodes: list[_types.IntArray] = [
allConnect[i] for i, id in enumerate(allIds) if counts[id] == 1
]
# contstruct the new group of elements from the existing ones
new_dict_groupElem: dict[ElemType, _GroupElem] = {
elemType: groupElem
for elemType, groupElem in mesh.dict_groupElem.items()
if groupElem.dim != 2
}
# create new elements 2d elements
for elemType in GroupElemFactory._Get_2d_element_types(mesh.elemType):
# get connect
nPe = GroupElemFactory.DICT_ELEMTYPE[elemType][1]
connect = np.asarray(
[nodes for nodes in uniqueNodes if nodes.size == nPe], dtype=int
)
# create the new group of elements
newGroupElem = GroupElemFactory.Create(elemType, connect, coordinates)
new_dict_groupElem[elemType] = newGroupElem
pass
# create the new mesh
newMesh = Mesh(new_dict_groupElem)
return newMesh
def __Get_dict_tags_converter(mesh: Mesh) -> dict[Any, int]:
"""Construct dict_tags as a dictionary with string keys and int values."""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
# get all the tags contained in the mesh
tags = []
[tags.extend(groupElem.nodeTags) for groupElem in mesh.dict_groupElem.values()] # type: ignore [func-returns-value]
tags = np.unique(tags).tolist()
# get all int values in each tags
dict_tags = {tag: int(re.sub(r"\D", "", tag)) for tag in tags}
# For now, it does not import strings different from P{i}, L{i}, S{i}, V{i}.
# It won't work for long strings.
return dict_tags
# ----------------------------------------------
# EasyFEA to Meshio
# ----------------------------------------------
def _EasyFEA_to_Meshio(
mesh: Mesh, dict_tags_converter: dict[Any, int] = {}, cellType: str = "tags"
) -> meshio.Mesh:
"""Converts EasyFEA mesh to meshio format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
dict_tags_converter : dict[Any, int], optional
Dictionary converting tags to integers, by default {}
cellType : str, optional
cell type to acces tags, by default "tags"
Returns
-------
meshio.Mesh
Converted meshio mesh object.
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
cells_dict: dict[str, _types.IntArray] = {}
list_elements: list[_types.IntArray] = []
# loop over the group elem in the mesh
for elemType, groupElem in mesh.dict_groupElem.items():
# get meshio type
meshioType = DICT_ELEMTYPE_TO_MESHIO[elemType]
# get connectivity
connect = groupElem.connect
# reorder gmsh/easyfea idx to vtk indexes
if elemType in DICT_GMSH_TO_VTK_INDEXES:
indexes = DICT_GMSH_TO_VTK_INDEXES[elemType]
connect = connect[:, indexes]
# set cell dict
cells_dict[meshioType] = connect
# get element tags
element_tags = np.zeros(groupElem.Ne, dtype=int)
# converts tags and make sure they are integers
for tag, val in dict_tags_converter.items():
assert isinstance(val, int), "dict_tags_converter values must be integers."
# elements
elements = groupElem.Get_Elements_Tag(tag)
if elements.size > 0:
element_tags[elements] = int(val)
list_elements.append(element_tags)
cell_data = {cellType: list_elements}
# import in meshio
try:
meshioMesh = meshio.Mesh(mesh.coordGlob, cells_dict, None, cell_data)
except KeyError:
raise KeyError(
f"To support {mesh.elemType} elements, you need to install meshio using the following meshio fork (https://github.com/matnoel/meshio/tree/medit_higher_order_elements)."
)
return meshioMesh
[docs]
def _Meshio_to_EasyFEA(meshioMesh: meshio.Mesh) -> Mesh:
"""Converts meshio mesh to EasyFEA format.
Parameters
----------
meshioMesh : meshio.Mesh
Meshio mesh object.
Returns
-------
Mesh
Converted EasyFEA mesh object.
"""
assert isinstance(meshioMesh, meshio.Mesh), "meshioMesh must be a meshio mesh!"
dict_groupElem: dict[ElemType, _GroupElem] = {}
# get coordinates
Nn, dim = meshioMesh.points.shape
coordinates = np.zeros((Nn, 3))
coordinates[:, :dim] = meshioMesh.points
for meshioType, connect in meshioMesh.cells_dict.items():
# get associated elemType
elemType = DICT_MESHIO_TO_ELEMTYPE[meshioType]
# reorder vtk idx to gmsh/easyfea indexes
cellType = DICT_ELEMTYPE_TO_VTK[elemType]
if cellType in DICT_VTK_TO_GMSH_INDEXES:
indexes = DICT_VTK_TO_GMSH_INDEXES[cellType]
connect = connect[:, indexes]
# get groupElem
groupElem = GroupElemFactory.Create(elemType, connect, coordinates)
dict_groupElem[elemType] = groupElem
mesh = Mesh(dict_groupElem)
Display.MyPrint("Successfully imported the mesh in EasyFEA.")
print(mesh)
# set tags
dict_tags: dict[str, _types.IntArray] = {
meshioType: tags
for values in meshioMesh.cell_data_dict.values()
for meshioType, tags in values.items()
if np.issubdtype(tags.dtype, np.integer)
}
_Set_Tags(mesh, dict_tags)
return mesh
def _Set_Tags(mesh: Mesh, dict_tags: dict[str, _types.IntArray]):
"""Set tags for nodes and elements in the EasyFEA mesh.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
dict_tags : dict[str, _types.IntArray]
Dictionary of tags for elements.
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
assert isinstance(dict_tags, dict), "dict_tags must be a dictionnary!"
# retrieve tags
for elemType, tags in dict_tags.items():
if elemType.startswith(("vertex")):
dim = 0
t = "P"
elif elemType.startswith(("line")):
dim = 1
t = "L"
elif elemType.startswith(("triangle", "quad")):
dim = 2
t = "S"
elif elemType.startswith(("tetra", "hexahedron", "wedge")):
dim = 3
t = "V"
else:
raise Exception(f"elemType {elemType} is unknown.")
uniqueTags = np.unique(tags)
list_elems = [np.where(tags == tag)[0] for tag in uniqueTags]
for groupElem in mesh.Get_list_groupElem(dim):
for elems, tag in zip(list_elems, uniqueTags):
if elems.max() > groupElem.Ne:
# We can be here when several elements are the same size.
# For example, in the case of a prism, there are triangles and quadrangles at the same time.
continue
nodes_set = set(groupElem.connect[elems].ravel())
nodes = np.array(list(nodes_set))
# set tag
groupElem.Set_Tag(nodes, t + str(tag))
print(f"{groupElem.elemType} -> Ne = {groupElem.Ne}")
# ----------------------------------------------
# Medit
# ----------------------------------------------
[docs]
def EasyFEA_to_Medit(
mesh: Mesh,
folder: str,
name: str,
dict_tags_converter: dict[str, int] = {},
useBinary=False,
) -> str:
"""Converts EasyFEA mesh to Medit format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
folder : str
Directory to save the Medit file.
name : str
The name of the Medit file, without the extension.
dict_tags_converter : dict[str, int], optional
Dictionary converting string tags to integers (default is {}).
useBinary : bool, optional
Whether to save as binary (default is False).
Returns
-------
str
Path to the saved Medit file.
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
meshioMesh = _EasyFEA_to_Meshio(mesh, dict_tags_converter)
extension = "meshb" if useBinary else "mesh"
filename = Folder.Join(folder, f"{name}.{extension}", mkdir=True)
Display.MyPrint(f"\nCreation of: {filename}\n", "green")
meshio.medit.write(filename, meshioMesh)
return filename
[docs]
def Medit_to_EasyFEA(meditMesh: str) -> Mesh:
"""Converts Medit mesh to EasyFEA format.
Parameters
----------
meditMesh : str
Path to the Medit mesh file.
Returns
-------
Mesh
Converted EasyFEA mesh object.
"""
meshioMesh = meshio.medit.read(meditMesh)
# Please note that your python's meshio must come from https://github.com/matnoel/meshio/tree/medit_higher_order_elements
if len(meshioMesh.cells) == 0:
Display.MyPrintError(
f"The medit mesh:\n {meditMesh}\n does not contain any elements!"
)
return None # type: ignore [return-value]
mesh = _Meshio_to_EasyFEA(meshioMesh)
return mesh
# ----------------------------------------------
# Gmsh
# ----------------------------------------------
[docs]
def EasyFEA_to_Gmsh(mesh: Mesh, folder: str, name: str, useBinary=False) -> str:
"""Converts EasyFEA mesh to Gmsh format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
folder : str
Directory to save the Gmsh file.
name : str
The name of the Gmsh file, without the extension.
useBinary : bool, optional
Whether to save as binary (default is False).
Returns
-------
str
Path to the saved Gmsh file.
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
dict_tags_converter = __Get_dict_tags_converter(mesh)
meshioMesh = _EasyFEA_to_Meshio(mesh, dict_tags_converter, "cell_tags")
filename = Folder.Join(folder, f"{name}.msh", mkdir=True)
Display.MyPrint(f"\nCreation of: {filename}", "green")
meshio.gmsh.write(filename, meshioMesh, "2.2", useBinary)
# Error with 4.1
return filename
[docs]
def Gmsh_to_EasyFEA(gmshMesh: str) -> Mesh:
"""Converts Gmsh mesh to EasyFEA format.
Args:
gmshMesh (str): Path to the Gmsh mesh file.
Returns:
Mesh: Converted EasyFEA mesh object.
"""
meshioMesh: meshio.Mesh = meshio.gmsh.read(gmshMesh)
if len(meshioMesh.cells) == 0:
Display.MyPrintError(
f"The gmsh mesh:\n {gmshMesh}\n does not contain any elements!"
)
return None # type: ignore [return-value]
mesh = _Meshio_to_EasyFEA(meshioMesh)
return mesh
# ----------------------------------------------
# PyVista
# ----------------------------------------------
def _Get_pyvista_cell(groupElem: _GroupElem) -> tuple[pv.CellType, _types.IntArray]:
elemType = groupElem.elemType
if elemType not in DICT_ELEMTYPE_TO_VTK:
raise TypeError(f"{elemType} is not implemented yet.")
# reorder gmsh idx to vtk indexes
if elemType in DICT_GMSH_TO_VTK_INDEXES:
vtkIndexes = DICT_GMSH_TO_VTK_INDEXES[elemType]
elif elemType in ["TRI10", "TRI15"]:
# forced to do this because pyvista simply does not have LAGRANGE_TRIANGLE
# do not put in DICT_VTK_INDEXES because paraview can read LAGRANGE_TRIANGLE without changing the indices
vtkIndexes = np.reshape(groupElem.triangles, (-1, 3)).tolist()
else:
vtkIndexes = np.arange(groupElem.nPe).tolist()
# get groupelem connectivity
connect = groupElem.connect[:, vtkIndexes]
connect = np.reshape(connect, (-1, np.shape(vtkIndexes)[-1]))
# create cellData
cellType = DICT_ELEMTYPE_TO_VTK[elemType]
return cellType, connect
[docs]
def EasyFEA_to_PyVista(
mesh: Mesh, coord: Optional[_types.FloatArray] = None, useAllElements=True
) -> pv.UnstructuredGrid:
"""Converts EasyFEA mesh to PyVista Multiblock format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
coord : _types.FloatArray, optional
mesh coordinates, by default None
useAllElements : bool, optional
Use all group of elements, by default True
Uses only the main group of elements if set to False.
Returns
-------
pv.UnstructuredGrid
pyvista mesh
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
# init dict of cell data
dict_cellData: dict[pv.CellType, np.ndarray] = {}
for groupElem in mesh.dict_groupElem.values():
if not useAllElements and groupElem is not mesh.groupElem:
continue
cellType, connect = _Get_pyvista_cell(groupElem)
dict_cellData[cellType] = connect
# get mesh coordinates
if coord is None:
coordinates = mesh.coordGlob
else:
expectedShape = mesh.coordGlob.shape
assert coord.shape == expectedShape, f"coord must be a {expectedShape} array"
coordinates = coord
# get UnstructuredGrid
pyVistaMesh = pv.UnstructuredGrid(dict_cellData, coordinates)
return pyVistaMesh
def _GroupElem_to_PyVista(
groupElem: _GroupElem, elements: Optional[_types.IntArray] = None
) -> pv.UnstructuredGrid:
"""Converts EasyFEA mesh to PyVista Multiblock format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
elements : _types.IntArray, optional
mesh coordinates, by default None
Returns
-------
pv.UnstructuredGrid
pyvista mesh
"""
assert isinstance(groupElem, _GroupElem), "groupElem must be a group of elements!"
cellType, connect = _Get_pyvista_cell(groupElem)
if isinstance(elements, np.ndarray):
assert elements.min() >= 0
assert elements.max() < groupElem.Ne
connect = connect[elements]
pyVistaMesh = pv.UnstructuredGrid({cellType: connect}, groupElem.coordGlob)
return pyVistaMesh
[docs]
def PyVista_to_EasyFEA(pyVistaMesh: Union[pv.UnstructuredGrid, pv.MultiBlock]) -> Mesh:
"""Converts PyVista mesh to EasyFEA format.
Parameters
----------
pyVistaMesh : pv.UnstructuredGrid | pv.MultiBlock
PyVista mesh object.
Returns
-------
Mesh
Converted EasyFEA mesh object.
"""
dict_groupElem: dict[ElemType, _GroupElem] = {}
def read_grid(grid: pv.UnstructuredGrid, part: int):
coordGlob = grid.points
cellTypes = grid.celltypes.astype(int)
for cellTypeId in list(set(cellTypes)):
# get cell and element types
cellType = pv.CellType(cellTypeId)
elemType = DICT_PYVISTA_TO_ELEMTYPE[cellType]
# get connect
connect = grid.cells_dict[cellTypeId].astype(int)
# reorder vtk idx to gmsh/easyfea indexes
if cellType in DICT_VTK_TO_GMSH_INDEXES:
indexes = DICT_VTK_TO_GMSH_INDEXES[cellType]
connect = connect[:, indexes]
if elemType not in dict_groupElem:
groupElem = GroupElemFactory.Create(elemType, connect, coordGlob)
groupElem.Set_Tag(groupElem.nodes, str(part))
else:
groupElem = dict_groupElem[elemType]
# get previous tags
tags = groupElem.nodeTags
nodeTags = [groupElem.Get_Nodes_Tag(tag) for tag in tags]
# concate new data in previous groupElem
newNodes = np.array(list(set(connect.ravel())))
connect = np.concat((groupElem.connect, connect), axis=0)
groupElem = GroupElemFactory.Create(elemType, connect, coordGlob)
# add previous tags
for nodes, tag in zip(nodeTags, tags):
groupElem.Set_Tag(nodes, tag)
# add new tags
groupElem.Set_Tag(newNodes, str(part))
dict_groupElem[elemType] = groupElem
if isinstance(pyVistaMesh, pv.MultiBlock):
pyVistaMesh = pyVistaMesh.as_unstructured_grid_blocks()
# loop over blocks
for part in range(pyVistaMesh.n_blocks):
grid = pyVistaMesh.get_block(part)
if isinstance(grid, pv.UnstructuredGrid):
read_grid(grid, part)
elif isinstance(pyVistaMesh, pv.UnstructuredGrid):
read_grid(pyVistaMesh, 0)
else:
raise TypeError("Wrond type.")
mesh = Mesh(dict_groupElem)
return mesh
# ----------------------------------------------
# Ensight
# ----------------------------------------------
def _Ensight_to_PyVista(geoFile: str) -> pv.MultiBlock:
"""Converts Ensight mesh to PyVista format.
Parameters
----------
geoFile : str
Path to the Ensight geo file.
Returns
-------
Mesh
Converted PyVista mesh object.
"""
# create case file
folder = Folder.Dir(geoFile)
name = Folder.os.path.basename(geoFile).split(".geo")[0]
caseFile = Folder.Join(folder, f"{name}.case")
with open(caseFile, "w") as f:
f.write("FORMAT\n")
f.write("type: ensight\n")
f.write("GEOMETRY\n")
f.write(f"model: 1 {name}.geo\n")
# import case to pyvista
reader = pv.EnSightReader(caseFile)
# get thepyvista Multi pyvista mesh
pyVistaMesh = reader.read()
# remove the created case file
Folder.os.remove(caseFile)
return pyVistaMesh
def _Ensight_to_Meshio(geoFile: str) -> Mesh:
"""Converts Ensight mesh to Meshio format.
Parameters
----------
geoFile : str
Path to the Ensight geo file.
Returns
-------
Mesh
Converted EasyFEA mesh object.
"""
pyVistaMesh = _Ensight_to_PyVista(geoFile)
mesh = PyVista_to_EasyFEA(pyVistaMesh)
meshioMesh = _EasyFEA_to_Meshio(mesh, {})
return meshioMesh
[docs]
def Ensight_to_EasyFEA(geoFile: str) -> Mesh:
"""Converts Ensight mesh to EasyFEA format.
Parameters
----------
geoFile : str
Path to the Ensight geo file.
Returns
-------
Mesh
Converted EasyFEA mesh object.
"""
with open(geoFile, "r") as file:
lines = file.readlines()
dict_ensightType_data: dict[str, list[tuple[str, _types.IntArray]]] = {}
index = 0
while index < len(lines):
line = lines[index].strip()
if line == "coordinates":
index += 1
Nn = int(lines[index].strip())
coordinates = np.array(
[
[
float(value)
# [+-]? (get sign)
# \d+\.\d+ (decimal part of the number)
# e[+-]?\d+ (exponential part of the number)
for value in re.findall(r"[+-]?\d+\.\d+e[+-]?\d+", line)
]
for line in lines[index + 1 : index + 1 + Nn]
],
dtype=float,
)
index += 1 + Nn # don't change
elif line.startswith("part"):
# get description
index += 1
description = lines[index].strip()
tag = re.sub(r"\D", "", description)
# get ensightType
index += 1
ensight = lines[index].strip()
# get Ne
index += 1
Ne = int(lines[index].strip())
# get connect
connect = np.array(
[
[int(value) for value in line.strip().split()]
for line in lines[index + 1 : index + 1 + Ne]
],
dtype=int,
)
# start connect index from 0
connect -= 1
index += 1 + Ne # don't change
# append data
if ensight not in dict_ensightType_data:
dict_ensightType_data[ensight] = [(tag, connect)]
else:
dict_ensightType_data[ensight].append((tag, connect))
else:
index += 1
# create groups of elements
dict_groupElem: dict[ElemType, _GroupElem] = {}
for ensight, list_data in dict_ensightType_data.items():
elemType = DICT_ENSIGHT_TO_ELEMTYPE[ensight]
# import connect
connect = np.concat([data[1] for data in list_data], axis=0, dtype=int)
# make sur connect is unique
unique_rows = set(tuple(row) for row in connect)
connect = np.array(list(unique_rows), dtype=int)
# reorder connect
if ensight in DICT_ENSIGHT_TO_GMSH_INDEXES:
indexes = DICT_ENSIGHT_TO_GMSH_INDEXES[ensight]
connect = connect[:, indexes]
# create the group of elements
groupElem = GroupElemFactory.Create(elemType, connect, coordinates)
# Set tags
for data in list_data:
tag, connect = data
nodes = np.array(list(set(connect.ravel())), dtype=int)
groupElem.Set_Tag(nodes, tag)
# add group of elements
dict_groupElem[elemType] = groupElem
# create the mesh
mesh = Mesh(dict_groupElem)
return mesh
[docs]
def EasyFEA_to_Ensight(mesh: Mesh, folder: str, name: str) -> str:
"""Converts EasyFEA mesh to Gmsh format.
Parameters
----------
mesh : Mesh
EasyFEA mesh object.
folder : str
Directory to save the Ensight .geo file.
name : str
The name of the Ensight .geo file, without the extension.
Returns
-------
str
Path to the saved Ensight .geo file.
"""
assert isinstance(mesh, Mesh), "mesh must be a EasyFEA mesh!"
filename = Folder.Join(folder, f"{name}.geo", mkdir=True)
Nn = mesh.coordGlob.shape[0]
dict_tags_converter = __Get_dict_tags_converter(mesh)
parts = np.unique([value for value in dict_tags_converter.values()])
def get_line(number: int, pos: int = 8):
return f"{' '*(pos-len(str(number)))}{number}"
# get tags with groupElem and elements
dict_tags = {
tag: (groupElem, groupElem.Get_Elements_Tag(tag))
for groupElem in mesh.dict_groupElem.values()
for tag in groupElem.elementTags
}
with open(filename, "w") as file:
file.write("Geometry ensight6 file\n")
file.write(f"{name}\n")
file.write("node id assign\n")
file.write("element id assign\n")
file.write("coordinates\n")
file.write(get_line(Nn) + "\n")
np.savetxt(file, mesh.coordGlob, fmt="%12.5e", delimiter="")
for part in parts:
for tag, (groupElem, elements) in dict_tags.items():
if dict_tags_converter[tag] != part:
continue
# write part (starts at 1)
file.write(f"part{get_line(part+1)}\n")
# write description
file.write(f"{groupElem.topology}_subdomain {part}\n")
# write ensight name
elemType = groupElem.elemType
file.write(f"{DICT_ELEMTYPE_TO_ENSIGHT[elemType]}\n")
# write elements
file.write(get_line(elements.size) + "\n")
# write connect (starts at 1)
connect = groupElem.connect[elements] + 1
if elemType in DICT_GMSH_TO_ENSIGHT_INDEXES:
indexes = DICT_GMSH_TO_ENSIGHT_INDEXES[elemType]
connect = connect[:, indexes]
np.savetxt(file, connect, fmt="%8i", delimiter="")
return filename