Source code for toughio._mesh._mesh

import collections
from copy import deepcopy

import meshio
import numpy as np

from ._filter import MeshFilter
from ._properties import (
    _connections,
    _face_areas,
    _face_normals,
    _faces,
    _materials,
    _qualities,
    _volumes,
)

__all__ = [
    "CellBlock",
    "Mesh",
    "from_meshio",
    "from_pyvista",
]


CellBlock = collections.namedtuple("CellBlock", ["type", "data"])


[docs] class Mesh(object): def __init__( self, points, cells, point_data=None, cell_data=None, field_data=None, point_sets=None, cell_sets=None, ): """ Unstructured toughio mesh. This class is updated following the latest :mod:`meshio` version and brings backward compatibility with its previous versions. Parameters ---------- points : array_like (n_points, 3) Cooordinates of points. cells : list of tuples (cell_type, data) Connectivity of cells. point_data : dict or None, optional, default None Point data arrays. cell_data : dict or None, optional, default None Cell data arrays. field_data : dict or None, optional, default None Field data names. point_sets : dict or None, optional, default None Sets of points. cell_sets : dict or None, optional, default None Sets of cells. """ self.points = points self.cells = cells self.point_data = point_data if point_data else {} self.cell_data = cell_data if cell_data else {} self.field_data = field_data if field_data else {} self.point_sets = point_sets if point_sets else {} self.cell_sets = cell_sets if cell_sets else {} self.label_length = None def __repr__(self): """Represent a :class:`toughio.Mesh`.""" lines = [ "<toughio mesh object>", f" Number of points: {len(self.points)}", ] if len(self.cells) > 0: lines.append(" Number of cells:") for tpe, elems in self.cells: lines.append(f" {tpe}: {len(elems)}") else: lines.append(" No cells.") if self.point_sets: lines.append(f" Point sets: {', '.join(self.point_sets)}") if self.point_data: lines.append(f" Point data: {', '.join(self.point_data)}") if self.cell_data: lines.append(f" Cell data: {', '.join(self.cell_data)}") return "\n".join(lines) def __getitem__(self, islice): """Slice mesh.""" if np.ndim(islice) == 0: islice = [islice] if np.ndim(islice) != 1: raise ValueError() if isinstance(islice[0], (bool, np.bool_)): if len(islice) != self.n_cells: raise ValueError() elif isinstance(islice[0], (int, np.int8, np.int16, np.int32, np.int64)): if np.max(islice) >= self.n_cells: raise ValueError() tmp = islice islice = np.zeros(self.n_cells, dtype=bool) islice[tmp] = True else: raise TypeError() count = 0 cell_idx = [] nodes_to_keep = set() for _, cell_data in self.cells: cell_idx.append([]) for j, cell in enumerate(cell_data): if islice[count]: cell_idx[-1].append(j) for node in cell: nodes_to_keep.add(node) count += 1 # Prune orphaned nodes node_map = {} point_idx = [] count = 0 for i in range(self.n_points): if i in nodes_to_keep: node_map[i] = count point_idx.append(i) count += 1 return Mesh( points=self.points[point_idx], cells=[ ( cell_type, np.array([[node_map[i] for i in cell] for cell in cell_data[idx]]), ) for idx, (cell_type, cell_data) in zip(cell_idx, self.cells) ], point_data={k: v[point_idx] for k, v in self.point_data.items()}, cell_data={k: v[islice] for k, v in self.cell_data.items()}, field_data={k: v for k, v in self.field_data.items()}, )
[docs] def extrude_to_3d(self, height=1.0, axis=2, inplace=True): """ Convert a 2D mesh to 3D by extruding cells along given axis. Parameters ---------- height : scalar or array_like, optional, default 1.0 Height of extrusion. axis : int (0, 1 or 2), optional, default 2 Axis along which extrusion is performed. inplace : bool, optional, default True If `False`, return a new :class:`toughio.Mesh`. Returns ------- toughio.Mesh Extruded mesh (only if ``inplace == False``). """ if axis not in [0, 1, 2]: raise ValueError("axis must be 0, 1 or 2.") mesh = self if inplace else deepcopy(self) height = [height] if np.ndim(height) == 0 else height npts, nh = len(mesh.points), len(height) if mesh.points.shape[1] == 3: if len(set(mesh.points[:, axis])) != 1: raise ValueError(f"Cannot extrude mesh along axis {axis}.") else: mesh.points = np.column_stack((mesh.points, np.zeros(npts))) if axis != 2: mesh.points[:, [axis, 2]] = mesh.points[:, [2, axis]] extra_points = np.array(mesh.points) for h in height: extra_points[:, axis] += h mesh.points = np.vstack((mesh.points, extra_points)) for k, v in mesh.point_data.items(): mesh.point_data[k] = ( np.tile(v, nh + 1) if np.ndim(v) == 1 else np.tile(v, (nh + 1, 1)) ) extruded_types = { "triangle": "wedge", "quad": "hexahedron", } cells = [] cell_data = {k: mesh.split(v) for k, v in mesh.cell_data.items()} for ic, c in enumerate(mesh.cells): if c.type in extruded_types: extruded_type = extruded_types[c.type] nr, nc = c.data.shape cell = CellBlock(extruded_type, np.tile(c.data, (nh, 2))) for i in range(nh): ibeg, iend = i * nr, (i + 1) * nr cell.data[ibeg:iend, :nc] += i * npts cell.data[ibeg:iend, nc:] += (i + 1) * npts cells.append(cell) for k, v in cell_data.items(): v[ic] = ( np.tile(v[ic], nh) if np.ndim(v[ic]) == 1 else np.tile(v[ic], (nh, 1)) ) mesh.cells = cells mesh.cell_data = {k: np.concatenate(v) for k, v in cell_data.items()} if mesh.field_data: for k in mesh.field_data: mesh.field_data[k][1] = 3 if not inplace: return mesh
[docs] def prune_duplicates(self, inplace=True): """ Delete duplicate points and cells. Parameters ---------- inplace : bool, optional, default True If `False`, return a new :class:`toughio.Mesh`. Returns ------- toughio.Mesh Pruned mesh (only if ``inplace == False``). Note ---- Does not preserve points order from original array in mesh. """ mesh = self if inplace else deepcopy(self) cells = [[c.type, c.data] for c in mesh.cells] # Prune duplicate points unique_points, pind, pinv = np.unique( mesh.points, axis=0, return_index=True, return_inverse=True, ) if len(unique_points) < len(mesh.points): mesh.points = unique_points for k, v in mesh.point_data.items(): mesh.point_data[k] = v[pind] for ic, (k, v) in enumerate(cells): cells[ic][1] = pinv[v] # Prune duplicate cells cell_data = {k: mesh.split(v) for k, v in mesh.cell_data.items()} for ic, (k, v) in enumerate(cells): vsort = np.sort(v, axis=1) _, order = np.unique(vsort, axis=0, return_index=True) cells[ic][1] = v[order] for kk, vv in cell_data.items(): cell_data[kk][ic] = vv[ic][order] mesh.cells = cells mesh.cell_data = {k: np.concatenate(v) for k, v in cell_data.items()} if not inplace: return mesh
[docs] def split(self, arr): """ Split input array into subarrays for each cell block in mesh. Parameters ---------- arr : array_like Input array. Returns ------- list of array_like List of subarrays. """ if len(arr) != self.n_cells: raise ValueError() sizes = np.cumsum([len(c.data) for c in self.cells]) return np.split(np.asarray(arr), sizes[:-1])
[docs] def to_meshio(self): """ Convert mesh to :class:`meshio.Mesh`. Returns ------- meshio.Mesh Output mesh. """ keys = ["points", "point_data", "field_data"] kwargs = {key: getattr(self, key) for key in keys} cell_data = {k: self.split(v) for k, v in self.cell_data.items()} kwargs.update( { "cells": self.cells, "cell_data": cell_data, "point_sets": self.point_sets, "cell_sets": self.cell_sets, } ) return meshio.Mesh(**kwargs)
[docs] def to_pyvista(self): """ Convert mesh to :class:`pyvista.UnstructuredGrid`. Returns ------- pyvista.UnstructuredGrid Output mesh. """ try: import pyvista import vtk from ._common import meshio_to_vtk_type, vtk_type_to_numnodes VTK9 = vtk.vtkVersion().GetVTKMajorVersion() >= 9 except ImportError: raise ImportError( "Converting to pyvista.UnstructuredGrid requires pyvista to be installed." ) # Extract cells from toughio.Mesh object offset = [] cells = [] cell_type = [] next_offset = 0 for c in self.cells: vtk_type = meshio_to_vtk_type[c.type] numnodes = vtk_type_to_numnodes[vtk_type] cells.append( np.hstack((np.full((len(c.data), 1), numnodes), c.data)).ravel() ) cell_type += [vtk_type] * len(c.data) if not VTK9: offset += [next_offset + i * (numnodes + 1) for i in range(len(c.data))] next_offset = offset[-1] + numnodes + 1 # Create pyvista.UnstructuredGrid object points = self.points if points.shape[1] == 2: points = np.hstack((points, np.zeros((len(points), 1)))) if VTK9: mesh = pyvista.UnstructuredGrid( np.concatenate(cells).astype(np.int64, copy=False), np.array(cell_type), np.array(points, np.float64), ) else: mesh = pyvista.UnstructuredGrid( np.array(offset), np.concatenate(cells).astype(np.int64, copy=False), np.array(cell_type), np.array(points, np.float64), ) # Set point data mesh.point_data.update( {k: np.array(v, np.float64) for k, v in self.point_data.items()} ) # Set cell data mesh.cell_data.update(self.cell_data) return mesh
[docs] def write_tough(self, filename="MESH", **kwargs): """ Write TOUGH `MESH` file. Parameters ---------- filename : str, pathlike or buffer, optional, default 'MESH' Output file name or buffer. Other Parameters ---------------- nodal_distance : str ('line' or 'orthogonal'), optional, default 'line' Method to calculate connection nodal distances: - 'line': distance between node and common face along connecting line (distance is not normal), - 'orthogonal' : distance between node and its orthogonal projection onto common face (shortest distance). material_name : dict or None, default None Rename cell material. material_end : str, array_like or None, default None Move cells to bottom of block 'ELEME' if their materials is in `material_end`. incon : bool, optional, default False If `True`, initial conditions will be written in file `INCON`. eos : str or None, optional, default None Equation of State. gravity : array_like or None, optional, default None Gravity direction vector. """ self.write(filename, file_format="tough", **kwargs)
[docs] def write_incon(self, filename="INCON", eos=None): """ Write TOUGH `INCON` file. Parameters ---------- filename : str, pathlike or buffer, optional, default 'INCON' Output file name or buffer. eos : str or None, optional, default None Equation of State. Note ---- Mostly useful to restart a simulation with other initial conditions but with the same mesh. """ from .tough._tough import check_incon, init_incon, write_incon primary_variables, porosities, permeabilities, phase_compositions = init_incon( self ) incon = check_incon( True, primary_variables, porosities, permeabilities, phase_compositions, self.n_cells, eos, ) if incon: write_incon( filename, self.labels, primary_variables, porosities, permeabilities, phase_compositions, eos, )
[docs] def read_output( self, file_or_output, time_step=-1, labels_order=None, connection=False ): """ Import TOUGH results to the mesh. Parameters ---------- file_or_output : str, pathlike, buffer, namedtuple or list of namedtuple Input file name or buffer, or output data. time_step : int, optional, default -1 Data for given time step to import. Default is last time step. labels_order : list of array_like or None, optional, default None List of labels. If None, output will be assumed ordered. connection : bool, optional, default False Only for standard TOUGH output file. If `True`, read data related to connections. """ from .. import read_output from .._io.output._common import Output, reorder_labels if not isinstance(time_step, int): raise TypeError() if isinstance(file_or_output, str): out = read_output(file_or_output, connection=connection) else: out = file_or_output if not isinstance(out, Output): if not (-len(out) <= time_step < len(out)): raise ValueError() out = out[time_step] if out.type == "element": if labels_order is not None: out = reorder_labels(out, labels_order) self.cell_data.update(out.data) elif out.type == "connection": centers = self.centers labels_map = {k: v for v, k in enumerate(self.labels)} data = { k: [[[0.0, 0.0, 0.0]] for _ in range(self.n_cells)] for k in out.data } for i, (label1, label2) in enumerate(out.labels): i1, i2 = labels_map[label1], labels_map[label2] line = centers[i1] - centers[i2] line /= np.linalg.norm(line) for k, v in out.data.items(): iv = i1 if v[i] > 0.0 else i2 data[k][iv].append(v[i] * line) data = { k: np.array([np.sum(vv, axis=0) for vv in v]) for k, v in data.items() } self.cell_data.update(data)
[docs] def write(self, filename, file_format=None, **kwargs): """ Write mesh to file. Parameters ---------- filename : str, pathlike or buffer Output file name or buffer. file_format : str or None, optional, default None Output file format. If `None`, it will be guessed from file's extension. To write TOUGH MESH, `file_format` must be specified as 'tough' (no specific extension exists for TOUGH MESH). Other Parameters ---------------- nodal_distance : str ('line' or 'orthogonal'), optional, default 'line' Only if ``file_format = "tough"``. Method to calculate connection nodal distances: - 'line': distance between node and common face along connecting line (distance is not normal), - 'orthogonal' : distance between node and its orthogonal projection onto common face (shortest distance). material_name : dict or None, default None Only if ``file_format = "tough"``. Rename cell material. material_end : str, array_like or None, default None Only if ``file_format = "tough"``. Move cells to bottom of block 'ELEME' if their materials is in `material_end`. incon : bool, optional, default False Only if ``file_format = "tough"``. If `True`, initial conditions will be written in file `INCON`. protocol : integer, optional, default `pickle.HIGHEST_PROTOCOL` Only if ``file_format = "pickle"``. :mod:`pickle` protocol version. """ from ._helpers import write write(filename, self, file_format, **kwargs)
[docs] def plot(self, *args, **kwargs): """Display mesh using :meth:`pyvista.UnstructuredGrid.plot`.""" mesh = self.to_pyvista() mesh.plot(*args, **kwargs)
[docs] def add_material(self, label, imat): """ Add a material name. Parameters ---------- label : str Material name. imat : int Material ID. """ self.field_data[label] = np.array([imat, 3])
[docs] def add_point_data(self, label, data): """ Add a new point data array. Parameters ---------- label : str Point data array name. data : array_like Point data array. """ if not isinstance(label, str): raise TypeError() if not isinstance(data, (list, tuple, np.ndarray)): raise TypeError() if len(data) != self.n_points: raise ValueError() self.point_data[label] = np.asarray(data)
[docs] def add_cell_data(self, label, data): """ Add a new cell data array. Parameters ---------- label : str Cell data array name. data : array_like Cell data array. """ if not isinstance(label, str): raise TypeError() if not isinstance(data, (list, tuple, np.ndarray)): raise TypeError() if len(data) != self.n_cells: raise ValueError() self.cell_data[label] = np.asarray(data)
[docs] def set_cell_labels(self, labels): """ Set labels to cells. Parameters ---------- labels : array_like Cell labels. Note ---- If labels are not set, a custom labeler with a naming convention different that of TOUGH is used. """ if not isinstance(labels, (list, tuple, np.ndarray)): raise TypeError() if len(labels) != self.n_cells: raise ValueError() if not all(isinstance(label, str) for label in labels): raise ValueError() self._labels = labels
[docs] def set_material(self, material, cells): """ Set material to cells. Parameters ---------- material : str Material name. cells : array_like Indices of cells or array of booleans. """ ints = (int, np.int8, np.int16, np.int32, np.int64) cond_int = all(isinstance(c, ints) for c in cells) cond_bool = all(isinstance(c, (bool, np.bool_)) for c in cells) if cond_int: if np.min(cells) < 0 or np.max(cells) >= self.n_cells: raise ValueError() elif cond_bool: if len(cells) != self.n_cells: raise ValueError() cells = np.arange(self.n_cells)[cells] else: raise TypeError() if len(cells): data = self.cell_data["material"] imat = ( self.field_data[material][0] if material in self.field_data else data.max() + 1 ) data[cells] = imat self.add_cell_data("material", data) self.add_material(material, imat)
[docs] def cell_data_to_point_data(self): """Interpolate cell data to point data.""" from ._common import interpolate_data points = [[] for _ in range(self.n_points)] weights = [[] for _ in range(self.n_points)] i = 0 volumes = self.volumes for cellblock in self.cells: for cell in cellblock.data: for c in cell: points[c].append(i) weights[c].append(volumes[i]) i += 1 point_data = interpolate_data(self._cell_data, points, weights) self._point_data.update(point_data) self._cell_data = {}
[docs] def point_data_to_cell_data(self): """Interpolate point data to cell data.""" from ._common import interpolate_data cells = [c for cell in self.cells for c in cell.data] cell_data = interpolate_data(self._point_data, cells) self._cell_data.update(cell_data) self._point_data = {}
[docs] def near(self, points): """ Return indices of cells nearest to query points. Parameters ---------- points : array_like Coordinates of points to query. Returns ------- int or array_like Index of cell or indices of cells. """ def distance(point, points): """Distance between point to list of points.""" dp = points - point return np.einsum("ij,ij->i", dp, dp) ndim = np.ndim(points) if ndim == 0: raise TypeError() elif ndim == 1: points = np.array([points]) else: points = np.asarray(points) if points.shape[1] != self.points.shape[1]: raise ValueError() centers = self.centers idx = np.argmin([distance(point, centers) for point in points], axis=1) return idx[0] if ndim == 1 else idx
@property def points(self): """Return coordinates of points.""" return self._points @points.setter def points(self, value): self._points = value @property def cells(self): """Return connectivity of cells.""" return self._cells @cells.setter def cells(self, value): self._cells = [ CellBlock(*c) if isinstance(c, (list, tuple)) else CellBlock(c.type, c.data) for c in value ] @property def point_data(self): """Return point data arrays.""" return self._point_data @point_data.setter def point_data(self, value): self._point_data = value @property def cell_data(self): """Return cell data arrays.""" return self._cell_data @cell_data.setter def cell_data(self, value): self._cell_data = value @property def field_data(self): """Return field data names.""" return self._field_data @field_data.setter def field_data(self, value): self._field_data = value @property def point_sets(self): """Return sets of points.""" return self._point_sets @point_sets.setter def point_sets(self, value): self._point_sets = value @property def cell_sets(self): """Return sets of cells.""" return self._cell_sets @cell_sets.setter def cell_sets(self, value): self._cell_sets = value @property def n_points(self): """Return number of points.""" return len(self.points) @property def n_cells(self): """Return number of cells.""" return sum(len(c.data) for c in self.cells) @property def label_length(self): """Return length of labels.""" return self._label_length @label_length.setter def label_length(self, value): self._label_length = value @property def labels(self): """Return labels of cell in mesh.""" from ._common import labeler return ( self._labels if hasattr(self, "_labels") and self._labels else labeler(self.n_cells, self.label_length) ) @property def centers(self): """Return node centers of cell in mesh.""" return np.concatenate([self.points[c.data].mean(axis=1) for c in self.cells]) @property def materials(self): """Return materials of cell in mesh.""" return _materials(self) @property def faces(self): """Return connectivity of faces of cell in mesh.""" out = _faces(self) arr = np.full((self.n_cells, 6, 4), -1) for i, x in enumerate(out): arr[i, : len(x[0]), : x[0].shape[1]] = x[0] if len(x) > 1: arr[i, len(x[0]) : len(x[0]) + len(x[1]), : x[1].shape[1]] = x[1] return arr @property def face_normals(self): """Return normal vectors of faces in mesh.""" return _face_normals(self) @property def face_areas(self): """Return areas of faces in mesh.""" return _face_areas(self) @property def volumes(self): """Return volumes of cell in mesh.""" return _volumes(self) @property def connections(self): """ Return mesh connections. Assume conformity and that points and cells are uniquely defined in mesh. Note ---- Only for 3D meshes and first order cells. """ return _connections(self) @property def qualities(self): """ Return qualities of cells in mesh. The quality of a cell is measured as the minimum cosine angle between the connection line and the interface normal vectors. """ return np.array([np.min(out) for out in _qualities(self)]) @property def dim(self): """Return mesh dimension.""" celltypes = set([cell.type for cell in self.cells]) if celltypes.intersection({"tetra", "pyramid", "wedge", "hexahedron"}): return 3 elif celltypes.intersection({"quad", "triangle"}): return 2 else: raise ValueError() @property def filter(self): """ Filter mesh. Parameters ---------- filter_ : str, optional, default 'box' Filter method. Returns ------- array_like Indices of cells filtered. """ return MeshFilter(self)
[docs] def from_meshio(mesh, material="dfalt"): """ Convert a :class:`meshio.Mesh` to :class:`toughio.Mesh`. Parameters ---------- mesh : meshio.Mesh Input mesh. material : str, optional, default 'dfalt' Default material name. Returns ------- toughio.Mesh Output mesh. """ from ._helpers import get_material_key if not isinstance(mesh, meshio.Mesh): raise TypeError() if not isinstance(material, str): raise TypeError() if mesh.cell_data: cells = mesh.cells cell_data = mesh.cell_data key = get_material_key(cell_data) if key: cell_data["material"] = cell_data.pop(key) cell_data = {k: np.concatenate(v) for k, v in cell_data.items()} else: cells = mesh.cells cell_data = {} out = Mesh( points=mesh.points, cells=cells, point_data=mesh.point_data, cell_data=cell_data, field_data=mesh.field_data, point_sets=( mesh.point_sets if hasattr(mesh, "point_sets") else mesh.node_sets if hasattr(mesh, "node_sets") else None ), cell_sets=mesh.cell_sets if hasattr(mesh, "cell_sets") else None, ) if "material" not in out.cell_data: imat = ( np.max([v[0] for v in mesh.field_data.values() if v[1] == 3]) + 1 if mesh.field_data else 1 ) out.cell_data["material"] = np.full(out.n_cells, imat, dtype=np.int64) out.field_data[material] = np.array([imat, 3]) return out
[docs] def from_pyvista(mesh, material="dfalt"): """ Convert a :class:`pyvista.UnstructuredGrid` to :class:`toughio.Mesh`. Parameters ---------- mesh : pyvista.UnstructuredGrid Input mesh. material : str, optional, default 'dfalt' Default material name. Returns ------- toughio.Mesh Output mesh. """ try: import pyvista import vtk from ._common import vtk_to_meshio_type VTK9 = vtk.vtkVersion().GetVTKMajorVersion() >= 9 except ImportError: raise ImportError( "Converting pyvista.UnstructuredGrid requires pyvista to be installed." ) if not isinstance(mesh, pyvista.UnstructuredGrid): raise TypeError() if not isinstance(material, str): raise TypeError() # Copy useful arrays to avoid repeated calls to properties vtk_offset = mesh.offset vtk_cells = mesh.cells vtk_cell_type = mesh.celltypes # Check that meshio supports all cell types in input mesh pixel_voxel = {8, 11} # Handle pixels and voxels for cell_type in np.unique(vtk_cell_type): if not (cell_type in vtk_to_meshio_type or cell_type in pixel_voxel): raise ValueError(f"toughio does not support VTK type {cell_type}.") # Get cells cells = [] c = 0 for offset, cell_type in zip(vtk_offset, vtk_cell_type): numnodes = vtk_cells[offset] if VTK9: cell = vtk_cells[offset + 1 + c : offset + 1 + c + numnodes] c += 1 else: cell = vtk_cells[offset + 1 : offset + 1 + numnodes] cell = ( cell if cell_type not in pixel_voxel else cell[[0, 1, 3, 2]] if cell_type == 8 else cell[[0, 1, 3, 2, 4, 5, 7, 6]] ) cell_type = cell_type if cell_type not in pixel_voxel else cell_type + 1 cell_type = ( vtk_to_meshio_type[cell_type] if cell_type != 7 else f"polygon{numnodes}" ) if len(cells) > 0 and cells[-1][0] == cell_type: cells[-1][1].append(cell) else: cells.append((cell_type, [cell])) for k, c in enumerate(cells): cells[k] = (c[0], np.array(c[1])) # Get point data point_data = {k.replace(" ", "_"): v for k, v in mesh.point_data.items()} # Get cell data cell_data = {k.replace(" ", "_"): v for k, v in mesh.cell_data.items()} # Create toughio.Mesh out = Mesh( points=np.array(mesh.points), cells=cells, point_data=point_data, cell_data=cell_data, ) if "material" not in out.cell_data: imat = 1 out.cell_data["material"] = np.full(out.n_cells, imat, dtype=np.int64) out.field_data[material] = np.array([imat, 3]) return out