class TreeMesh(_TreeMesh, BaseTensorMesh, InnerProducts, TreeMeshIO): """ TreeMesh is a class for adaptive QuadTree (2D) and OcTree (3D) meshes. """ _meshType = "TREE" _aliases = { **BaseTensorMesh._aliases, **DiffOperators._aliases, **{ "ntN": "n_total_nodes", "ntEx": "n_total_edges_x", "ntEy": "n_total_edges_y", "ntEz": "n_total_edges_z", "ntE": "n_total_edges", "ntFx": "n_total_faces_x", "ntFy": "n_total_faces_y", "ntFz": "n_total_faces_z", "ntF": "n_total_faces", "nhN": "n_hanging_nodes", "nhEx": "n_hanging_edges_x", "nhEy": "n_hanging_edges_y", "nhEz": "n_hanging_edges_z", "nhE": "n_hanging_edges", "nhFx": "n_hanging_faces_x", "nhFy": "n_hanging_faces_y", "nhFz": "n_hanging_faces_z", "nhF": "n_hanging_faces", "gridhN": "hanging_nodes", "gridhFx": "hanging_faces_x", "gridhFy": "hanging_faces_y", "gridhFz": "hanging_faces_z", "gridhEx": "hanging_edges_x", "gridhEy": "hanging_edges_y", "gridhEz": "hanging_edges_z", }, } # inheriting stuff from BaseTensorMesh that isn't defined in _QuadTree def __init__(self, h=None, origin=None, **kwargs): if "x0" in kwargs: origin = kwargs.pop("x0") BaseTensorMesh.__init__( self, h, origin ) # TODO:, **kwargs) # pass the kwargs for copy/paste nx = len(self.h[0]) ny = len(self.h[1]) nz = len(self.h[2]) if self.dim == 3 else 2 def is_pow2(num): return ((num & (num - 1)) == 0) and num != 0 if not (is_pow2(nx) and is_pow2(ny) and is_pow2(nz)): raise ValueError("length of cell width vectors must be a power of 2") # Now can initialize cpp tree parent _TreeMesh.__init__(self, self.h, self.origin) if "cell_levels" in kwargs.keys() and "cell_indexes" in kwargs.keys(): inds = kwargs.pop("cell_indexes") levels = kwargs.pop("cell_levels") self.__setstate__((inds, levels)) def __repr__(self): """Plain text representation.""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) top = "\n" + mesh_name + ": {0:2.2f}% filled\n\n".format(self.fill * 100) # Number of cells per level level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] cell_display = ["Level : Number of cells"] cell_display.append("-----------------------") for level in non_zero_levels: cell_display.append("{:^5} : {:^15}".format(level, level_count[level])) cell_display.append("-----------------------") cell_display.append("Total : {:^15}".format(self.nC)) extent_display = [" Mesh Extent "] extent_display.append(" min , max ") extent_display.append(" ---------------------------") dim_label = {0: "x", 1: "y", 2: "z"} for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) extent_display.append( "{}: {:^13},{:^13}".format(dim_label[dim], n_vector[0], n_vector[-1]) ) for i, line in enumerate(extent_display): if i == len(cell_display): cell_display.append(" " * (len(cell_display[0]) - 3 - len(line))) cell_display[i] += 3 * " " + line h_display = [" Cell Widths "] h_display.append(" min , max ") h_display.append("-" * (len(h_display[0]))) h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) for dim in range(self.dim): h_display.append("{:^10}, {:^10}".format(mins[dim], maxs[dim])) for i, line in enumerate(h_display): if i == len(cell_display): cell_display.append(" " * len(cell_display[0])) cell_display[i] += 3 * " " + line return top + "\n".join(cell_display) def _repr_html_(self): """html representation""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] dim_label = {0: "x", 1: "y", 2: "z"} h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) style = " style='padding: 5px 20px 5px 20px;'" # Cell level table: cel_tbl = "<table>\n" cel_tbl += "<tr>\n" cel_tbl += "<th" + style + ">Level</th>\n" cel_tbl += "<th" + style + ">Number of cells</th>\n" cel_tbl += "</tr>\n" for level in non_zero_levels: cel_tbl += "<tr>\n" cel_tbl += "<td" + style + ">{}</td>\n".format(level) cel_tbl += "<td" + style + ">{}</td>\n".format(level_count[level]) cel_tbl += "</tr>\n" cel_tbl += "<tr>\n" cel_tbl += ( "<td style='font-weight: bold; padding: 5px 20px 5px 20px;'> Total </td>\n" ) cel_tbl += "<td" + style + "> {} </td>\n".format(self.nC) cel_tbl += "</tr>\n" cel_tbl += "</table>\n" det_tbl = "<table>\n" det_tbl += "<tr>\n" det_tbl += "<th></th>\n" det_tbl += "<th" + style + " colspan='2'>Mesh extent</th>\n" det_tbl += "<th" + style + " colspan='2'>Cell widths</th>\n" det_tbl += "</tr>\n" det_tbl += "<tr>\n" det_tbl += "<th></th>\n" det_tbl += "<th" + style + ">min</th>\n" det_tbl += "<th" + style + ">max</th>\n" det_tbl += "<th" + style + ">min</th>\n" det_tbl += "<th" + style + ">max</th>\n" det_tbl += "</tr>\n" for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) det_tbl += "<tr>\n" det_tbl += "<td" + style + ">{}</td>\n".format(dim_label[dim]) det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[0]) det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[-1]) det_tbl += "<td" + style + ">{}</td>\n".format(mins[dim]) det_tbl += "<td" + style + ">{}</td>\n".format(maxs[dim]) det_tbl += "</tr>\n" det_tbl += "</table>\n" full_tbl = "<table>\n" full_tbl += "<tr>\n" full_tbl += "<td style='font-weight: bold; font-size: 1.2em; text-align: center;'>{}</td>\n".format( mesh_name ) full_tbl += "<td style='font-size: 1.2em; text-align: center;' colspan='2'>{0:2.2f}% filled</td>\n".format( 100 * self.fill ) full_tbl += "</tr>\n" full_tbl += "<tr>\n" full_tbl += "<td>\n" full_tbl += cel_tbl full_tbl += "</td>\n" full_tbl += "<td>\n" full_tbl += det_tbl full_tbl += "</td>\n" full_tbl += "</tr>\n" full_tbl += "</table>\n" return full_tbl @properties.validator("origin") def _origin_validator(self, change): self._set_origin(change["value"]) @property def vntF(self): """Total number of hanging and non-hanging faces in a [nx,ny,nz] form""" return [self.ntFx, self.ntFy] + ([] if self.dim == 2 else [self.ntFz]) @property def vntE(self): """Total number of hanging and non-hanging edges in a [nx,ny,nz] form""" return [self.ntEx, self.ntEy] + ([] if self.dim == 2 else [self.ntEz]) @property def stencil_cell_gradient(self): if getattr(self, "_stencil_cell_gradient", None) is None: self._stencil_cell_gradient = sp.vstack( [self.stencil_cell_gradient_x, self.stencil_cell_gradient_y] ) if self.dim == 3: self._stencil_cell_gradient = sp.vstack( [self._stencil_cell_gradient, self.stencil_cell_gradient_z] ) return self._stencil_cell_gradient @property def cell_gradient(self): """ Cell centered Gradient operator built off of the faceDiv operator. Grad = - (Mf)^{-1} * Div * diag (volume) """ if getattr(self, "_cell_gradient", None) is None: i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invMat=True) if self.dim == 2: Pi = sp.block_diag([Pafx, Pafy]) elif self.dim == 3: iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) Pi = sp.block_diag([Pafx, Pafy, Pafz]) self._cell_gradient = ( -Pi * MfI * self.face_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient @property def cell_gradient_x(self): """ Cell centered Gradient operator in x-direction (Gradx) Grad = sp.vstack((Gradx, Grady, Gradz)) """ if getattr(self, "_cell_gradient_x", None) is None: nFx = self.nFx i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) MfI = self.get_face_inner_product(invMat=True) MfIx = sp.diags(MfI.diagonal()[:nFx]) self._cell_gradient_x = ( -Pafx * MfIx * self.face_x_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_x @property def cell_gradient_y(self): """ Cell centered Gradient operator in y-direction (Grady) Grad = sp.vstack((Gradx, Grady, Gradz)) """ if getattr(self, "_cell_gradient_y", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invMat=True) MfIy = sp.diags(MfI.diagonal()[nFx : nFx + nFy]) self._cell_gradient_y = ( -Pafy * MfIy * self.face_y_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_y @property def cell_gradient_z(self): """ Cell centered Gradient operator in z-direction (Gradz) Grad = sp.vstack((Gradx, Grady, Gradz)) """ if self.dim == 2: raise TypeError("z derivative not defined in 2D") if getattr(self, "_cell_gradient_z", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) MfI = self.get_face_inner_product(invMat=True) MfIz = sp.diags(MfI.diagonal()[nFx + nFy :]) self._cell_gradient_z = ( -Pafz * MfIz * self.face_z_divergence.T * sp.diags(self.cell_volumes) ) return self._cell_gradient_z @property def face_x_divergence(self): if getattr(self, "_face_x_divergence", None) is None: self._face_x_divergence = self.face_divergence[:, : self.nFx] return self._face_x_divergence @property def face_y_divergence(self): if getattr(self, "_face_y_divergence", None) is None: self._face_y_divergence = self.face_divergence[:, self.nFx : self.nFx + self.nFy] return self._face_y_divergence @property def face_z_divergence(self): if getattr(self, "_face_z_divergence", None) is None: self._face_z_divergence = self.face_divergence[:, self.nFx + self.nFy :] return self._face_z_divergence def point2index(self, locs): """Finds cells that contain the given points. Returns an array of index values of the cells that contain the given points Parameters ---------- locs: array_like of shape (N, dim) points to search for the location of Returns ------- numpy.array of integers of length(N) Cell indices that contain the points """ locs = as_array_n_by_dim(locs, self.dim) inds = self._get_containing_cell_indexes(locs) return inds def cell_levels_by_index(self, indices): """Fast function to return a list of levels for the given cell indices Parameters ---------- index: array_like of length (N) Cell indexes to query Returns ------- numpy.array of length (N) Levels for the cells. """ return self._cell_levels_by_indexes(indices) def get_interpolation_matrix( self, locs, location_type="CC", zeros_outside=False, **kwargs ): """Produces interpolation matrix Parameters ---------- loc : numpy.ndarray Location of points to interpolate to location_type: str What to interpolate location_type can be:: 'Ex' -> x-component of field defined on edges 'Ey' -> y-component of field defined on edges 'Ez' -> z-component of field defined on edges 'Fx' -> x-component of field defined on faces 'Fy' -> y-component of field defined on faces 'Fz' -> z-component of field defined on faces 'N' -> scalar field defined on nodes 'CC' -> scalar field defined on cell centers Returns ------- scipy.sparse.csr_matrix M, the interpolation matrix """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] if "zerosOutside" in kwargs: warnings.warn( "The zerosOutside keyword argument has been deprecated, please use zeros_outside. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) zeros_outside = kwargs["zerosOutside"] locs = as_array_n_by_dim(locs, self.dim) if location_type not in ["N", "CC", "Ex", "Ey", "Ez", "Fx", "Fy", "Fz"]: raise Exception("location_type must be one of N, CC, Ex, Ey, Ez, Fx, Fy, or Fz") if self.dim == 2 and location_type in ["Ez", "Fz"]: raise Exception("Unable to interpolate from Z edges/face in 2D") locs = np.require(np.atleast_2d(locs), dtype=np.float64, requirements="C") if location_type == "N": Av = self._getNodeIntMat(locs, zeros_outside) elif location_type in ["Ex", "Ey", "Ez"]: Av = self._getEdgeIntMat(locs, zeros_outside, location_type[1]) elif location_type in ["Fx", "Fy", "Fz"]: Av = self._getFaceIntMat(locs, zeros_outside, location_type[1]) elif location_type in ["CC"]: Av = self._getCellIntMat(locs, zeros_outside) return Av @property def permute_cells(self): """Permutation matrix re-ordering of cells sorted by x, then y, then z""" # TODO: cache these? P = np.lexsort(self.gridCC.T) # sort by x, then y, then z return sp.identity(self.nC).tocsr()[P] @property def permute_faces(self): """Permutation matrix re-ordering of faces sorted by x, then y, then z""" # TODO: cache these? Px = np.lexsort(self.gridFx.T) Py = np.lexsort(self.gridFy.T) + self.nFx if self.dim == 2: P = np.r_[Px, Py] else: Pz = np.lexsort(self.gridFz.T) + (self.nFx + self.nFy) P = np.r_[Px, Py, Pz] return sp.identity(self.nF).tocsr()[P] @property def permute_edges(self): """Permutation matrix re-ordering of edges sorted by x, then y, then z""" # TODO: cache these? Px = np.lexsort(self.gridEx.T) Py = np.lexsort(self.gridEy.T) + self.nEx if self.dim == 2: P = np.r_[Px, Py] if self.dim == 3: Pz = np.lexsort(self.gridEz.T) + (self.nEx + self.nEy) P = np.r_[Px, Py, Pz] return sp.identity(self.nE).tocsr()[P] def serialize(self): serial = BaseTensorMesh.serialize(self) inds, levels = self.__getstate__() serial["cell_indexes"] = inds.tolist() serial["cell_levels"] = levels.tolist() return serial @classmethod def deserialize(cls, serial): mesh = cls(**serial) return mesh def __reduce__(self): return TreeMesh, (self.h, self.origin), self.__getstate__() cellGrad = deprecate_property("cell_gradient", "cellGrad", removal_version="1.0.0") cellGradx = deprecate_property( "cell_gradient_x", "cellGradx", removal_version="1.0.0" ) cellGrady = deprecate_property( "cell_gradient_y", "cellGrady", removal_version="1.0.0" ) cellGradz = deprecate_property( "cell_gradient_z", "cellGradz", removal_version="1.0.0" ) cellGradStencil = deprecate_property( "cell_gradient_stencil", "cellGradStencil", removal_version="1.0.0" ) nodalGrad = deprecate_property( "nodal_gradient", "nodalGrad", removal_version="1.0.0" ) nodalLaplacian = deprecate_property( "nodal_laplacian", "nodalLaplacian", removal_version="1.0.0" ) faceDiv = deprecate_property("face_divergence", "faceDiv", removal_version="1.0.0") faceDivx = deprecate_property( "face_x_divergence", "faceDivx", removal_version="1.0.0" ) faceDivy = deprecate_property( "face_y_divergence", "faceDivy", removal_version="1.0.0" ) faceDivz = deprecate_property( "face_z_divergence", "faceDivz", removal_version="1.0.0" ) edgeCurl = deprecate_property("edge_curl", "edgeCurl", removal_version="1.0.0") maxLevel = deprecate_property("max_used_level", "maxLevel", removal_version="1.0.0") vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0") areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0") areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0") areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0") area = deprecate_property("face_areas", "area", removal_version="1.0.0") edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0") edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0") edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0") edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0") permuteCC = deprecate_property( "permute_cells", "permuteCC", removal_version="1.0.0" ) permuteF = deprecate_property("permute_faces", "permuteF", removal_version="1.0.0") permuteE = deprecate_property("permute_edges", "permuteE", removal_version="1.0.0") faceBoundaryInd = deprecate_property( "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0" ) cellBoundaryInd = deprecate_property( "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0" ) _aveCC2FxStencil = deprecate_property( "average_cell_to_total_face_x", "_aveCC2FxStencil", removal_version="1.0.0" ) _aveCC2FyStencil = deprecate_property( "average_cell_to_total_face_y", "_aveCC2FyStencil", removal_version="1.0.0" ) _aveCC2FzStencil = deprecate_property( "average_cell_to_total_face_z", "_aveCC2FzStencil", removal_version="1.0.0" ) _cellGradStencil = deprecate_property("stencil_cell_gradient", "_cellGradStencil", removal_version="1.0.0") _cellGradxStencil = deprecate_property("stencil_cell_gradient_x", "_cellGradxStencil", removal_version="1.0.0") _cellGradyStencil = deprecate_property("stencil_cell_gradient_y", "_cellGradyStencil", removal_version="1.0.0") _cellGradzStencil = deprecate_property("stencil_cell_gradient_z", "_cellGradzStencil", removal_version="1.0.0")
class TensorMesh( BaseTensorMesh, BaseRectangularMesh, DiffOperators, InnerProducts, TensorMeshIO, InterfaceMixins, ): """ Tensor mesh class. Tensor meshes are numerical grids whose cell centers, nodes, faces, edges, widths, volumes, etc... can be directly expressed as tensor products. The axes defining coordinates of the mesh are orthogonal. And cell properties along one axis do not vary with respect to the position along any other axis. Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths along each axis. The length of the iterable object is equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]* . Along each axis, the user has 3 choices for defining the cells widths: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at *[0, 0, 0]* . For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N') (see Examples). See Also -------- utils.unpack_widths : The function used to expand a tuple to generate widths. Examples -------- An example of a 2D tensor mesh is shown below. Here we use a list of tuple to define the discretization along the x-axis and a numpy array to define the discretization along the y-axis. We also use a string argument to center the x-axis about x = 0 and set the top of the mesh to y = 0. >>> from discretize import TensorMesh >>> import matplotlib.pyplot as plt >>> ncx = 10 # number of core mesh cells in x >>> dx = 5 # base cell width x >>> npad_x = 3 # number of padding cells in x >>> exp_x = 1.25 # expansion rate of padding cells in x >>> ncy = 24 # total number of mesh cells in y >>> dy = 5 # base cell width y >>> hx = [(dx, npad_x, -exp_x), (dx, ncx), (dx, npad_x, exp_x)] >>> hy = dy * np.ones(ncy) >>> mesh = TensorMesh([hx, hy], origin='CN') >>> fig = plt.figure(figsize=(5,5)) >>> ax = fig.add_subplot(111) >>> mesh.plot_grid(ax=ax) >>> plt.show() """ _meshType = "TENSOR" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **BaseTensorMesh._aliases, } def __repr__(self): """Plain text representation.""" fmt = "\n {}: {:,} cells\n\n".format(type(self).__name__, self.nC) fmt += 22 * " " + "MESH EXTENT" + 13 * " " + "CELL WIDTH FACTOR\n" fmt += " dir nC min max min max " fmt += " max\n --- --- " + 27 * "-" + " " + 18 * "-" + " ------\n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " {}".format(name) fmt += " {:6}".format(iattr["nC"]) for p in ["min", "max"]: fmt += " {:13,.2f}".format(iattr[p]) for p in ["h_min", "h_max"]: fmt += " {:9,.2f}".format(iattr[p]) fmt += "{:8,.2f}".format(iattr["max_fact"]) fmt += "\n" # End row fmt += "\n" return fmt def _repr_html_(self): """HTML representation.""" style = " style='padding: 5px 20px 5px 20px;'" fmt = "<table>\n" fmt += " <tr>\n" fmt += " <td style='font-weight: bold; font-size: 1.2em; text-align" fmt += ": center;' colspan='3'>{}</td>\n".format(type(self).__name__) fmt += " <td style='font-size: 1.2em; text-align: center;'" fmt += "colspan='4'>{:,} cells</td>\n".format(self.nC) fmt += " </tr>\n" fmt += " <tr>\n" fmt += " <th></th>\n" fmt += " <th></th>\n" fmt += " <th colspan='2'" + style + ">MESH EXTENT</th>\n" fmt += " <th colspan='2'" + style + ">CELL WIDTH</th>\n" fmt += " <th" + style + ">FACTOR</th>\n" fmt += " </tr>\n" fmt += " <tr>\n" fmt += " <th" + style + ">dir</th>\n" fmt += " <th" + style + ">nC</th>\n" fmt += " <th" + style + ">min</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " <th" + style + ">min</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " </tr>\n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " <tr>\n" # Start row fmt += " <td" + style + ">{}</td>\n".format(name) fmt += " <td" + style + ">{}</td>\n".format(iattr["nC"]) for p in ["min", "max", "h_min", "h_max", "max_fact"]: fmt += " <td" + style + ">{:,.2f}</td>\n".format(iattr[p]) fmt += " </tr>\n" # End row fmt += "</table>\n" return fmt # --------------- Geometries --------------------- @property def cell_volumes(self): """Return cell volumes Calling this property will compute and return the volumes of the tensor mesh cells. Returns ------- (n_cells) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* Returns the cell widths - *2D:* Returns the cell areas - *3D:* Returns the cell volumes """ if getattr(self, "_cell_volumes", None) is None: vh = self.h # Compute cell volumes if self.dim == 1: self._cell_volumes = mkvc(vh[0]) elif self.dim == 2: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(vh[0], vh[1])) elif self.dim == 3: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(mkvc(np.outer(vh[0], vh[1])), vh[2])) return self._cell_volumes @property def face_x_areas(self): """Returns the areas of the x-faces Calling this property will compute and return the areas of faces whose normal vector is along the x-axis. Returns ------- (n_faces_x) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* Numpy array of ones whose length is equal to the number of nodes - *2D:* Areas of x-faces (equivalent to the lengths of y-edges) - *3D:* Areas of x-faces """ if getattr(self, "_face_x_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: areaFx = np.ones(n[0] + 1) elif self.dim == 2: areaFx = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: areaFx = np.outer(np.ones(n[0] + 1), mkvc(np.outer(vh[1], vh[2]))) self._face_x_areas = mkvc(areaFx) return self._face_x_areas @property def face_y_areas(self): """Returns the areas of the y-faces Calling this property will compute and return the areas of faces whose normal vector is along the y-axis. Note that only 2D and 3D tensor meshes have z-faces. Returns ------- (n_faces_y) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have y-faces - *2D:* Areas of y-faces (equivalent to the lengths of x-edges) - *3D:* Areas of y-faces """ if getattr(self, "_face_y_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: raise Exception("1D meshes do not have y-Faces") elif self.dim == 2: areaFy = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: areaFy = np.outer(vh[0], mkvc(np.outer(np.ones(n[1] + 1), vh[2]))) self._face_y_areas = mkvc(areaFy) return self._face_y_areas @property def face_z_areas(self): """Returns the areas of the z-faces Calling this property will compute and return the areas of faces whose normal vector is along the z-axis. Note that only 3D tensor meshes will have z-faces. Returns ------- (n_faces_z) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have z-faces - *2D:* N/A since 2D meshes do not have z-faces - *3D:* Areas of z-faces """ if getattr(self, "_face_z_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have z-Faces".format(self.dim)) elif self.dim == 3: areaFz = np.outer(vh[0], mkvc(np.outer(vh[1], np.ones(n[2] + 1)))) self._face_z_areas = mkvc(areaFz) return self._face_z_areas @property def face_areas(self): """Returns the areas of all faces in the mesh Calling this property will compute and return the areas of all faces as a 1D numpy array. The returned quantity is ordered x-face areas, then y-face areas, then z-face areas. Returns ------- (n_faces) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-face areas - *2D:* returns the x-face and y-face areas in order; i.e. y-edge and x-edge lengths, respectively - *3D:* returns the x, y and z-face areas in order """ if self.dim == 1: return self.face_x_areas elif self.dim == 2: return np.r_[self.face_x_areas, self.face_y_areas] elif self.dim == 3: return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas] @property def edge_x_lengths(self): """Returns the x-edge lengths Calling this property will compute and return the lengths of edges parallel to the x-axis. Returns ------- (n_edges_x) numpy.ndarray X-edge lengths """ if getattr(self, "_edge_x_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: edgeEx = vh[0] elif self.dim == 2: edgeEx = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: edgeEx = np.outer( vh[0], mkvc(np.outer(np.ones(n[1] + 1), np.ones(n[2] + 1))) ) self._edge_x_lengths = mkvc(edgeEx) return self._edge_x_lengths @property def edge_y_lengths(self): """Returns the y-edge lengths Calling this property will compute and return the lengths of edges parallel to the y-axis. Returns ------- (n_edges_y) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have y-edges - *2D:* Returns y-edge lengths - *3D:* Returns y-edge lengths """ if getattr(self, "_edge_y_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: raise Exception("1D meshes do not have y-edges") elif self.dim == 2: edgeEy = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: edgeEy = np.outer( np.ones(n[0] + 1), mkvc(np.outer(vh[1], np.ones(n[2] + 1))) ) self._edge_y_lengths = mkvc(edgeEy) return self._edge_y_lengths @property def edge_z_lengths(self): """Returns the z-edge lengths Calling this property will compute and return the lengths of edges parallel to the z-axis. Returns ------- (n_edges_z) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *1D:* N/A since 1D meshes do not have z-edges - *2D:* N/A since 2D meshes do not have z-edges - *3D:* Returns z-edge lengths """ if getattr(self, "_edge_z_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have y-edges".format(self.dim)) elif self.dim == 3: edgeEz = np.outer( np.ones(n[0] + 1), mkvc(np.outer(np.ones(n[1] + 1), vh[2])) ) self._edge_z_lengths = mkvc(edgeEz) return self._edge_z_lengths @property def edge_lengths(self): """Returns the lengths of all edges in the mesh Calling this property will compute and return the lengths of all edges in the mesh. The returned quantity is ordered x-edge lengths, then y-edge lengths, then z-edge lengths. Returns ------- (n_edges) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-edge lengths - *2D:* returns the x-edge and y-edge lengths in order - *3D:* returns the x, y and z-edge lengths in order """ if self.dim == 1: return self.edge_x_lengths elif self.dim == 2: return np.r_[self.edge_x_lengths, self.edge_y_lengths] elif self.dim == 3: return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths] return self._edge @property def face_boundary_indices(self): """Returns the indices of the x, (y and z) boundary faces For x, (y and z) faces, this property returns the indices of the faces on the boundaries. That is, the property returns the indices of the x-faces that lie on the x-boundary; likewise for y and z. Note that each Cartesian direction will have both a lower and upper boundary, and the property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- (dim * 2) list of numpy.ndarray of bool The length of list returned depends on the dimension of the mesh. And the length of each array containing the indices depends on the number of faces in each direction. For 1D, 2D and 3D tensor meshes, the returns take the following form: - *1D:* returns [ind_Bx1, ind_Bx2] - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2] - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2] Examples -------- Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices of the x and y-boundary faces. In this case there are 3 x-faces on each x-boundary, and there are 4 y-faces on each y-boundary. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = [1, 1, 1, 1] >>> hy = [2, 2, 2] >>> mesh = TensorMesh([hx, hy]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.face_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.faces_x[ind_Bx1].T) >>> plt.show() """ if self.dim == 1: indxd = self.gridFx == min(self.gridFx) indxu = self.gridFx == max(self.gridFx) return indxd, indxu elif self.dim == 2: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) indzd = self.gridFz[:, 2] == min(self.gridFz[:, 2]) indzu = self.gridFz[:, 2] == max(self.gridFz[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu @property def cell_boundary_indices(self): """Returns the indices of the x, (y and z) boundary cells This property returns the indices of the cells on the x, (y and z) boundaries, respectively. Note that each axis direction will have both a lower and upper boundary. The property will return the indices corresponding to the lower and upper boundaries separately. E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total). In this case, the return is a list of length 4 organized [ind_Bx1, ind_Bx2, ind_By1, ind_By2]:: By2 + ------------- + | | | | Bx1 | | Bx2 | | | | + ------------- + By1 Returns ------- (2 * dim) list of numpy.ndarray of bool The length of list returned depends on the dimension of the mesh (= 2 x dim). And the length of each array containing the indices is equal to the number of cells in the mesh. For 1D, 2D and 3D tensor meshes, the returns take the following form: - *1D:* returns [ind_Bx1, ind_Bx2] - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2] - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2] Examples -------- Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices of the x and y-boundary cells. In this case there are 3 cells touching each x-boundary, and there are 4 cells touching each y-boundary. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> hx = [1, 1, 1, 1] >>> hy = [2, 2, 2] >>> mesh = TensorMesh([hx, hy]) >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.cell_boundary_indices >>> ax = plt.subplot(111) >>> mesh.plot_grid(ax=ax) >>> ax.scatter(*mesh.cell_centers[ind_Bx1].T) >>> plt.show() """ if self.dim == 1: indxd = self.gridCC == min(self.gridCC) indxu = self.gridCC == max(self.gridCC) return indxd, indxu elif self.dim == 2: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) indzd = self.gridCC[:, 2] == min(self.gridCC[:, 2]) indzu = self.gridCC[:, 2] == max(self.gridCC[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu def _repr_attributes(self): """Attributes for the representation of the mesh.""" attrs = {} attrs["names"] = ["x", "y", "z"][: self.dim] # Loop over dimensions. for i in range(self.dim): name = attrs["names"][i] # Name of this dimension attrs[name] = {} # Get min/max node. n_vector = getattr(self, "nodes_" + name) attrs[name]["min"] = np.nanmin(n_vector) attrs[name]["max"] = np.nanmax(n_vector) # Get min/max cell width. h_vector = self.h[i] attrs[name]["h_min"] = np.nanmin(h_vector) attrs[name]["h_max"] = np.nanmax(h_vector) # Get max stretching factor. if len(h_vector) < 2: attrs[name]["max_fact"] = 1.0 else: attrs[name]["max_fact"] = np.nanmax( np.r_[h_vector[:-1] / h_vector[1:], h_vector[1:] / h_vector[:-1]] ) # Add number of cells. attrs[name]["nC"] = self.shape_cells[i] return attrs # DEPRECATIONS vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", future_warn=False) areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0", future_warn=False) areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0", future_warn=False) areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0", future_warn=False) area = deprecate_property("face_areas", "area", removal_version="1.0.0", future_warn=False) edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0", future_warn=False) edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0", future_warn=False) edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0", future_warn=False) edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0", future_warn=False) faceBoundaryInd = deprecate_property( "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0", future_warn=False ) cellBoundaryInd = deprecate_property( "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0", future_warn=False )
class CurvilinearMesh( BaseRectangularMesh, DiffOperators, InnerProducts, InterfaceMixins ): """Curvilinear mesh class. Curvilinear meshes are numerical grids whose cells are general quadrilaterals (2D) or cuboid (3D); unlike tensor meshes (see :class:`~discretize.TensorMesh`) whose cells are rectangles or rectangular prisms. That being said, the combinatorial structure (i.e. connectivity of mesh cells) of curvilinear meshes is the same as tensor meshes. Parameters ---------- node_list : list of array_like List :class:`array_like` containing the gridded x, y (and z) node locations. - For a 2D curvilinear mesh, *node_list* = [X, Y] where X and Y have shape (``n_nodes_x``, ``n_nodes_y``) - For a 3D curvilinear mesh, *node_list* = [X, Y, Z] where X, Y and Z have shape (``n_nodes_x``, ``n_nodes_y``, ``n_nodes_z``) Examples -------- Using the :py:func:`~discretize.utils.example_curvilinear_grid` utility, we provide an example of a curvilinear mesh. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid >>> import matplotlib.pyplot as plt The example grid slightly rotates the nodes in the center of the mesh, >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> x.shape (11, 11) >>> y.shape (11, 11) >>> curvilinear_mesh = CurvilinearMesh([x, y]) >>> curvilinear_mesh.shape_nodes (11, 11) >>> fig = plt.figure(figsize=(5,5)) >>> ax = fig.add_subplot(111) >>> curvilinear_mesh.plot_grid(ax=ax) >>> plt.show() """ _meshType = "Curv" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **{ "gridCC": "cell_centers", "gridN": "nodes", "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } _items = {"node_list"} def __init__(self, node_list, **kwargs): if "nodes" in kwargs: node_list = kwargs.pop("nodes") node_list = tuple(np.asarray(item, dtype=np.float64) for item in node_list) # check shapes of each node array match dim = len(node_list) if dim not in [2, 3]: raise ValueError( f"Only supports 2 and 3 dimensional meshes, saw a node_list of length {dim}" ) for i, nodes in enumerate(node_list): if len(nodes.shape) != dim: raise ValueError( f"Unexpected shape of item in node list, expect array with {dim} dimensions, got {len(nodes.shape)}" ) if node_list[0].shape != nodes.shape: raise ValueError( f"The shape of nodes are not consistent, saw {node_list[0].shape} and {nodes.shape}" ) self._node_list = tuple(node_list) # Save nodes to private variable _nodes as vectors self._nodes = np.ones((self.node_list[0].size, dim)) for i, nodes in enumerate(self.node_list): self._nodes[:, i] = mkvc(nodes) shape_cells = (n - 1 for n in self.node_list[0].shape) # absorb the rest of kwargs, and do not pass to super super().__init__(shape_cells, origin=self.nodes[0]) @property def node_list(self): """Returns the gridded x, y (and z) node locations used to create the mesh. Returns ------- (dim) list of numpy.ndarray Gridded x, y (and z) node locations used to create the mesh. - *2D:* return is a list [X, Y] where X and Y have shape (n_nodes_x, n_nodes_y) - *3D:* return is a list [X, Y, Z] where X, Y and Z have shape (n_nodes_x, n_nodes_y, n_nodes_z) """ return self._node_list @classmethod def deserialize(cls, value, **kwargs): if "nodes" in value: value["node_list"] = value.pop("nodes") return super().deserialize(value, **kwargs) @property def cell_centers(self): """Gridded cell center locations (staggered grid) For 2D or 3D curvilinear meshes, this property returns a numpy array of shape (n_cells, dim) containing the gridded cell center locations. The bottom-front-leftmost cell is the first cell. Returns ------- (n_cells, dim) numpy.ndarray of float The shape of the output array is the number of cells by the dimension. """ if getattr(self, "_cell_centers", None) is None: self._cell_centers = np.concatenate( [self.aveN2CC * self.gridN[:, i] for i in range(self.dim)] ).reshape((-1, self.dim), order="F") return self._cell_centers @property def nodes(self): """Gridded node locations (staggered grid) For 2D or 3D curvilinear meshes, this property returns a numpy array of shape (n_nodes, dim) containing the gridded node locations. The bottom-front-leftmost node is the first node. Returns ------- (n_nodes, dim) numpy.ndarray of float Gridded node locations """ if getattr(self, "_nodes", None) is None: raise Exception("Someone deleted this. I blame you.") return self._nodes @property def faces_x(self): """Gridded x-face locations (staggered grid) This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all x-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the x-faces are faces whose normal vectors are primarily along the x-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded x-face locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x-faces have normal vectors that are primarily along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> x_faces = mesh1.faces_x >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'X-faces'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the x-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> x_faces = mesh2.faces_x >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-faces'], fontsize=16) >>> plt.plot() """ if getattr(self, "_faces_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._faces_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:, :-1, :-1] + n[:, :-1, 1:] + n[:, 1:, :-1] + n[:, 1:, 1:] ) ) for n in N ] self._faces_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_x @property def faces_y(self): """Gridded y-face locations (staggered grid) This property returns a numpy array of shape (n_faces_y, dim) containing gridded locations for all y-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the y-faces are faces whose normal vectors are primarily along the y-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_faces_y, dim) numpy.ndarray of float Gridded y-face locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the y-faces have normal vectors that are primarily along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> y_faces = mesh1.faces_y >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'Y-faces'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> y_faces = mesh2.faces_y >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'Y-faces'], fontsize=16) >>> plt.plot() """ if getattr(self, "_faces_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._faces_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:-1, :, :-1] + n[:-1, :, 1:] + n[1:, :, :-1] + n[1:, :, 1:] ) ) for n in N ] self._faces_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_y @property def faces_z(self): """Gridded z-face locations (staggered grid) This property returns a numpy array of shape (n_faces_z, dim) containing gridded locations for all z-faces in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the z-faces are faces whose normal vectors are primarily along the z-direction. For highly irregular meshes however, this is not the case. Returns ------- (n_faces_z, dim) numpy.ndarray of float Gridded z-face locations (staggered grid) """ if getattr(self, "_faces_z", None) is None: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [ mkvc( 0.25 * (n[:-1, :-1, :] + n[:-1, 1:, :] + n[1:, :-1, :] + n[1:, 1:, :]) ) for n in N ] self._faces_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_z @property def faces(self): """Gridded face locations (staggered grid) This property returns a numpy array of shape (n_faces, dim) containing gridded locations for all faces in the mesh (staggered grid). This is equivalent to calling np.r_[faces_x, faces_y, faces_z]. Returns ------- (n_faces, dim) numpy.ndarray of float Gridded face locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x and y-faces have normal vectors that are primarily along the x and y-directions, respectively. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> faces = mesh1.faces >>> x_faces = faces[:mesh1.n_faces_x] >>> y_faces = faces[mesh1.n_faces_x:] >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g') >>> ax1.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-faces are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> faces = mesh2.faces >>> x_faces = faces[:mesh2.n_faces_x] >>> y_faces = faces[mesh2.n_faces_x:] >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r') >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g') >>> ax2.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16) >>> plt.plot() """ faces = np.r_[self.faces_x, self.faces_y] if self.dim > 2: faces = np.r_[faces, self.faces_z] return faces @property def edges_x(self): """Gridded x-edge locations (staggered grid) This property returns a numpy array of shape (n_edges_x, dim) containing gridded locations for all x-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the x-edges are edges oriented primarily along the x-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_edges_x, dim) numpy.ndarray of float Gridded x-edge locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x-edges are primarily oriented along the x-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> x_edges = mesh1.edges_x >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the x-edges are not aligned primarily along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> x_edges = mesh2.edges_x >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.plot() """ if getattr(self, "_edges_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._edges_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:-1, :, :] + n[1:, :, :])) for n in N] self._edges_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_x @property def edges_y(self): """Gridded y-edge locations (staggered grid) This property returns a numpy array of shape (n_edges_y, dim) containing gridded locations for all y-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the y-edges are edges oriented primarily along the y-direction. For highly irregular meshes however, this is not the case; see the examples below. Returns ------- (n_edges_y, dim) numpy.ndarray of float Gridded y-edge locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the y-edges are primarily oriented along the y-direction. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> y_edges = mesh1.edges_y >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r') >>> ax1.legend(['Mesh', 'Y-edges'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-edges are not aligned primarily along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> y_edges = mesh2.edges_y >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r') >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16) >>> plt.plot() """ if getattr(self, "_edges_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._edges_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:, :-1, :] + n[:, 1:, :])) for n in N] self._edges_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_y @property def edges_z(self): """Gridded z-edge locations (staggered grid) This property returns a numpy array of shape (n_edges_z, dim) containing gridded locations for all z-edges in the mesh (staggered grid). For curvilinear meshes whose structure is minimally staggered, the z-edges are faces whose normal vectors are primarily along the z-direction. For highly irregular meshes however, this is not the case. Returns ------- (n_edges_z, dim) numpy.ndarray of float Gridded z-edge locations (staggered grid) """ if getattr(self, "_edges_z", None) is None and self.dim == 3: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [mkvc(0.5 * (n[:, :, :-1] + n[:, :, 1:])) for n in N] self._edges_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_z @property def edges(self): """Gridded edge locations (staggered grid) This property returns a numpy array of shape (n_edges, dim) containing gridded locations for all edges in the mesh (staggered grid). This is equivalent to calling np.r_[edges_x, edges_y, edges_z]. Returns ------- (n_edges, dim) numpy.ndarray of float Gridded edge locations (staggered grid) Examples -------- Here, we provide an example of a minimally staggered curvilinear mesh. In this case, the x and y-edges have normal vectors that are primarily along the x and y-directions, respectively. >>> from discretize import CurvilinearMesh >>> from discretize.utils import example_curvilinear_grid, mkvc >>> from matplotlib import pyplot as plt >>> x, y = example_curvilinear_grid([10, 10], "rotate") >>> mesh1 = CurvilinearMesh([x, y]) >>> edges = mesh1.edges >>> x_edges = edges[:mesh1.n_edges_x] >>> y_edges = edges[mesh1.n_edges_x:] >>> fig1 = plt.figure(figsize=(5, 5)) >>> ax1 = fig1.add_subplot(111) >>> mesh1.plot_grid(ax=ax1) >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g') >>> ax1.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16) >>> plt.plot() Here, we provide an example of a highly irregular curvilinear mesh. In this case, the y-edges are not defined by normal vectors along a particular direction. >>> x, y = example_curvilinear_grid([10, 10], "sphere") >>> mesh2 = CurvilinearMesh([x, y]) >>> edges = mesh2.edges >>> x_edges = edges[:mesh2.n_edges_x] >>> y_edges = edges[mesh2.n_edges_x:] >>> fig2 = plt.figure(figsize=(5, 5)) >>> ax2 = fig2.add_subplot(111) >>> mesh2.plot_grid(ax=ax2) >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r') >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g') >>> ax2.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16) >>> plt.show() """ edges = np.r_[self.edges_x, self.edges_y] if self.dim > 2: edges = np.r_[edges, self.edges_z] return edges @property def boundary_nodes(self): """Gridded boundary node locations This property returns a numpy array of shape (n_boundary_nodes, dim) containing the gridded locations of the nodes on the boundary of the mesh. Returns ------- (n_boundary_nodes, dim) numpy.ndarray of float Gridded boundary node locations """ return self.nodes[make_boundary_bool(self.shape_nodes)] @property def boundary_edges(self): """Gridded boundary edge locations This property returns a numpy array of shape (n_boundary_edges, dim) containing the gridded locations of the edges on the boundary of the mesh. The returned quantity is organized *np.r_[edges_x, edges_y, edges_z]* . Returns ------- (n_boundary_edges, dim) numpy.ndarray of float Gridded boundary edge locations """ if self.dim == 2: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, dir="y")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, dir="x")] return np.r_[ex, ey] elif self.dim == 3: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, dir="yz")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, dir="xz")] ez = self.edges_z[make_boundary_bool(self.shape_edges_z, dir="xy")] return np.r_[ex, ey, ez] @property def boundary_faces(self): """Gridded locations of non-hanging x-faces This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all non-hanging x-faces. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded locations of all non-hanging x-faces """ fx = self.faces_x[make_boundary_bool(self.shape_faces_x, dir="x")] fy = self.faces_y[make_boundary_bool(self.shape_faces_y, dir="y")] if self.dim == 2: return np.r_[fx, fy] elif self.dim == 3: fz = self.faces_z[make_boundary_bool(self.shape_faces_z, dir="z")] return np.r_[fx, fy, fz] @property def boundary_face_outward_normals(self): """Outward normals of boundary faces For all boundary faces in the mesh, this property returns the unit vectors denoting the outward normals to the boundary. The returned quantity is a numpy array of shape (n_boundary_faces, dim). Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Outward normals of boundary faces """ is_bxm = np.zeros(self.shape_faces_x, order="F", dtype=bool) is_bxm[0, :] = True is_bxm = is_bxm.reshape(-1, order="F") is_bym = np.zeros(self.shape_faces_y, order="F", dtype=bool) is_bym[:, 0] = True is_bym = is_bym.reshape(-1, order="F") is_b = np.r_[ make_boundary_bool(self.shape_faces_x, dir="x"), make_boundary_bool(self.shape_faces_y, dir="y"), ] switch = np.r_[is_bxm, is_bym] if self.dim == 3: is_bzm = np.zeros(self.shape_faces_z, order="F", dtype=bool) is_bzm[:, :, 0] = True is_bzm = is_bzm.reshape(-1, order="F") is_b = np.r_[is_b, make_boundary_bool(self.shape_faces_z, dir="z")] switch = np.r_[switch, is_bzm] face_normals = self.face_normals.copy() face_normals[switch] *= -1 return face_normals[is_b] # --------------- Geometries --------------------- # # # ------------------- 2D ------------------------- # # node(i,j) node(i,j+1) # A -------------- B # | | # | cell(i,j) | # | I | # | | # D -------------- C # node(i+1,j) node(i+1,j+1) # # ------------------- 3D ------------------------- # # # node(i,j,k+1) node(i,j+1,k+1) # E --------------- F # /| / | # / | / | # / | / | # node(i,j,k) node(i,j+1,k) # A -------------- B | # | H ----------|---- G # | /cell(i,j) | / # | / I | / # | / | / # D -------------- C # node(i+1,j,k) node(i+1,j+1,k) @property def cell_volumes(self): """Return cell volumes Calling this property will compute and return a 1D array containing the volumes of mesh cells. Returns ------- (n_cells) numpy.ndarray The quantity returned depends on the dimensions of the mesh: - *2D:* Returns the cell areas - *3D:* Returns the cell volumes """ if getattr(self, "_cell_volumes", None) is None: if self.dim == 2: A, B, C, D = index_cube("ABCD", self.vnN) normal, area = face_info( np.c_[self.gridN, np.zeros((self.nN, 1))], A, B, C, D ) self._cell_volumes = area elif self.dim == 3: # Each polyhedron can be decomposed into 5 tetrahedrons # However, this presents a choice so we may as well divide in # two ways and average. A, B, C, D, E, F, G, H = index_cube("ABCDEFGH", self.vnN) vol1 = ( volume_tetrahedron(self.gridN, A, B, D, E) + volume_tetrahedron(self.gridN, B, E, F, G) # cutted edge top + volume_tetrahedron(self.gridN, B, D, E, G) # cutted edge top + volume_tetrahedron(self.gridN, B, C, D, G) # middle + volume_tetrahedron(self.gridN, D, E, G, H) # cutted edge bottom ) # cutted edge bottom vol2 = ( volume_tetrahedron(self.gridN, A, F, B, C) + volume_tetrahedron(self.gridN, A, E, F, H) # cutted edge top + volume_tetrahedron(self.gridN, A, H, F, C) # cutted edge top + volume_tetrahedron(self.gridN, C, H, D, A) # middle + volume_tetrahedron(self.gridN, C, G, H, F) # cutted edge bottom ) # cutted edge bottom self._cell_volumes = (vol1 + vol2) / 2 return self._cell_volumes @property def face_areas(self): """Returns the areas of all faces in the mesh Calling this property will compute and return the areas of all faces as a 1D numpy array. The returned quantity is ordered x-face areas, then y-face areas, then z-face areas. Returns ------- (n_faces) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-face areas - *2D:* returns the x-face and y-face areas in order; i.e. y-edge and x-edge lengths, respectively - *3D:* returns the x, y and z-face areas in order """ if ( getattr(self, "_face_areas", None) is None or getattr(self, "_normals", None) is None ): # Compute areas of cell faces if self.dim == 2: xy = self.gridN A, B = index_cube("AB", self.vnN, self.vnFx) edge1 = xy[B, :] - xy[A, :] normal1 = np.c_[edge1[:, 1], -edge1[:, 0]] area1 = _length2D(edge1) A, D = index_cube("AD", self.vnN, self.vnFy) # Note that we are doing A-D to make sure the normal points the # right way. # Think about it. Look at the picture. Normal points towards C # iff you do this. edge2 = xy[A, :] - xy[D, :] normal2 = np.c_[edge2[:, 1], -edge2[:, 0]] area2 = _length2D(edge2) self._face_areas = np.r_[mkvc(area1), mkvc(area2)] self._normals = [_normalize2D(normal1), _normalize2D(normal2)] elif self.dim == 3: A, E, F, B = index_cube("AEFB", self.vnN, self.vnFx) normal1, area1 = face_info( self.gridN, A, E, F, B, average=False, normalizeNormals=False ) A, D, H, E = index_cube("ADHE", self.vnN, self.vnFy) normal2, area2 = face_info( self.gridN, A, D, H, E, average=False, normalizeNormals=False ) A, B, C, D = index_cube("ABCD", self.vnN, self.vnFz) normal3, area3 = face_info( self.gridN, A, B, C, D, average=False, normalizeNormals=False ) self._face_areas = np.r_[mkvc(area1), mkvc(area2), mkvc(area3)] self._normals = [normal1, normal2, normal3] return self._face_areas @property def face_normals(self): """Gridded average face normals for all mesh faces. This property computes and returns a numpy array of shape (n_faces, dim) containing the normal vectors for all mesh faces. For 3D meshes, there are 4 nodes in which cross-products can be used to compute the normal vector. In this case, the average normal vector is returned so there is only 1 vector per face. Returns ------- (n_faces, dim) numpy.ndarray of shape Gridded average face normals for all mesh faces. """ if getattr(self, "_normals", None) is None: self.face_areas # calling .face_areas will create the face normals if self.dim == 2: return _normalize2D(np.r_[self._normals[0], self._normals[1]]) elif self.dim == 3: normal1 = ( self._normals[0][0] + self._normals[0][1] + self._normals[0][2] + self._normals[0][3] ) / 4 normal2 = ( self._normals[1][0] + self._normals[1][1] + self._normals[1][2] + self._normals[1][3] ) / 4 normal3 = ( self._normals[2][0] + self._normals[2][1] + self._normals[2][2] + self._normals[2][3] ) / 4 return _normalize3D(np.r_[normal1, normal2, normal3]) @property def edge_lengths(self): """Returns the lengths of all edges in the mesh Calling this property will compute and return the lengths of all edges in the mesh. The returned quantity is ordered x-edge lengths, then y-edge lengths, then z-edge lengths. Returns ------- (n_edges) numpy.ndarray The length of the quantity returned depends on the dimensions of the mesh: - *1D:* returns the x-edge lengths - *2D:* returns the x-edge and y-edge lengths in order - *3D:* returns the x, y and z-edge lengths in order """ if getattr(self, "_edge_lengths", None) is None: if self.dim == 2: xy = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xy[D, :] - xy[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xy[B, :] - xy[A, :] self._edge_lengths = np.r_[ mkvc(_length2D(edge1)), mkvc(_length2D(edge2)) ] self._edge_tangents = ( np.r_[edge1, edge2] / np.c_[self._edge_lengths, self._edge_lengths] ) elif self.dim == 3: xyz = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xyz[D, :] - xyz[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xyz[B, :] - xyz[A, :] A, E = index_cube("AE", self.vnN, self.vnEz) edge3 = xyz[E, :] - xyz[A, :] self._edge_lengths = np.r_[ mkvc(_length3D(edge1)), mkvc(_length3D(edge2)), mkvc(_length3D(edge3)), ] self._edge_tangents = ( np.r_[edge1, edge2, edge3] / np.c_[self._edge_lengths, self._edge_lengths, self._edge_lengths] ) return self._edge_lengths @property def edge_tangents(self): """Gridded edge tangents directions for all mesh edges This property computes and returns a numpy array of shape (n_edges, dim) containing the edge tangent directions for all mesh edges. Returns ------- (n_edges, dim) numpy.ndarray of shape Gridded edge tangent directions for all mesh edges """ if getattr(self, "_edge_tangents", None) is None: self.edge_lengths # calling .edge_lengths will create the tangents return self._edge_tangents # DEPRECATIONS vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", future_warn=False) area = deprecate_property("face_areas", "area", removal_version="1.0.0", future_warn=False) edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0", future_warn=False)
class BaseTensorMesh(BaseMesh): """Base class for tensor-product style meshes This class contains properites and methods that are common to Cartesian and cylindrical meshes. That is, meshes whose cell centers, nodes, faces and edges can be constructed with tensor-products of vectors. Do not use this class directly! Practical tensor meshes supported in discretize will inherit this class; i.e. :class:`discretize.TensorMesh` and :class:`~discretize.CylindricalMesh`. Inherit this class if you plan to develop a new tensor-style mesh class (e.g. a spherical mesh). Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths along each axis. The length of the iterable object is equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]* . Along each axis, the user has 3 choices for defining the cells widths: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at ``[0, 0, 0]``. For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N'). See Also -------- utils.unpack_widths : The function used to expand a ``list`` or ``tuple`` to generate widths. """ _meshType = "BASETENSOR" _aliases = { **BaseMesh._aliases, **{ "gridCC": "cell_centers", "gridN": "nodes", "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } _unitDimensions = [1, 1, 1] _items = {"h"} | BaseMesh._items def __init__(self, h, origin=None, **kwargs): if "x0" in kwargs: origin = kwargs.pop("x0") try: h = list(h) # ensure value is a list (and make a copy) except TypeError: raise TypeError("h must be an iterable object, not {}".format(type(h))) if len(h) == 0 or len(h) > 3: raise ValueError("h must be of dimension 1, 2, or 3 not {}".format(len(h))) # expand value for i, h_i in enumerate(h): if is_scalar(h_i) and not isinstance(h_i, np.ndarray): # This gives you something over the unit cube. h_i = self._unitDimensions[i] * np.ones(int(h_i)) / int(h_i) elif isinstance(h_i, (list, tuple)): h_i = unpack_widths(h_i) if not isinstance(h_i, np.ndarray): raise TypeError("h[{0:d}] is not a numpy array.".format(i)) if len(h_i.shape) != 1: raise ValueError("h[{0:d}] must be a 1D numpy array.".format(i)) h[i] = h_i[:] # make a copy. self._h = tuple(h) shape_cells = tuple([len(h_i) for h_i in h]) kwargs.pop("shape_cells", None) super().__init__(shape_cells=shape_cells, **kwargs) # do not pass origin here if origin is not None: self.origin = origin @property def h(self): """Cell widths along each axis direction The widths of the cells along each axis direction are returned as a tuple of 1D arrays; e.g. (hx, hy, hz) for a 3D mesh. The lengths of the 1D arrays in the tuple are given by :py:attr:`~discretize.base.BaseMesh.shape_cells`. Ordering begins at the bottom southwest corner. These are the cell widths used when creating the mesh. Returns ------- (dim) tuple of numpy.ndarray Cell widths along each axis direction. This depends on the mesh class: - :class:`~discretize.TensorMesh`: cell widths along the *x* , [*y* and *z* ] directions - :class:`~discretize.CylindricalMesh`: cell widths along the *r*, :math:`\\phi` and *z* directions - :class:`~discretize.TreeMesh`: cells widths of the *underlying tensor mesh* along the *x* , *y* [and *z* ] directions """ return self._h @BaseMesh.origin.setter def origin(self, value): # ensure value is a 1D array at all times try: value = list(value) except: raise TypeError("origin must be iterable") if len(value) != self.dim: raise ValueError("Dimension mismatch. len(origin) != len(h)") for i, (val, h_i) in enumerate(zip(value, self.h)): if val == "C": value[i] = -h_i.sum() * 0.5 elif val == "N": value[i] = -h_i.sum() value = np.asarray(value, dtype=np.float64) self._origin = value @property def nodes_x(self): """ Return x-coordinates of the nodes along the x-direction This property returns a vector containing the x-coordinate values of the nodes along the x-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the x-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the x-coordinate values of the nodes along the x-direction for the underlying tensor mesh. Returns ------- (n_nodes_x) numpy.ndarray of float A 1D array containing the x-coordinates of the nodes along the x-direction. """ return np.r_[self.origin[0], self.h[0]].cumsum() @property def nodes_y(self): """ Return y-coordinates of the nodes along the y-direction For 2D and 3D meshes, this property returns a vector containing the y-coordinate values of the nodes along the y-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the y-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the y-coordinate values of the nodes along the y-direction for the underlying tensor mesh. Returns ------- (n_nodes_y) numpy.ndarray of float or None A 1D array containing the y-coordinates of the nodes along the y-direction. Returns *None* for 1D meshes. """ return None if self.dim < 2 else np.r_[self.origin[1], self.h[1]].cumsum() @property def nodes_z(self): """ Return z-coordinates of the nodes along the z-direction For 3D meshes, this property returns a 1D vector containing the z-coordinate values of the nodes along the z-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the node positions which define the tensor along the z-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the z-coordinate values of the nodes along the z-direction for the underlying tensor mesh. Returns ------- (n_nodes_z) numpy.ndarray of float or None A 1D array containing the z-coordinates of the nodes along the z-direction. Returns *None* for 1D and 2D meshes. """ return None if self.dim < 3 else np.r_[self.origin[2], self.h[2]].cumsum() @property def cell_centers_x(self): """ Return x-coordinates of the cell centers along the x-direction For 1D, 2D and 3D meshes, this property returns a 1D vector containing the x-coordinate values of the cell centers along the x-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the x-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the x-coordinate values of the cell centers along the x-direction for the underlying tensor mesh. Returns ------- (n_cells_x) numpy.ndarray of float A 1D array containing the x-coordinates of the cell centers along the x-direction. """ nodes = self.nodes_x return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_y(self): """ Return y-coordinates of the cell centers along the y-direction For 2D and 3D meshes, this property returns a 1D vector containing the y-coordinate values of the cell centers along the y-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the y-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the y-coordinate values of the cell centers along the y-direction for the underlying tensor mesh . Returns ------- (n_cells_y) numpy.ndarray of float or None A 1D array containing the y-coordinates of the cell centers along the y-direction. Returns *None* for 1D meshes. """ if self.dim < 2: return None nodes = self.nodes_y return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_z(self): """ Return z-coordinates of the cell centers along the z-direction For 3D meshes, this property returns a 1D vector containing the z-coordinate values of the cell centers along the z-direction. For instances of :class:`~discretize.TensorMesh` or :class:`~discretize.CylindricalMesh`, this is equivalent to the cell center positions which define the tensor along the z-axis. For instances of :class:`~discretize.TreeMesh` however, this property returns the z-coordinate values of the cell centers along the z-direction for the underlying tensor mesh . Returns ------- (n_cells_z) numpy.ndarray of float or None A 1D array containing the z-coordinates of the cell centers along the z-direction. Returns *None* for 1D and 2D meshes. """ if self.dim < 3: return None nodes = self.nodes_z return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers(self): """Return gridded cell center locations This property returns a numpy array of shape (n_cells, dim) containing gridded cell center locations for all cells in the mesh. The cells are ordered along the x, then y, then z directions. Returns ------- (n_cells, dim) numpy.ndarray of float Gridded cell center locations Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx], '0') >>> mesh_1D.cell_centers array([0.5, 1.5, 2.5, 3.5, 4.5]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz], '000') >>> mesh_3D.cell_centers array([[0.5, 1. , 1.5], [1.5, 1. , 1.5], [0.5, 3. , 1.5], [1.5, 3. , 1.5], [0.5, 1. , 4.5], [1.5, 1. , 4.5], [0.5, 3. , 4.5], [1.5, 3. , 4.5]]) """ return self._getTensorGrid("cell_centers") @property def nodes(self): """Return gridded node locations This property returns a numpy array of shape (n_nodes, dim) containing gridded node locations for all nodes in the mesh. The nodes are ordered along the x, then y, then z directions. Returns ------- (n_nodes, dim) numpy.ndarray of float Gridded node locations Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx], '0') >>> mesh_1D.nodes array([0., 1., 2., 3., 4., 5.]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz], '000') >>> mesh_3D.nodes array([[0., 0., 0.], [1., 0., 0.], [2., 0., 0.], [0., 2., 0.], [1., 2., 0.], [2., 2., 0.], [0., 4., 0.], [1., 4., 0.], [2., 4., 0.], [0., 0., 3.], [1., 0., 3.], [2., 0., 3.], [0., 2., 3.], [1., 2., 3.], [2., 2., 3.], [0., 4., 3.], [1., 4., 3.], [2., 4., 3.], [0., 0., 6.], [1., 0., 6.], [2., 0., 6.], [0., 2., 6.], [1., 2., 6.], [2., 2., 6.], [0., 4., 6.], [1., 4., 6.], [2., 4., 6.]]) """ return self._getTensorGrid("nodes") @property def boundary_nodes(self): """Boundary node locations This property returns the locations of the nodes on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary nodes by the dimension of the mesh. Returns ------- (n_boundary_nodes, dim) numpy.ndarray of float Boundary node locations """ dim = self.dim if dim == 1: return self.nodes_x[[0, -1]] return self.nodes[make_boundary_bool(self.shape_nodes)] @property def h_gridded(self): """Return dimensions of all mesh cells as staggered grid. This property returns a numpy array of shape (n_cells, dim) containing gridded x, (y and z) dimensions for all cells in the mesh. The first row corresponds to the bottom-front-leftmost cell. The cells are ordered along the x, then y, then z directions. Returns ------- (n_cells, dim) numpy.ndarray of float Dimensions of all mesh cells as staggered grid Examples -------- The following is a 1D example. >>> from discretize import TensorMesh >>> hx = np.ones(5) >>> mesh_1D = TensorMesh([hx]) >>> mesh_1D.h_gridded array([[1.], [1.], [1.], [1.], [1.]]) The following is a 3D example. >>> hx, hy, hz = np.ones(2), 2*np.ones(2), 3*np.ones(2) >>> mesh_3D = TensorMesh([hx, hy, hz]) >>> mesh_3D.h_gridded array([[1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.], [1., 2., 3.]]) """ if self.dim == 1: return self.h[0][:, None] return ndgrid(*self.h) @property def faces_x(self): """Gridded x-face locations This property returns a numpy array of shape (n_faces_x, dim) containing gridded locations for all x-faces in the mesh. The first row corresponds to the bottom-front-leftmost x-face. The x-faces are ordered along the x, then y, then z directions. Returns ------- (n_faces_x, dim) numpy.ndarray of float Gridded x-face locations """ if self.nFx == 0: return return self._getTensorGrid("faces_x") @property def faces_y(self): """Gridded y-face locations This property returns a numpy array of shape (n_faces_y, dim) containing gridded locations for all y-faces in the mesh. The first row corresponds to the bottom-front-leftmost y-face. The y-faces are ordered along the x, then y, then z directions. Returns ------- n_faces_y, dim) numpy.ndarray of float or None Gridded y-face locations for 2D and 3D mesh. Returns *None* for 1D meshes. """ if self.nFy == 0 or self.dim < 2: return return self._getTensorGrid("faces_y") @property def faces_z(self): """Gridded z-face locations This property returns a numpy array of shape (n_faces_z, dim) containing gridded locations for all z-faces in the mesh. The first row corresponds to the bottom-front-leftmost z-face. The z-faces are ordered along the x, then y, then z directions. Returns ------- (n_faces_z, dim) numpy.ndarray of float or None Gridded z-face locations for 3D mesh. Returns *None* for 1D and 2D meshes. """ if self.nFz == 0 or self.dim < 3: return return self._getTensorGrid("faces_z") @property def faces(self): """Gridded face locations This property returns a numpy array of shape (n_faces, dim) containing gridded locations for all faces in the mesh. The first row corresponds to the bottom-front-leftmost x-face. The output array returns the x-faces, then the y-faces, then the z-faces; i.e. *mesh.faces* is equivalent to *np.r_[mesh.faces_x, mesh.faces_y, mesh.face_z]* . For each face type, the locations are ordered along the x, then y, then z directions. Returns ------- (n_faces, dim) numpy.ndarray of float Gridded face locations """ faces = self.faces_x if self.dim > 1: faces = np.r_[faces, self.faces_y] if self.dim > 2: faces = np.r_[faces, self.faces_z] return faces @property def boundary_faces(self): """Boundary face locations This property returns the locations of the faces on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary faces by the dimension of the mesh. Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Boundary faces locations """ dim = self.dim if dim == 1: return self.nodes_x[[0, -1]] if dim == 2: fx = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y) fy = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]]) return np.r_[fx, fy] if dim == 3: fx = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y, self.cell_centers_z) fy = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]], self.cell_centers_z) fz = ndgrid(self.cell_centers_x, self.cell_centers_y, self.nodes_z[[0, -1]]) return np.r_[fx, fy, fz] @property def boundary_face_outward_normals(self): """Outward normal vectors of boundary faces This property returns the outward normal vectors of faces the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary faces by the dimension of the mesh. Returns ------- (n_boundary_faces, dim) numpy.ndarray of float Outward normal vectors of boundary faces """ dim = self.dim if dim == 1: return np.array([-1, 1]) if dim == 2: nx = ndgrid(np.r_[-1, 1], np.zeros(self.shape_cells[1])) ny = ndgrid(np.zeros(self.shape_cells[0]), np.r_[-1, 1]) return np.r_[nx, ny] if dim == 3: nx = ndgrid( np.r_[-1, 1], np.zeros(self.shape_cells[1]), np.zeros(self.shape_cells[2]), ) ny = ndgrid( np.zeros(self.shape_cells[0]), np.r_[-1, 1], np.zeros(self.shape_cells[2]), ) nz = ndgrid( np.zeros(self.shape_cells[0]), np.zeros(self.shape_cells[1]), np.r_[-1, 1], ) return np.r_[nx, ny, nz] @property def edges_x(self): """Gridded x-edge locations This property returns a numpy array of shape (n_edges_x, dim) containing gridded locations for all x-edges in the mesh. The first row corresponds to the bottom-front-leftmost x-edge. The x-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_x, dim) numpy.ndarray of float or None Gridded x-edge locations. Returns *None* if `shape_edges_x[0]` is 0. """ if self.nEx == 0: return return self._getTensorGrid("edges_x") @property def edges_y(self): """Gridded y-edge locations This property returns a numpy array of shape (n_edges_y, dim) containing gridded locations for all y-edges in the mesh. The first row corresponds to the bottom-front-leftmost y-edge. The y-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_y, dim) numpy.ndarray of float Gridded y-edge locations. Returns *None* for 1D meshes. """ if self.nEy == 0 or self.dim < 2: return return self._getTensorGrid("edges_y") @property def edges_z(self): """Gridded z-edge locations This property returns a numpy array of shape (n_edges_z, dim) containing gridded locations for all z-edges in the mesh. The first row corresponds to the bottom-front-leftmost z-edge. The z-edges are ordered along the x, then y, then z directions. Returns ------- (n_edges_z, dim) numpy.ndarray of float Gridded z-edge locations. Returns *None* for 1D and 2D meshes. """ if self.nEz == 0 or self.dim < 3: return return self._getTensorGrid("edges_z") @property def edges(self): """Gridded edge locations This property returns a numpy array of shape (n_edges, dim) containing gridded locations for all edges in the mesh. The first row corresponds to the bottom-front-leftmost x-edge. The output array returns the x-edges, then the y-edges, then the z-edges; i.e. *mesh.edges* is equivalent to *np.r_[mesh.edges_x, mesh.edges_y, mesh.edges_z]* . For each edge type, the locations are ordered along the x, then y, then z directions. Returns ------- (n_edges, dim) numpy.ndarray of float Gridded edge locations """ edges = self.edges_x if self.dim > 1: edges = np.r_[edges, self.edges_y] if self.dim > 2: edges = np.r_[edges, self.edges_z] return edges @property def boundary_edges(self): """Boundary edge locations This property returns the locations of the edges on the boundary of the mesh as a numpy array. The shape of the numpy array is the number of boundary edges by the dimension of the mesh. Returns ------- (n_boundary_edges, dim) numpy.ndarray of float Boundary edge locations """ dim = self.dim if dim == 1: return None # no boundary edges in 1D if dim == 2: ex = ndgrid(self.cell_centers_x, self.nodes_y[[0, -1]]) ey = ndgrid(self.nodes_x[[0, -1]], self.cell_centers_y) return np.r_[ex, ey] if dim == 3: ex = self.edges_x[make_boundary_bool(self.shape_edges_x, dir="yz")] ey = self.edges_y[make_boundary_bool(self.shape_edges_y, dir="xz")] ez = self.edges_z[make_boundary_bool(self.shape_edges_z, dir="xy")] return np.r_[ex, ey, ez] def _getTensorGrid(self, key): if getattr(self, "_" + key, None) is None: setattr(self, "_" + key, ndgrid(self.get_tensor(key))) return getattr(self, "_" + key) def get_tensor(self, key): """Returns the base 1D arrays for a specified mesh tensor. The cell-centers, nodes, x-faces, z-edges, etc... of a tensor mesh can be constructed by applying tensor products to the set of base 1D arrays; i.e. (vx, vy, vz). These 1D arrays define the gridded locations for the mesh tensor along each axis. For a given mesh tensor (i.e. cell centers, nodes, x/y/z faces or x/y/z edges), **get_tensor** returns a list containing the base 1D arrays. Parameters ---------- key : str Specifies the tensor being returned. Please choose from:: 'CC', 'cell_centers' -> location of cell centers 'N', 'nodes' -> location of nodes 'Fx', 'faces_x' -> location of faces with an x normal 'Fy', 'faces_y' -> location of faces with an y normal 'Fz', 'faces_z' -> location of faces with an z normal 'Ex', 'edges_x' -> location of edges with an x tangent 'Ey', 'edges_y' -> location of edges with an y tangent 'Ez', 'edges_z' -> location of edges with an z tangent Returns ------- (dim) list of 1D numpy.ndarray list of base 1D arrays for the tensor. """ key = self._parse_location_type(key) if key == "faces_x": ten = [ self.nodes_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "faces_y": ten = [ self.cell_centers_x, self.nodes_y, self.cell_centers_z, ] elif key == "faces_z": ten = [ self.cell_centers_x, self.cell_centers_y, self.nodes_z, ] elif key == "edges_x": ten = [self.cell_centers_x, self.nodes_y, self.nodes_z] elif key == "edges_y": ten = [self.nodes_x, self.cell_centers_y, self.nodes_z] elif key == "edges_z": ten = [self.nodes_x, self.nodes_y, self.cell_centers_z] elif key == "cell_centers": ten = [ self.cell_centers_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "nodes": ten = [self.nodes_x, self.nodes_y, self.nodes_z] else: raise KeyError(r"Unrecognized key {key}") return [t for t in ten if t is not None] # --------------- Methods --------------------- def is_inside(self, pts, location_type="nodes", **kwargs): """Determine which points lie within the mesh For an arbitrary set of points, **is_indside** returns a boolean array identifying which points lie within the mesh. Parameters ---------- pts : (n_pts, dim) numpy.ndarray Locations of input points. Must have same dimension as the mesh. location_type : str, optional Use *N* to determine points lying within the cluster of mesh nodes. Use *CC* to determine points lying within the cluster of mesh cell centers. Returns ------- (n_pts) numpy.ndarray of bool Boolean array identifying points which lie within the mesh """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] pts = as_array_n_by_dim(pts, self.dim) tensors = self.get_tensor(location_type) if location_type[0].lower() == "n" and self._meshType == "CYL": # NOTE: for a CYL mesh we add a node to check if we are inside in # the radial direction! tensors[0] = np.r_[0.0, tensors[0]] tensors[1] = np.r_[tensors[1], 2.0 * np.pi] inside = np.ones(pts.shape[0], dtype=bool) for i, tensor in enumerate(tensors): TOL = np.diff(tensor).min() * 1.0e-10 inside = ( inside & (pts[:, i] >= tensor.min() - TOL) & (pts[:, i] <= tensor.max() + TOL) ) return inside def _getInterpolationMat( self, loc, location_type="cell_centers", zeros_outside=False ): """Produces interpolation matrix Parameters ---------- loc : numpy.ndarray Location of points to interpolate to location_type: str, optional What to interpolate location_type can be:: 'Ex', 'edges_x' -> x-component of field defined on x edges 'Ey', 'edges_y' -> y-component of field defined on y edges 'Ez', 'edges_z' -> z-component of field defined on z edges 'Fx', 'faces_x' -> x-component of field defined on x faces 'Fy', 'faces_y' -> y-component of field defined on y faces 'Fz', 'faces_z' -> z-component of field defined on z faces 'N', 'nodes' -> scalar field defined on nodes 'CC', 'cell_centers' -> scalar field defined on cell centers 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers Returns ------- scipy.sparse.csr_matrix M, the interpolation matrix """ loc = as_array_n_by_dim(loc, self.dim) if not zeros_outside: if not np.all(self.is_inside(loc)): raise ValueError("Points outside of mesh") else: indZeros = np.logical_not(self.is_inside(loc)) loc[indZeros, :] = np.array([v.mean() for v in self.get_tensor("CC")]) location_type = self._parse_location_type(location_type) if location_type in [ "faces_x", "faces_y", "faces_z", "edges_x", "edges_y", "edges_z", ]: ind = {"x": 0, "y": 1, "z": 2}[location_type[-1]] if self.dim < ind: raise ValueError("mesh is not high enough dimension.") if "f" in location_type.lower(): items = (self.nFx, self.nFy, self.nFz)[: self.dim] else: items = (self.nEx, self.nEy, self.nEz)[: self.dim] components = [spzeros(loc.shape[0], n) for n in items] components[ind] = interpolation_matrix(loc, *self.get_tensor(location_type)) # remove any zero blocks (hstack complains) components = [comp for comp in components if comp.shape[1] > 0] Q = sp.hstack(components) elif location_type in ["cell_centers", "nodes"]: Q = interpolation_matrix(loc, *self.get_tensor(location_type)) elif location_type in ["cell_centers_x", "cell_centers_y", "cell_centers_z"]: Q = interpolation_matrix(loc, *self.get_tensor("CC")) Z = spzeros(loc.shape[0], self.nC) if location_type[-1] == "x": Q = sp.hstack([Q, Z, Z]) elif location_type[-1] == "y": Q = sp.hstack([Z, Q, Z]) elif location_type[-1] == "z": Q = sp.hstack([Z, Z, Q]) else: raise NotImplementedError( "getInterpolationMat: location_type==" + location_type + " and mesh.dim==" + str(self.dim) ) if zeros_outside: Q[indZeros, :] = 0 return Q.tocsr() def get_interpolation_matrix( self, loc, location_type="cell_centers", zeros_outside=False, **kwargs ): """Construct linear interpolation matrix from mesh This method constructs a linear interpolation matrix from tensor locations (nodes, cell-centers, faces, etc...) on the mesh to a set of arbitrary locations. Parameters ---------- loc : (n_pts, dim) numpy.ndarray Location of points being to interpolate to. Must have same dimensions as the mesh. location_type : str, optional Tensor locations on the mesh being interpolated from. *location_type* must be one of: - 'Ex', 'edges_x' -> x-component of field defined on x edges - 'Ey', 'edges_y' -> y-component of field defined on y edges - 'Ez', 'edges_z' -> z-component of field defined on z edges - 'Fx', 'faces_x' -> x-component of field defined on x faces - 'Fy', 'faces_y' -> y-component of field defined on y faces - 'Fz', 'faces_z' -> z-component of field defined on z faces - 'N', 'nodes' -> scalar field defined on nodes - 'CC', 'cell_centers' -> scalar field defined on cell centers - 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers - 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers - 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers zeros_outside : bool, optional If *False*, nearest neighbour is used to compute the interpolate value at locations outside the mesh. If *True* , values at locations outside the mesh will be zero. Returns ------- (n_pts, n_loc_type) scipy.sparse.csr_matrix A sparse matrix which interpolates the specified tensor quantity on mesh to the set of specified locations. Examples -------- Here is a 1D example where a function evaluated on the nodes is interpolated to a set of random locations. To compare the accuracy, the function is evaluated at the set of random locations. >>> from discretize import TensorMesh >>> import numpy as np >>> import matplotlib.pyplot as plt >>> np.random.seed(14) >>> locs = np.random.rand(50)*0.8+0.1 >>> dense = np.linspace(0, 1, 200) >>> fun = lambda x: np.cos(2*np.pi*x) >>> hx = 0.125 * np.ones(8) >>> mesh1D = TensorMesh([hx]) >>> Q = mesh1D.get_interpolation_matrix(locs, 'nodes') .. collapse:: Expand to see scripting for plot >>> plt.figure(figsize=(5, 3)) >>> plt.plot(dense, fun(dense), ':', c="C0", lw=3, label="True Function") >>> plt.plot(mesh1D.nodes, fun(mesh1D.nodes), 's', c="C0", ms=8, label="True sampled") >>> plt.plot(locs, Q*fun(mesh1D.nodes), 'o', ms=4, label="Interpolated") >>> plt.legend() >>> plt.show() Here, demonstrate a similar example on a 2D mesh using a 2D Gaussian distribution. We interpolate the Gaussian from the nodes to cell centers and examine the relative error. >>> hx = np.ones(10) >>> hy = np.ones(10) >>> mesh2D = TensorMesh([hx, hy], x0='CC') >>> def fun(x, y): ... return np.exp(-(x**2 + y**2)/2**2) >>> nodes = mesh2D.nodes >>> val_nodes = fun(nodes[:, 0], nodes[:, 1]) >>> centers = mesh2D.cell_centers >>> val_centers = fun(centers[:, 0], centers[:, 1]) >>> A = mesh2D.get_interpolation_matrix(centers, 'nodes') >>> val_interp = A.dot(val_nodes) .. collapse:: Expand to see scripting for plot >>> fig = plt.figure(figsize=(11,3.3)) >>> clim = (0., 1.) >>> ax1 = fig.add_subplot(131) >>> ax2 = fig.add_subplot(132) >>> ax3 = fig.add_subplot(133) >>> mesh2D.plot_image(val_centers, ax=ax1, clim=clim) >>> mesh2D.plot_image(val_interp, ax=ax2, clim=clim) >>> mesh2D.plot_image(val_centers-val_interp, ax=ax3, clim=clim) >>> ax1.set_title('Analytic at Centers') >>> ax2.set_title('Interpolated from Nodes') >>> ax3.set_title('Relative Error') >>> plt.show() """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] if "zerosOutside" in kwargs: warnings.warn( "The zerosOutside keyword argument has been deprecated, please use zeros_outside. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) zeros_outside = kwargs["zerosOutside"] return self._getInterpolationMat(loc, location_type, zeros_outside) def _fastInnerProduct( self, projection_type, model=None, invert_model=False, invert_matrix=False ): """Fast version of getFaceInnerProduct. This does not handle the case of a full tensor property. Parameters ---------- model : numpy.ndarray material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) projection_type : str 'edges' or 'faces' returnP : bool returns the projection matrices invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix M, the inner product matrix """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError("projection_type must be 'F' for faces or 'E' for edges") if model is None: model = np.ones(self.nC) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nC) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim # Isotropic? or anisotropic? if model.size == self.nC: Av = getattr(self, "ave" + projection_type + "2CC") Vprop = self.cell_volumes * mkvc(model) M = n_elements * sdiag(Av.T * Vprop) elif model.size == self.nC * self.dim: Av = getattr(self, "ave" + projection_type + "2CCV") # if cyl, then only certain components are relevant due to symmetry # for faces, x, z matters, for edges, y (which is theta) matters if self._meshType == "CYL": if projection_type == "E": model = model[:, 1] # this is the action of a projection mat elif projection_type == "F": model = model[:, [0, 2]] V = sp.kron(sp.identity(n_elements), sdiag(self.cell_volumes)) M = sdiag(Av.T * V * mkvc(model)) else: return None if invert_matrix: return sdinv(M) else: return M def _fastInnerProductDeriv( self, projection_type, model, invert_model=False, invert_matrix=False ): """ Parameters ---------- projection_type : str 'E' or 'F' tensorType : TensorType type of the tensor invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- function dMdmu, the derivative of the inner product matrix """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError("projection_type must be 'F' for faces or 'E' for edges") tensorType = TensorType(self, model) dMdprop = None if invert_matrix or invert_model: MI = self._fastInnerProduct( projection_type, model, invert_model=invert_model, invert_matrix=invert_matrix, ) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim if tensorType == 0: # isotropic, constant Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) ones = sp.csr_matrix( (np.ones(self.nC), (range(self.nC), np.zeros(self.nC))), shape=(self.nC, 1), ) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V * ones elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * V * ones * sdiag(1.0 / model ** 2) ) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model ** 2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) elif tensorType == 1: # isotropic, variable in space Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V elif invert_matrix and invert_model: dMdprop = n_elements * ( sdiag(MI.diagonal() ** 2) * Av.T * V * sdiag(1.0 / model ** 2) ) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model ** 2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal() ** 2) * Av.T * V) elif tensorType == 2: # anisotropic Av = getattr(self, "ave" + projection_type + "2CCV") V = sp.kron(sp.identity(self.dim), sdiag(self.cell_volumes)) if self._meshType == "CYL": Zero = sp.csr_matrix((self.nC, self.nC)) Eye = sp.eye(self.nC) if projection_type == "E": P = sp.hstack([Zero, Eye, Zero]) # print(P.todense()) elif projection_type == "F": P = sp.vstack( [sp.hstack([Eye, Zero, Zero]), sp.hstack([Zero, Zero, Eye])] ) # print(P.todense()) else: P = sp.eye(self.nC * self.dim) if not invert_matrix and not invert_model: dMdprop = Av.T * P * V elif invert_matrix and invert_model: dMdprop = ( sdiag(MI.diagonal() ** 2) * Av.T * P * V * sdiag(1.0 / model ** 2) ) elif invert_model: dMdprop = Av.T * P * V * sdiag(-1.0 / model ** 2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal() ** 2) * Av.T * P * V if dMdprop is not None: def innerProductDeriv(v=None): if v is None: warnings.warn( "Depreciation Warning: TensorMesh.innerProductDeriv." " You should be supplying a vector. " "Use: sdiag(u)*dMdprop", DeprecationWarning, ) return dMdprop return sdiag(v) * dMdprop return innerProductDeriv else: return None # DEPRECATED @property def hx(self): """Width of cells in the x direction Returns ------- numpy.ndarray .. deprecated:: 0.5.0 `hx` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[0]`. """ warnings.warn( "hx has been deprecated, please access as mesh.h[0]", DeprecationWarning ) return self.h[0] @property def hy(self): """Width of cells in the y direction Returns ------- numpy.ndarray or None .. deprecated:: 0.5.0 `hy` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[1]`. """ warnings.warn( "hy has been deprecated, please access as mesh.h[1]", DeprecationWarning ) return None if self.dim < 2 else self.h[1] @property def hz(self): """Width of cells in the z direction Returns ------- numpy.ndarray or None .. deprecated:: 0.5.0 `hz` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[2]`. """ warnings.warn( "hz has been deprecated, please access as mesh.h[2]", DeprecationWarning ) return None if self.dim < 3 else self.h[2] vectorNx = deprecate_property("nodes_x", "vectorNx", removal_version="1.0.0", future_warn=False) vectorNy = deprecate_property("nodes_y", "vectorNy", removal_version="1.0.0", future_warn=False) vectorNz = deprecate_property("nodes_z", "vectorNz", removal_version="1.0.0", future_warn=False) vectorCCx = deprecate_property( "cell_centers_x", "vectorCCx", removal_version="1.0.0", future_warn=False ) vectorCCy = deprecate_property( "cell_centers_y", "vectorCCy", removal_version="1.0.0", future_warn=False ) vectorCCz = deprecate_property( "cell_centers_z", "vectorCCz", removal_version="1.0.0", future_warn=False ) getInterpolationMat = deprecate_method( "get_interpolation_matrix", "getInterpolationMat", removal_version="1.0.0", future_warn=False ) isInside = deprecate_method("is_inside", "isInside", removal_version="1.0.0", future_warn=False) getTensor = deprecate_method("get_tensor", "getTensor", removal_version="1.0.0", future_warn=False)
class TreeMesh(_TreeMesh, BaseTensorMesh, InnerProducts, DiffOperators, TreeMeshIO, InterfaceMixins): """Class for QuadTree (2D) and OcTree (3D) meshes. Tree meshes are numerical grids where the dimensions of each cell are powers of 2 larger than some base cell dimension. Unlike the :class:`~discretize.TensorMesh` class, gridded locations and numerical operators for instances of ``TreeMesh`` cannot be simply constructed using tensor products. Furthermore, each cell is an instance of ``TreeMesh`` is an instance of the :class:`~discretize.tree_mesh.TreeCell` . Parameters ---------- h : (dim) iterable of int, numpy.ndarray, or tuple Defines the cell widths of the *underlying tensor mesh* along each axis. The length of the iterable object is equal to the dimension of the mesh (2 or 3). For a 3D mesh, the list would have the form *[hx, hy, hz]*. The number of cells along each axis **must be a power of 2** . Along each axis, the user has 3 choices for defining the cells widths for the underlying tensor mesh: - :class:`int` -> A unit interval is equally discretized into `N` cells. - :class:`numpy.ndarray` -> The widths are explicity given for each cell - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])* where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional) is a padding factor denoting exponential increase/decrease in the cell width for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]* origin : (dim) iterable, default: 0 Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost corner. By default, the mesh is anchored such that its origin is at [0, 0, 0]. For each dimension (x, y or z), The user may set the origin 2 ways: - a ``scalar`` which explicitly defines origin along that dimension. - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along each axis is the first node location ('0'), in the center ('C') or the last node location ('N') (see Examples). Examples -------- Here we generate a basic 2D tree mesh. >>> from discretize import TreeMesh >>> import numpy as np >>> import matplotlib.pyplot as plt Define base mesh (domain and finest discretization), >>> dh = 5 # minimum cell width (base mesh cell width) >>> nbc = 64 # number of base mesh cells >>> h = dh * np.ones(nbc) >>> mesh = TreeMesh([h, h]) Define corner points for a rectangular box, and subdived the mesh within the box to the maximum refinement level. >>> x0s = [120.0, 80.0] >>> x1s = [240.0, 160.0] >>> levels = [mesh.max_level] >>> mesh.refine_box(x0s, x1s, levels) >>> mesh.plot_grid() >>> plt.show() """ _meshType = "TREE" _aliases = { **BaseTensorMesh._aliases, **DiffOperators._aliases, **{ "ntN": "n_total_nodes", "ntEx": "n_total_edges_x", "ntEy": "n_total_edges_y", "ntEz": "n_total_edges_z", "ntE": "n_total_edges", "ntFx": "n_total_faces_x", "ntFy": "n_total_faces_y", "ntFz": "n_total_faces_z", "ntF": "n_total_faces", "nhN": "n_hanging_nodes", "nhEx": "n_hanging_edges_x", "nhEy": "n_hanging_edges_y", "nhEz": "n_hanging_edges_z", "nhE": "n_hanging_edges", "nhFx": "n_hanging_faces_x", "nhFy": "n_hanging_faces_y", "nhFz": "n_hanging_faces_z", "nhF": "n_hanging_faces", "gridhN": "hanging_nodes", "gridhFx": "hanging_faces_x", "gridhFy": "hanging_faces_y", "gridhFz": "hanging_faces_z", "gridhEx": "hanging_edges_x", "gridhEy": "hanging_edges_y", "gridhEz": "hanging_edges_z", }, } _items = {"h", "origin", "cell_state"} # inheriting stuff from BaseTensorMesh that isn't defined in _QuadTree def __init__(self, h=None, origin=None, **kwargs): if "x0" in kwargs: origin = kwargs.pop("x0") super().__init__(h=h, origin=origin) cell_state = kwargs.pop("cell_state", None) cell_indexes = kwargs.pop("cell_indexes", None) cell_levels = kwargs.pop("cell_levels", None) if cell_state is None: if cell_indexes is not None and cell_levels is not None: cell_state = {} cell_state["indexes"] = cell_indexes cell_state["levels"] = cell_levels if cell_state is not None: indexes = cell_state["indexes"] levels = cell_state["levels"] self.__setstate__((indexes, levels)) def __repr__(self): """Plain text representation.""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) top = "\n" + mesh_name + ": {0:2.2f}% filled\n\n".format( self.fill * 100) # Number of cells per level level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] cell_display = ["Level : Number of cells"] cell_display.append("-----------------------") for level in non_zero_levels: cell_display.append("{:^5} : {:^15}".format( level, level_count[level])) cell_display.append("-----------------------") cell_display.append("Total : {:^15}".format(self.nC)) extent_display = [" Mesh Extent "] extent_display.append(" min , max ") extent_display.append(" ---------------------------") dim_label = {0: "x", 1: "y", 2: "z"} for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) extent_display.append("{}: {:^13},{:^13}".format( dim_label[dim], n_vector[0], n_vector[-1])) for i, line in enumerate(extent_display): if i == len(cell_display): cell_display.append(" " * (len(cell_display[0]) - 3 - len(line))) cell_display[i] += 3 * " " + line h_display = [" Cell Widths "] h_display.append(" min , max ") h_display.append("-" * (len(h_display[0]))) h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) for dim in range(self.dim): h_display.append("{:^10}, {:^10}".format(mins[dim], maxs[dim])) for i, line in enumerate(h_display): if i == len(cell_display): cell_display.append(" " * len(cell_display[0])) cell_display[i] += 3 * " " + line return top + "\n".join(cell_display) def _repr_html_(self): """html representation""" mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad")) level_count = self._count_cells_per_index() non_zero_levels = np.nonzero(level_count)[0] dim_label = {0: "x", 1: "y", 2: "z"} h_gridded = self.h_gridded mins = np.min(h_gridded, axis=0) maxs = np.max(h_gridded, axis=0) style = " style='padding: 5px 20px 5px 20px;'" # Cell level table: cel_tbl = "<table>\n" cel_tbl += "<tr>\n" cel_tbl += "<th" + style + ">Level</th>\n" cel_tbl += "<th" + style + ">Number of cells</th>\n" cel_tbl += "</tr>\n" for level in non_zero_levels: cel_tbl += "<tr>\n" cel_tbl += "<td" + style + ">{}</td>\n".format(level) cel_tbl += "<td" + style + ">{}</td>\n".format(level_count[level]) cel_tbl += "</tr>\n" cel_tbl += "<tr>\n" cel_tbl += ( "<td style='font-weight: bold; padding: 5px 20px 5px 20px;'> Total </td>\n" ) cel_tbl += "<td" + style + "> {} </td>\n".format(self.nC) cel_tbl += "</tr>\n" cel_tbl += "</table>\n" det_tbl = "<table>\n" det_tbl += "<tr>\n" det_tbl += "<th></th>\n" det_tbl += "<th" + style + " colspan='2'>Mesh extent</th>\n" det_tbl += "<th" + style + " colspan='2'>Cell widths</th>\n" det_tbl += "</tr>\n" det_tbl += "<tr>\n" det_tbl += "<th></th>\n" det_tbl += "<th" + style + ">min</th>\n" det_tbl += "<th" + style + ">max</th>\n" det_tbl += "<th" + style + ">min</th>\n" det_tbl += "<th" + style + ">max</th>\n" det_tbl += "</tr>\n" for dim in range(self.dim): n_vector = getattr(self, "nodes_" + dim_label[dim]) det_tbl += "<tr>\n" det_tbl += "<td" + style + ">{}</td>\n".format(dim_label[dim]) det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[0]) det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[-1]) det_tbl += "<td" + style + ">{}</td>\n".format(mins[dim]) det_tbl += "<td" + style + ">{}</td>\n".format(maxs[dim]) det_tbl += "</tr>\n" det_tbl += "</table>\n" full_tbl = "<table>\n" full_tbl += "<tr>\n" full_tbl += "<td style='font-weight: bold; font-size: 1.2em; text-align: center;'>{}</td>\n".format( mesh_name) full_tbl += "<td style='font-size: 1.2em; text-align: center;' colspan='2'>{0:2.2f}% filled</td>\n".format( 100 * self.fill) full_tbl += "</tr>\n" full_tbl += "<tr>\n" full_tbl += "<td>\n" full_tbl += cel_tbl full_tbl += "</td>\n" full_tbl += "<td>\n" full_tbl += det_tbl full_tbl += "</td>\n" full_tbl += "</tr>\n" full_tbl += "</table>\n" return full_tbl @BaseTensorMesh.origin.setter def origin(self, value): # first use the BaseTensorMesh to set the origin to handle "0, C, N" BaseTensorMesh.origin.fset(self, value) # then update the TreeMesh with the hidden value self._set_origin(self._origin) @property def vntF(self): """ Vector number of total faces along each axis This property returns the total number of hanging and non-hanging faces along each axis direction. The returned quantity is a list of integers of the form [nFx,nFy,nFz]. Returns ------- list of int Vector number of total faces along each axis """ return [self.ntFx, self.ntFy] + ([] if self.dim == 2 else [self.ntFz]) @property def vntE(self): """ Vector number of total edges along each axis This property returns the total number of hanging and non-hanging edges along each axis direction. The returned quantity is a list of integers of the form [nEx,nEy,nEz]. Returns ------- list of int Vector number of total edges along each axis """ return [self.ntEx, self.ntEy] + ([] if self.dim == 2 else [self.ntEz]) @property def stencil_cell_gradient(self): if getattr(self, "_stencil_cell_gradient", None) is None: self._stencil_cell_gradient = sp.vstack( [self.stencil_cell_gradient_x, self.stencil_cell_gradient_y]) if self.dim == 3: self._stencil_cell_gradient = sp.vstack([ self._stencil_cell_gradient, self.stencil_cell_gradient_z ]) return self._stencil_cell_gradient @property def cell_gradient(self): if getattr(self, "_cell_gradient", None) is None: i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invMat=True) if self.dim == 2: Pi = sp.block_diag([Pafx, Pafy]) elif self.dim == 3: iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) Pi = sp.block_diag([Pafx, Pafy, Pafz]) self._cell_gradient = (-Pi * MfI * self.face_divergence.T * sp.diags(self.cell_volumes)) return self._cell_gradient @property def cell_gradient_x(self): if getattr(self, "_cell_gradient_x", None) is None: nFx = self.nFx i_s = self.face_boundary_indices ix = np.ones(self.nFx) ix[i_s[0]] = 0.0 ix[i_s[1]] = 0.0 Pafx = sp.diags(ix) MfI = self.get_face_inner_product(invMat=True) MfIx = sp.diags(MfI.diagonal()[:nFx]) self._cell_gradient_x = (-Pafx * MfIx * self.face_x_divergence.T * sp.diags(self.cell_volumes)) return self._cell_gradient_x @property def cell_gradient_y(self): if getattr(self, "_cell_gradient_y", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iy = np.ones(self.nFy) iy[i_s[2]] = 0.0 iy[i_s[3]] = 0.0 Pafy = sp.diags(iy) MfI = self.get_face_inner_product(invMat=True) MfIy = sp.diags(MfI.diagonal()[nFx:nFx + nFy]) self._cell_gradient_y = (-Pafy * MfIy * self.face_y_divergence.T * sp.diags(self.cell_volumes)) return self._cell_gradient_y @property def cell_gradient_z(self): if self.dim == 2: raise TypeError("z derivative not defined in 2D") if getattr(self, "_cell_gradient_z", None) is None: nFx = self.nFx nFy = self.nFy i_s = self.face_boundary_indices iz = np.ones(self.nFz) iz[i_s[4]] = 0.0 iz[i_s[5]] = 0.0 Pafz = sp.diags(iz) MfI = self.get_face_inner_product(invMat=True) MfIz = sp.diags(MfI.diagonal()[nFx + nFy:]) self._cell_gradient_z = (-Pafz * MfIz * self.face_z_divergence.T * sp.diags(self.cell_volumes)) return self._cell_gradient_z @property def face_x_divergence(self): if getattr(self, "_face_x_divergence", None) is None: self._face_x_divergence = self.face_divergence[:, :self.nFx] return self._face_x_divergence @property def face_y_divergence(self): if getattr(self, "_face_y_divergence", None) is None: self._face_y_divergence = self.face_divergence[:, self.nFx:self.nFx + self.nFy] return self._face_y_divergence @property def face_z_divergence(self): if getattr(self, "_face_z_divergence", None) is None: self._face_z_divergence = self.face_divergence[:, self.nFx + self.nFy:] return self._face_z_divergence def point2index(self, locs): """Finds cells that contain the given points. Returns an array of index values of the cells that contain the given points Parameters ---------- locs: (N, dim) array_like points to search for the location of Returns ------- (N) array_like of int Cell indices that contain the points """ locs = as_array_n_by_dim(locs, self.dim) inds = self._get_containing_cell_indexes(locs) return inds def cell_levels_by_index(self, indices): """Fast function to return a list of levels for the given cell indices Parameters ---------- index: (N) array_like Cell indexes to query Returns ------- (N) numpy.ndarray of int Levels for the cells. """ return self._cell_levels_by_indexes(indices) def get_interpolation_matrix(self, locs, location_type="CC", zeros_outside=False, **kwargs): """Produces interpolation matrix Parameters ---------- loc : (N, dim) array_like Location of points to interpolate to location_type: str, optional What to interpolate location_type can be: - 'CC' -> scalar field defined on cell centers - 'Ex' -> x-component of field defined on edges - 'Ey' -> y-component of field defined on edges - 'Ez' -> z-component of field defined on edges - 'Fx' -> x-component of field defined on faces - 'Fy' -> y-component of field defined on faces - 'Fz' -> z-component of field defined on faces - 'N' -> scalar field defined on nodes Returns ------- (N, n_loc_type) scipy.sparse.csr_matrix the interpolation matrix """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] if "zerosOutside" in kwargs: warnings.warn( "The zerosOutside keyword argument has been deprecated, please use zeros_outside. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) zeros_outside = kwargs["zerosOutside"] locs = as_array_n_by_dim(locs, self.dim) if location_type not in [ "N", "CC", "Ex", "Ey", "Ez", "Fx", "Fy", "Fz" ]: raise Exception( "location_type must be one of N, CC, Ex, Ey, Ez, Fx, Fy, or Fz" ) if self.dim == 2 and location_type in ["Ez", "Fz"]: raise Exception("Unable to interpolate from Z edges/face in 2D") locs = np.require(np.atleast_2d(locs), dtype=np.float64, requirements="C") if location_type == "N": Av = self._getNodeIntMat(locs, zeros_outside) elif location_type in ["Ex", "Ey", "Ez"]: Av = self._getEdgeIntMat(locs, zeros_outside, location_type[1]) elif location_type in ["Fx", "Fy", "Fz"]: Av = self._getFaceIntMat(locs, zeros_outside, location_type[1]) elif location_type in ["CC"]: Av = self._getCellIntMat(locs, zeros_outside) return Av @property def permute_cells(self): """Permutation matrix re-ordering of cells sorted by x, then y, then z Returns ------- (n_cells, n_cells) scipy.sparse.csr_matrix """ # TODO: cache these? P = np.lexsort(self.gridCC.T) # sort by x, then y, then z return sp.identity(self.nC).tocsr()[P] @property def permute_faces(self): """Permutation matrix re-ordering of faces sorted by x, then y, then z Returns ------- (n_faces, n_faces) scipy.sparse.csr_matrix """ # TODO: cache these? Px = np.lexsort(self.gridFx.T) Py = np.lexsort(self.gridFy.T) + self.nFx if self.dim == 2: P = np.r_[Px, Py] else: Pz = np.lexsort(self.gridFz.T) + (self.nFx + self.nFy) P = np.r_[Px, Py, Pz] return sp.identity(self.nF).tocsr()[P] @property def permute_edges(self): """Permutation matrix re-ordering of edges sorted by x, then y, then z Returns ------- (n_edges, n_edges) scipy.sparse.csr_matrix """ # TODO: cache these? Px = np.lexsort(self.gridEx.T) Py = np.lexsort(self.gridEy.T) + self.nEx if self.dim == 2: P = np.r_[Px, Py] if self.dim == 3: Pz = np.lexsort(self.gridEz.T) + (self.nEx + self.nEy) P = np.r_[Px, Py, Pz] return sp.identity(self.nE).tocsr()[P] @property def cell_state(self): """ The current state of the cells on the mesh. This represents the x, y, z indices of the cells in the base tensor mesh, as well as their levels. It can be used to reconstruct the mesh. Returns ------- dict dictionary with two entries: - ``"indexes"``: the indexes of the cells - ``"levels"``: the levels of the cells """ indexes, levels = self.__getstate__() return {"indexes": indexes.tolist(), "levels": levels.tolist()} def validate(self): return self.finalized def equals(self, other): try: if self.finalized and other.finalized: return super().equals(other) except AttributeError: pass return False def __reduce__(self): return TreeMesh, (self.h, self.origin), self.__getstate__() cellGrad = deprecate_property("cell_gradient", "cellGrad", removal_version="1.0.0", future_warn=False) cellGradx = deprecate_property("cell_gradient_x", "cellGradx", removal_version="1.0.0", future_warn=False) cellGrady = deprecate_property("cell_gradient_y", "cellGrady", removal_version="1.0.0", future_warn=False) cellGradz = deprecate_property("cell_gradient_z", "cellGradz", removal_version="1.0.0", future_warn=False) cellGradStencil = deprecate_property("cell_gradient_stencil", "cellGradStencil", removal_version="1.0.0", future_warn=False) nodalGrad = deprecate_property("nodal_gradient", "nodalGrad", removal_version="1.0.0", future_warn=False) nodalLaplacian = deprecate_property("nodal_laplacian", "nodalLaplacian", removal_version="1.0.0", future_warn=False) faceDiv = deprecate_property("face_divergence", "faceDiv", removal_version="1.0.0", future_warn=False) faceDivx = deprecate_property("face_x_divergence", "faceDivx", removal_version="1.0.0", future_warn=False) faceDivy = deprecate_property("face_y_divergence", "faceDivy", removal_version="1.0.0", future_warn=False) faceDivz = deprecate_property("face_z_divergence", "faceDivz", removal_version="1.0.0", future_warn=False) edgeCurl = deprecate_property("edge_curl", "edgeCurl", removal_version="1.0.0", future_warn=False) maxLevel = deprecate_property("max_used_level", "maxLevel", removal_version="1.0.0", future_warn=False) vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", future_warn=False) areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0", future_warn=False) areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0", future_warn=False) areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0", future_warn=False) area = deprecate_property("face_areas", "area", removal_version="1.0.0", future_warn=False) edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0", future_warn=False) edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0", future_warn=False) edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0", future_warn=False) edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0", future_warn=False) permuteCC = deprecate_property("permute_cells", "permuteCC", removal_version="1.0.0", future_warn=False) permuteF = deprecate_property("permute_faces", "permuteF", removal_version="1.0.0", future_warn=False) permuteE = deprecate_property("permute_edges", "permuteE", removal_version="1.0.0", future_warn=False) faceBoundaryInd = deprecate_property("face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0", future_warn=False) cellBoundaryInd = deprecate_property("cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0", future_warn=False) _aveCC2FxStencil = deprecate_property("average_cell_to_total_face_x", "_aveCC2FxStencil", removal_version="1.0.0", future_warn=False) _aveCC2FyStencil = deprecate_property("average_cell_to_total_face_y", "_aveCC2FyStencil", removal_version="1.0.0", future_warn=False) _aveCC2FzStencil = deprecate_property("average_cell_to_total_face_z", "_aveCC2FzStencil", removal_version="1.0.0", future_warn=False) _cellGradStencil = deprecate_property("stencil_cell_gradient", "_cellGradStencil", removal_version="1.0.0", future_warn=False) _cellGradxStencil = deprecate_property("stencil_cell_gradient_x", "_cellGradxStencil", removal_version="1.0.0", future_warn=False) _cellGradyStencil = deprecate_property("stencil_cell_gradient_y", "_cellGradyStencil", removal_version="1.0.0", future_warn=False) _cellGradzStencil = deprecate_property("stencil_cell_gradient_z", "_cellGradzStencil", removal_version="1.0.0", future_warn=False)
class CurvilinearMesh(BaseRectangularMesh, DiffOperators, InnerProducts): """CurvilinearMesh is a mesh class that deals with curvilinear meshes. Example of a curvilinear mesh: .. plot:: :include-source: import discretize X, Y = discretize.utils.exampleLrmGrid([3,3],'rotate') mesh = discretize.CurvilinearMesh([X, Y]) mesh.plot_grid(show_it=True) """ _meshType = "Curv" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **{ "gridCC": "cell_centers", "gridN": "nodes", "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } node_list = properties.List( "List of arrays describing the node locations", prop=properties.Array( "node locations in an n-dimensional array", shape={("*", "*"), ("*", "*", "*")}, ), min_length=2, max_length=3, ) def __init__(self, node_list=None, **kwargs): if 'nodes' in kwargs: node_list = kwargs.pop('nodes') self.node_list = node_list if "_n" in kwargs.keys(): n = kwargs.pop("_n") if np.any(n != np.array(self.node_list[0].shape) - 1): raise ValueError( "Unexpected n-values. {} was provided, {} was expected".format( n, np.array(self.node_list[0].shape) - 1 ) ) else: n = np.array(self.node_list[0].shape) - 1 BaseRectangularMesh.__init__(self, n, **kwargs) # Save nodes to private variable _nodes as vectors self._nodes = np.ones((self.node_list[0].size, self.dim)) for i, node_i in enumerate(self.node_list): self._nodes[:, i] = mkvc(node_i.astype(float)) self.origin = self.nodes.min(axis=0) @properties.validator("node_list") def _check_nodes(self, change): if len(change["value"]) <= 1: raise ValueError("len(node) must be greater than 1") for i, change["value"][i] in enumerate(change["value"]): if change["value"][i].shape != change["value"][0].shape: raise ValueError( "change['value'][{0:d}] is not the same shape as " "change['value'][0]".format(i) ) if len(change["value"][0].shape) != len(change["value"]): raise ValueError("Dimension mismatch") @classmethod def deserialize(cls, value, **kwargs): if "nodes" in value: value["node_list"] = value.pop("nodes") return super().deserialize(value, **kwargs) @property def cell_centers(self): """ Cell-centered grid """ if getattr(self, "_cell_centers", None) is None: self._cell_centers = np.concatenate( [self.aveN2CC * self.gridN[:, i] for i in range(self.dim)] ).reshape((-1, self.dim), order="F") return self._cell_centers @property def nodes(self): """ Nodal grid. """ if getattr(self, "_nodes", None) is None: raise Exception("Someone deleted this. I blame you.") return self._nodes @property def faces_x(self): """ Face staggered grid in the x direction. """ if getattr(self, "_faces_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._faces_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:, :-1, :-1] + n[:, :-1, 1:] + n[:, 1:, :-1] + n[:, 1:, 1:] ) ) for n in N ] self._faces_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_x @property def faces_y(self): """ Face staggered grid in the y direction. """ if getattr(self, "_faces_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._faces_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [ mkvc( 0.25 * ( n[:-1, :, :-1] + n[:-1, :, 1:] + n[1:, :, :-1] + n[1:, :, 1:] ) ) for n in N ] self._faces_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_y @property def faces_z(self): """ Face staggered grid in the y direction. """ if getattr(self, "_faces_z", None) is None: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [ mkvc( 0.25 * (n[:-1, :-1, :] + n[:-1, 1:, :] + n[1:, :-1, :] + n[1:, 1:, :]) ) for n in N ] self._faces_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._faces_z @property def edges_x(self): """ Edge staggered grid in the x direction. """ if getattr(self, "_edges_x", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N] self._edges_x = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:-1, :, :] + n[1:, :, :])) for n in N] self._edges_x = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_x @property def edges_y(self): """ Edge staggered grid in the y direction. """ if getattr(self, "_edges_y", None) is None: N = self.reshape(self.gridN, "N", "N", "M") if self.dim == 2: XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N] self._edges_y = np.c_[XY[0], XY[1]] elif self.dim == 3: XYZ = [mkvc(0.5 * (n[:, :-1, :] + n[:, 1:, :])) for n in N] self._edges_y = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_y @property def edges_z(self): """ Edge staggered grid in the z direction. """ if getattr(self, "_edges_z", None) is None and self.dim == 3: N = self.reshape(self.gridN, "N", "N", "M") XYZ = [mkvc(0.5 * (n[:, :, :-1] + n[:, :, 1:])) for n in N] self._edges_z = np.c_[XYZ[0], XYZ[1], XYZ[2]] return self._edges_z # --------------- Geometries --------------------- # # # ------------------- 2D ------------------------- # # node(i,j) node(i,j+1) # A -------------- B # | | # | cell(i,j) | # | I | # | | # D -------------- C # node(i+1,j) node(i+1,j+1) # # ------------------- 3D ------------------------- # # # node(i,j,k+1) node(i,j+1,k+1) # E --------------- F # /| / | # / | / | # / | / | # node(i,j,k) node(i,j+1,k) # A -------------- B | # | H ----------|---- G # | /cell(i,j) | / # | / I | / # | / | / # D -------------- C # node(i+1,j,k) node(i+1,j+1,k) @property def cell_volumes(self): """ Construct cell volumes of the 3D model as 1d array """ if getattr(self, "_cell_volumes", None) is None: if self.dim == 2: A, B, C, D = index_cube("ABCD", self.vnN) normal, area = face_info( np.c_[self.gridN, np.zeros((self.nN, 1))], A, B, C, D ) self._cell_volumes = area elif self.dim == 3: # Each polyhedron can be decomposed into 5 tetrahedrons # However, this presents a choice so we may as well divide in # two ways and average. A, B, C, D, E, F, G, H = index_cube("ABCDEFGH", self.vnN) vol1 = ( volume_tetrahedron(self.gridN, A, B, D, E) + volume_tetrahedron(self.gridN, B, E, F, G) # cutted edge top + volume_tetrahedron(self.gridN, B, D, E, G) # cutted edge top + volume_tetrahedron(self.gridN, B, C, D, G) # middle + volume_tetrahedron(self.gridN, D, E, G, H) # cutted edge bottom ) # cutted edge bottom vol2 = ( volume_tetrahedron(self.gridN, A, F, B, C) + volume_tetrahedron(self.gridN, A, E, F, H) # cutted edge top + volume_tetrahedron(self.gridN, A, H, F, C) # cutted edge top + volume_tetrahedron(self.gridN, C, H, D, A) # middle + volume_tetrahedron(self.gridN, C, G, H, F) # cutted edge bottom ) # cutted edge bottom self._cell_volumes = (vol1 + vol2) / 2 return self._cell_volumes @property def face_areas(self): """ Area of the faces """ if ( getattr(self, "_face_areas", None) is None or getattr(self, "_normals", None) is None ): # Compute areas of cell faces if self.dim == 2: xy = self.gridN A, B = index_cube("AB", self.vnN, self.vnFx) edge1 = xy[B, :] - xy[A, :] normal1 = np.c_[edge1[:, 1], -edge1[:, 0]] area1 = _length2D(edge1) A, D = index_cube("AD", self.vnN, self.vnFy) # Note that we are doing A-D to make sure the normal points the # right way. # Think about it. Look at the picture. Normal points towards C # iff you do this. edge2 = xy[A, :] - xy[D, :] normal2 = np.c_[edge2[:, 1], -edge2[:, 0]] area2 = _length2D(edge2) self._face_areas = np.r_[mkvc(area1), mkvc(area2)] self._normals = [_normalize2D(normal1), _normalize2D(normal2)] elif self.dim == 3: A, E, F, B = index_cube("AEFB", self.vnN, self.vnFx) normal1, area1 = face_info( self.gridN, A, E, F, B, average=False, normalizeNormals=False ) A, D, H, E = index_cube("ADHE", self.vnN, self.vnFy) normal2, area2 = face_info( self.gridN, A, D, H, E, average=False, normalizeNormals=False ) A, B, C, D = index_cube("ABCD", self.vnN, self.vnFz) normal3, area3 = face_info( self.gridN, A, B, C, D, average=False, normalizeNormals=False ) self._face_areas = np.r_[mkvc(area1), mkvc(area2), mkvc(area3)] self._normals = [normal1, normal2, normal3] return self._face_areas @property def face_normals(self): """ Face normals: calling this will average the computed normals so that there is one per face. This is especially relevant in 3D, as there are up to 4 different normals for each face that will be different. To reshape the normals into a matrix and get the y component:: NyX, NyY, NyZ = M.reshape(M.face_normals, 'F', 'Fy', 'M') """ if getattr(self, "_normals", None) is None: self.face_areas # calling .face_areas will create the face normals if self.dim == 2: return _normalize2D(np.r_[self._normals[0], self._normals[1]]) elif self.dim == 3: normal1 = ( self._normals[0][0] + self._normals[0][1] + self._normals[0][2] + self._normals[0][3] ) / 4 normal2 = ( self._normals[1][0] + self._normals[1][1] + self._normals[1][2] + self._normals[1][3] ) / 4 normal3 = ( self._normals[2][0] + self._normals[2][1] + self._normals[2][2] + self._normals[2][3] ) / 4 return _normalize3D(np.r_[normal1, normal2, normal3]) @property def edge_lengths(self): """Edge lengths""" if getattr(self, "_edge_lengths", None) is None: if self.dim == 2: xy = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xy[D, :] - xy[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xy[B, :] - xy[A, :] self._edge_lengths = np.r_[mkvc(_length2D(edge1)), mkvc(_length2D(edge2))] self._edge_tangents = np.r_[edge1, edge2] / np.c_[self._edge_lengths, self._edge_lengths] elif self.dim == 3: xyz = self.gridN A, D = index_cube("AD", self.vnN, self.vnEx) edge1 = xyz[D, :] - xyz[A, :] A, B = index_cube("AB", self.vnN, self.vnEy) edge2 = xyz[B, :] - xyz[A, :] A, E = index_cube("AE", self.vnN, self.vnEz) edge3 = xyz[E, :] - xyz[A, :] self._edge_lengths = np.r_[ mkvc(_length3D(edge1)), mkvc(_length3D(edge2)), mkvc(_length3D(edge3)), ] self._edge_tangents = ( np.r_[edge1, edge2, edge3] / np.c_[self._edge_lengths, self._edge_lengths, self._edge_lengths] ) return self._edge_lengths @property def edge_tangents(self): """Edge tangents""" if getattr(self, "_edge_tangents", None) is None: self.edge_lengths # calling .edge_lengths will create the tangents return self._edge_tangents # DEPRECATIONS vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0") area = deprecate_property("face_areas", "area", removal_version="1.0.0") edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0")
class BaseMesh(properties.HasProperties, InterfaceMixins): """ BaseMesh does all the counting you don't want to do. BaseMesh should be inherited by meshes with a regular structure. """ _REGISTRY = {} _aliases = { "nC": "n_cells", "nN": "n_nodes", "nEx": "n_edges_x", "nEy": "n_edges_y", "nEz": "n_edges_z", "nE": "n_edges", "vnE": "n_edges_per_direction", "nFx": "n_faces_x", "nFy": "n_faces_y", "nFz": "n_faces_z", "nF": "n_faces", "vnF": "n_faces_per_direction", } # Properties _n = properties.Tuple( "Tuple of number of cells in each direction (dim, )", prop=properties.Integer("Number of cells along a particular direction", cast=True, min=1), min_length=1, max_length=3, coerce=True, required=True, ) origin = properties.Array( "origin of the mesh (dim, )", dtype=(float, int), shape=("*", ), required=True, ) # Instantiate the class def __init__(self, n=None, origin=None, **kwargs): if n is not None: self._n = n # number of dimensions if "x0" in kwargs: origin = kwargs.pop('x0') if origin is None: self.origin = np.zeros(len(self._n)) else: self.origin = origin super(BaseMesh, self).__init__(**kwargs) def __getattr__(self, name): if name == "_aliases": raise AttributeError # http://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html name = self._aliases.get(name, name) return object.__getattribute__(self, name) @property def x0(self): return self.origin @x0.setter def x0(self, val): self.origin = val @classmethod def deserialize(cls, value, **kwargs): if "x0" in value: value["origin"] = value.pop("x0") return super().deserialize(value, **kwargs) # Validators @properties.validator("_n") def _check_n_shape(self, change): if change["previous"] != properties.undefined: # _n can only be set once if change["previous"] != change["value"]: raise AttributeError( "Cannot change n. Instead, create a new mesh") else: # check that if h has been set, sizes still agree if getattr(self, "h", None) is not None and len(self.h) > 0: for i in range(len(change["value"])): if len(self.h[i]) != change["value"][i]: raise properties.ValidationError( "Mismatched shape of n. Expected {}, len(h[{}]), got " "{}".format(len(self.h[i]), i, change["value"][i])) # check that if nodes have been set for curvi mesh, sizes still # agree if getattr(self, "node_list", None) is not None and len(self.node_list) > 0: for i in range(len(change["value"])): if self.node_list[0].shape[i] - 1 != change["value"][i]: raise properties.ValidationError( "Mismatched shape of n. Expected {}, len(node_list[{}]), " "got {}".format(self.node_list[0].shape[i] - 1, i, change["value"][i])) @properties.validator("origin") def _check_origin(self, change): if not (not isinstance(change["value"], properties.utils.Sentinel) and change["value"] is not None): raise Exception("n must be set prior to setting origin") if len(self._n) != len(change["value"]): raise Exception( "Dimension mismatch. origin has length {} != len(n) which is " "{}".format(len(self.origin), len(self._n))) @property def dim(self): """The dimension of the mesh (1, 2, or 3). Returns ------- int dimension of the mesh """ return len(self._n) @property def n_cells(self): """Total number of cells in the mesh. Returns ------- int number of cells in the mesh Notes ----- Also accessible as `nC`. Examples -------- >>> import discretize >>> import numpy as np >>> import matplotlib.pyplot as plt >>> mesh = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> mesh.plot_grid(centers=True, show_it=True) >>> print(mesh.n_cells) """ return int(np.prod(self._n)) def __len__(self): """The number of cells on the mesh.""" return self.n_cells @property def n_nodes(self): """Total number of nodes Returns ------- int number of nodes in the mesh Notes ----- Also accessible as `nN`. Examples -------- >>> import discretize >>> import numpy as np >>> import matplotlib.pyplot as plt >>> mesh = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> mesh.plot_grid(nodes=True, show_it=True) >>> print(mesh.n_nodes) """ return int(np.prod(x + 1 for x in self._n)) @property def n_edges_x(self): """Number of x-edges Returns ------- int Notes ----- Also accessible as `nEx`. """ return int(np.prod(x + y for x, y in zip(self._n, (0, 1, 1)))) @property def n_edges_y(self): """Number of y-edges Returns ------- int Notes ----- Also accessible as `nEy`. """ if self.dim < 2: return None return int(np.prod(x + y for x, y in zip(self._n, (1, 0, 1)))) @property def n_edges_z(self): """Number of z-edges Returns ------- int Notes ----- Also accessible as `nEz`. """ if self.dim < 3: return None return int(np.prod(x + y for x, y in zip(self._n, (1, 1, 0)))) @property def n_edges_per_direction(self): """The number of edges in each direction Returns ------- n_edges_per_direction : tuple [n_edges_x, n_edges_y, n_edges_z], (dim, ) Notes ----- Also accessible as `vnE`. Examples -------- >>> import discretize >>> import matplotlib.pyplot as plt >>> import numpy as np >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> M.plot_grid(edges=True, show_it=True) """ return tuple(x for x in [self.n_edges_x, self.n_edges_y, self.n_edges_z] if x is not None) @property def n_edges(self): """Total number of edges. Returns ------- int sum([n_edges_x, n_edges_y, n_edges_z]) Notes ----- Also accessible as `nE`. """ n = self.n_edges_x if self.dim > 1: n += self.n_edges_y if self.dim > 2: n += self.n_edges_z return n @property def n_faces_x(self): """Number of x-faces Returns ------- int Notes ----- Also accessible as `nFx`. """ return int(np.prod(x + y for x, y in zip(self._n, (1, 0, 0)))) @property def n_faces_y(self): """Number of y-faces Returns ------- int Notes ----- Also accessible as `nFy`. """ if self.dim < 2: return None return int(np.prod(x + y for x, y in zip(self._n, (0, 1, 0)))) @property def n_faces_z(self): """Number of z-faces Returns ------- int Notes ----- Also accessible as `nFz`. """ if self.dim < 3: return None return int(np.prod(x + y for x, y in zip(self._n, (0, 0, 1)))) @property def n_faces_per_direction(self): """The number of faces in each direction Returns ------- n_faces_per_direction : tuple [n_faces_x, n_faces_y, n_faces_z], (dim, ) Notes ----- Also accessible as `vnF`. Examples -------- >>> import discretize >>> import numpy as np >>> import matplotlib.pyplot as plt >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]]) >>> M.plot_grid(faces=True, show_it=True) """ return tuple(x for x in [self.n_faces_x, self.n_faces_y, self.n_faces_z] if x is not None) @property def n_faces(self): """Total number of faces. Returns ------- int sum([n_faces_x, n_faces_y, n_faces_z]) Notes ----- Also accessible as `nF`. """ n = self.n_faces_x if self.dim > 1: n += self.n_faces_y if self.dim > 2: n += self.n_faces_z return n @property def face_normals(self): """Face Normals Returns ------- numpy.ndarray normals, (n_faces, dim) """ if self.dim == 2: nX = np.c_[np.ones(self.n_faces_x), np.zeros(self.n_faces_x)] nY = np.c_[np.zeros(self.n_faces_y), np.ones(self.n_faces_y)] return np.r_[nX, nY] elif self.dim == 3: nX = np.c_[np.ones(self.n_faces_x), np.zeros(self.n_faces_x), np.zeros(self.n_faces_x), ] nY = np.c_[np.zeros(self.n_faces_y), np.ones(self.n_faces_y), np.zeros(self.n_faces_y), ] nZ = np.c_[np.zeros(self.n_faces_z), np.zeros(self.n_faces_z), np.ones(self.n_faces_z), ] return np.r_[nX, nY, nZ] @property def edge_tangents(self): """Edge Tangents Returns ------- numpy.ndarray normals, (n_edges, dim) """ if self.dim == 2: tX = np.c_[np.ones(self.n_edges_x), np.zeros(self.n_edges_x)] tY = np.c_[np.zeros(self.n_edges_y), np.ones(self.n_edges_y)] return np.r_[tX, tY] elif self.dim == 3: tX = np.c_[np.ones(self.n_edges_x), np.zeros(self.n_edges_x), np.zeros(self.n_edges_x), ] tY = np.c_[np.zeros(self.n_edges_y), np.ones(self.n_edges_y), np.zeros(self.n_edges_y), ] tZ = np.c_[np.zeros(self.n_edges_z), np.zeros(self.n_edges_z), np.ones(self.n_edges_z), ] return np.r_[tX, tY, tZ] def project_face_vector(self, face_vector): """Project vectors onto the faces of the mesh. Given a vector, face_vector, in cartesian coordinates, this will project it onto the mesh using the normals Parameters ---------- face_vector : numpy.ndarray face vector with shape (n_faces, dim) Returns ------- numpy.ndarray projected face vector, (n_faces, ) """ if not isinstance(face_vector, np.ndarray): raise Exception("face_vector must be an ndarray") if not (len(face_vector.shape) == 2 and face_vector.shape[0] == self.n_faces and face_vector.shape[1] == self.dim): raise Exception( "face_vector must be an ndarray of shape (n_faces x dim)") return np.sum(face_vector * self.face_normals, 1) def project_edge_vector(self, edge_vector): """Project vectors onto the edges of the mesh Given a vector, edge_vector, in cartesian coordinates, this will project it onto the mesh using the tangents Parameters ---------- edge_vector : numpy.ndarray edge vector with shape (n_edges, dim) Returns ------- numpy.ndarray projected edge vector, (n_edges, ) """ if not isinstance(edge_vector, np.ndarray): raise Exception("edge_vector must be an ndarray") if not (len(edge_vector.shape) == 2 and edge_vector.shape[0] == self.n_edges and edge_vector.shape[1] == self.dim): raise Exception( "edge_vector must be an ndarray of shape (nE x dim)") return np.sum(edge_vector * self.edge_tangents, 1) def save(self, file_name="mesh.json", verbose=False, **kwargs): """ Save the mesh to json :param str file: file_name for saving the casing properties :param str directory: working directory for saving the file """ if 'filename' in kwargs: file_name = kwargs['filename'] warnings.warn( "The filename keyword argument has been deprecated, please use file_name. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) f = os.path.abspath( file_name) # make sure we are working with abs path with open(f, "w") as outfile: json.dump(self.serialize(), outfile) if verbose: print("Saved {}".format(f)) return f def copy(self): """ Make a copy of the current mesh """ return properties.copy(self) axis_u = properties.Vector3( "Vector orientation of u-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.", default="X", length=1, ) axis_v = properties.Vector3( "Vector orientation of v-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.", default="Y", length=1, ) axis_w = properties.Vector3( "Vector orientation of w-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.", default="Z", length=1, ) @properties.validator def _validate_orientation(self): """Check if axes are orthogonal""" tol = 1E-6 if not (np.abs(self.axis_u.dot(self.axis_v) < tol) and np.abs(self.axis_v.dot(self.axis_w) < tol) and np.abs(self.axis_w.dot(self.axis_u) < tol)): raise ValueError("axis_u, axis_v, and axis_w must be orthogonal") return True @property def reference_is_rotated(self): """True if the axes are rotated from the traditional <X,Y,Z> system with vectors of :math:`(1,0,0)`, :math:`(0,1,0)`, and :math:`(0,0,1)` """ if (np.allclose(self.axis_u, (1, 0, 0)) and np.allclose(self.axis_v, (0, 1, 0)) and np.allclose(self.axis_w, (0, 0, 1))): return False return True @property def rotation_matrix(self): """Builds a rotation matrix to transform coordinates from their coordinate system into a conventional cartesian system. This is built off of the three `axis_u`, `axis_v`, and `axis_w` properties; these mapping coordinates use the letters U, V, and W (the three letters preceding X, Y, and Z in the alphabet) to define the projection of the X, Y, and Z durections. These UVW vectors describe the placement and transformation of the mesh's coordinate sytem assuming at most 3 directions. Why would you want to use these UVW mapping vectors the this `rotation_matrix` property? They allow us to define the realationship between local and global coordinate systems and provide a tool for switching between the two while still maintaing the connectivity of the mesh's cells. For a visual example of this, please see the figure in the docs for the :class:`~discretize.mixins.vtk_mod.InterfaceVTK`. """ return np.array([self.axis_u, self.axis_v, self.axis_w]) reference_system = properties.String( "The type of coordinate reference frame. Can take on the values " + "cartesian, cylindrical, or spherical. Abbreviations of these are allowed.", default="cartesian", change_case="lower", ) @properties.validator def _validate_reference_system(self): """Check if the reference system is of a known type.""" choices = ["cartesian", "cylindrical", "spherical"] # Here are a few abbreviations that users can harnes abrevs = { "car": choices[0], "cart": choices[0], "cy": choices[1], "cyl": choices[1], "sph": choices[2], } # Get the name and fix it if it is abbreviated self.reference_system = abrevs.get(self.reference_system, self.reference_system) if self.reference_system not in choices: raise ValueError("Coordinate system ({}) unknown.".format( self.reference_system)) return True def _parse_location_type(self, location_type): if len(location_type) == 0: return location_type elif location_type[0] == "F": if len(location_type) > 1: return "faces_" + location_type[-1] else: return "faces" elif location_type[0] == "E": if len(location_type) > 1: return "edges_" + location_type[-1] else: return "edges" elif location_type[0] == "N": return "nodes" elif location_type[0] == "C": if len(location_type) > 2: return "cell_centers_" + location_type[-1] else: return "cell_centers" else: return location_type # DEPRECATED normals = deprecate_property("face_normals", "normals", removal_version="1.0.0") tangents = deprecate_property("edge_tangents", "tangents", removal_version="1.0.0") projectEdgeVector = deprecate_method("project_edge_vector", "projectEdgeVector", removal_version="1.0.0") projectFaceVector = deprecate_method("project_face_vector", "projectFaceVector", removal_version="1.0.0")
class BaseTensorMesh(BaseMesh): """ Base class for tensor-product style meshes This class contains properites and methods that are common to cartesian and cylindrical meshes defined by tensor-produts of vectors describing cell spacings. Do not use this class directly, instead, inherit it if you plan to develop a tensor-style mesh (e.g. a spherical mesh) or use the :meth:`discretize.TensorMesh` class to create a cartesian tensor mesh. """ _meshType = "BASETENSOR" _aliases = { **BaseMesh._aliases, **{ "gridCC": "cell_centers", "gridN": "nodes", "gridFx": "faces_x", "gridFy": "faces_y", "gridFz": "faces_z", "gridEx": "edges_x", "gridEy": "edges_y", "gridEz": "edges_z", }, } _unitDimensions = [1, 1, 1] # properties h = properties.Tuple( "h is a list containing the cell widths of the tensor mesh in each " "dimension.", properties.Array( "widths of the tensor mesh in a single dimension", dtype=float, shape=("*", ), ), min_length=1, max_length=3, coerce=True, required=True, ) def __init__(self, h=None, origin=None, **kwargs): h_in = h if "x0" in kwargs: origin = kwargs.pop('x0') origin_in = origin # Sanity Checks if not isinstance(h_in, (list, tuple)): raise TypeError("h_in must be a list, not {}".format(type(h_in))) if len(h_in) not in [1, 2, 3]: raise ValueError( "h_in must be of dimension 1, 2, or 3 not {}".format( len(h_in))) # build h h = list(range(len(h_in))) for i, h_i in enumerate(h_in): if is_scalar(h_i) and not isinstance(h_i, np.ndarray): # This gives you something over the unit cube. h_i = self._unitDimensions[i] * np.ones(int(h_i)) / int(h_i) elif isinstance(h_i, (list, tuple)): h_i = unpack_widths(h_i) if not isinstance(h_i, np.ndarray): raise TypeError("h[{0:d}] is not a numpy array.".format(i)) if len(h_i.shape) != 1: raise ValueError( "h[{0:d}] must be a 1D numpy array.".format(i)) h[i] = h_i[:] # make a copy. # Origin of the mesh origin = np.zeros(len(h)) if origin_in is not None: if len(h) != len(origin_in): raise ValueError("Dimension mismatch. origin != len(h)") for i in range(len(h)): x_i, h_i = origin_in[i], h[i] if is_scalar(x_i): origin[i] = x_i elif x_i == "0": origin[i] = 0.0 elif x_i == "C": origin[i] = -h_i.sum() * 0.5 elif x_i == "N": origin[i] = -h_i.sum() else: raise Exception( "origin[{0:d}] must be a scalar or '0' to be zero, " "'C' to center, or 'N' to be negative. The input value" " {1} {2} is invalid".format(i, x_i, type(x_i))) if "n" in kwargs.keys(): n = kwargs.pop("n") if np.any(n != np.array([x.size for x in h])): raise ValueError( "Dimension mismatch. The provided n doesn't h") else: n = np.array([x.size for x in h]) super(BaseTensorMesh, self).__init__(n, origin=origin, **kwargs) # Ensure h contains 1D vectors self.h = [mkvc(x.astype(float)) for x in h] @property def nodes_x(self): """Nodal grid vector (1D) in the x direction.""" return np.r_[self.origin[0], self.h[0]].cumsum() @property def nodes_y(self): """Nodal grid vector (1D) in the y direction.""" return None if self.dim < 2 else np.r_[self.origin[1], self.h[1]].cumsum() @property def nodes_z(self): """Nodal grid vector (1D) in the z direction.""" return None if self.dim < 3 else np.r_[self.origin[2], self.h[2]].cumsum() @property def cell_centers_x(self): """Cell-centered grid vector (1D) in the x direction.""" nodes = self.nodes_x return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_y(self): """Cell-centered grid vector (1D) in the y direction.""" if self.dim < 2: return None nodes = self.nodes_y return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers_z(self): """Cell-centered grid vector (1D) in the z direction.""" if self.dim < 3: return None nodes = self.nodes_z return (nodes[1:] + nodes[:-1]) / 2 @property def cell_centers(self): """Cell-centered grid.""" return self._getTensorGrid("cell_centers") @property def nodes(self): """Nodal grid.""" return self._getTensorGrid("nodes") @property def h_gridded(self): """ Returns an (nC, dim) numpy array with the widths of all cells in order """ if self.dim == 1: return self.h[0][:, None] return ndgrid(*self.h) @property def faces_x(self): """Face staggered grid in the x direction.""" if self.nFx == 0: return return self._getTensorGrid("faces_x") @property def faces_y(self): """Face staggered grid in the y direction.""" if self.nFy == 0 or self.dim < 2: return return self._getTensorGrid("faces_y") @property def faces_z(self): """Face staggered grid in the z direction.""" if self.nFz == 0 or self.dim < 3: return return self._getTensorGrid("faces_z") @property def edges_x(self): """Edge staggered grid in the x direction.""" if self.nEx == 0: return return self._getTensorGrid("edges_x") @property def edges_y(self): """Edge staggered grid in the y direction.""" if self.nEy == 0 or self.dim < 2: return return self._getTensorGrid("edges_y") @property def edges_z(self): """Edge staggered grid in the z direction.""" if self.nEz == 0 or self.dim < 3: return return self._getTensorGrid("edges_z") def _getTensorGrid(self, key): if getattr(self, "_" + key, None) is None: setattr(self, "_" + key, ndgrid(self.get_tensor(key))) return getattr(self, "_" + key) def get_tensor(self, key): """Returns a tensor list. Parameters ---------- key : str Which tensor (see below) key can be:: 'CC', 'cell_centers' -> location of cell centers 'N', 'nodes' -> location of nodes 'Fx', 'faces_x' -> location of faces with an x normal 'Fy', 'faces_y' -> location of faces with an y normal 'Fz', 'faces_z' -> location of faces with an z normal 'Ex', 'edges_x' -> location of edges with an x tangent 'Ey', 'edges_y' -> location of edges with an y tangent 'Ez', 'edges_z' -> location of edges with an z tangent Returns ------- list list of the tensors that make up the mesh. """ key = self._parse_location_type(key) if key == "faces_x": ten = [ self.nodes_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "faces_y": ten = [ self.cell_centers_x, self.nodes_y, self.cell_centers_z, ] elif key == "faces_z": ten = [ self.cell_centers_x, self.cell_centers_y, self.nodes_z, ] elif key == "edges_x": ten = [self.cell_centers_x, self.nodes_y, self.nodes_z] elif key == "edges_y": ten = [self.nodes_x, self.cell_centers_y, self.nodes_z] elif key == "edges_z": ten = [self.nodes_x, self.nodes_y, self.cell_centers_z] elif key == "cell_centers": ten = [ self.cell_centers_x, self.cell_centers_y, self.cell_centers_z, ] elif key == "nodes": ten = [self.nodes_x, self.nodes_y, self.nodes_z] else: raise KeyError(r"Unrecognized key {key}") return [t for t in ten if t is not None] # --------------- Methods --------------------- def is_inside(self, pts, location_type="nodes", **kwargs): """ Determines if a set of points are inside a mesh. :param numpy.ndarray pts: Location of points to test :rtype: numpy.ndarray :return: inside, numpy array of booleans """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] pts = as_array_n_by_dim(pts, self.dim) tensors = self.get_tensor(location_type) if location_type[0].lower() == "n" and self._meshType == "CYL": # NOTE: for a CYL mesh we add a node to check if we are inside in # the radial direction! tensors[0] = np.r_[0.0, tensors[0]] tensors[1] = np.r_[tensors[1], 2.0 * np.pi] inside = np.ones(pts.shape[0], dtype=bool) for i, tensor in enumerate(tensors): TOL = np.diff(tensor).min() * 1.0e-10 inside = (inside & (pts[:, i] >= tensor.min() - TOL) & (pts[:, i] <= tensor.max() + TOL)) return inside def _getInterpolationMat(self, loc, location_type="cell_centers", zeros_outside=False): """Produces interpolation matrix Parameters ---------- loc : numpy.ndarray Location of points to interpolate to location_type: stc What to interpolate location_type can be:: 'Ex', 'edges_x' -> x-component of field defined on x edges 'Ey', 'edges_y' -> y-component of field defined on y edges 'Ez', 'edges_z' -> z-component of field defined on z edges 'Fx', 'faces_x' -> x-component of field defined on x faces 'Fy', 'faces_y' -> y-component of field defined on y faces 'Fz', 'faces_z' -> z-component of field defined on z faces 'N', 'nodes' -> scalar field defined on nodes 'CC', 'cell_centers' -> scalar field defined on cell centers 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers Returns ------- scipy.sparse.csr_matrix M, the interpolation matrix """ loc = as_array_n_by_dim(loc, self.dim) if not zeros_outside: if not np.all(self.is_inside(loc)): raise ValueError("Points outside of mesh") else: indZeros = np.logical_not(self.is_inside(loc)) loc[indZeros, :] = np.array( [v.mean() for v in self.get_tensor("CC")]) location_type = self._parse_location_type(location_type) if location_type in [ "faces_x", "faces_y", "faces_z", "edges_x", "edges_y", "edges_z" ]: ind = {"x": 0, "y": 1, "z": 2}[location_type[-1]] if self.dim < ind: raise ValueError("mesh is not high enough dimension.") if "f" in location_type.lower(): items = (self.nFx, self.nFy, self.nFz)[:self.dim] else: items = (self.nEx, self.nEy, self.nEz)[:self.dim] components = [spzeros(loc.shape[0], n) for n in items] components[ind] = interpolation_matrix( loc, *self.get_tensor(location_type)) # remove any zero blocks (hstack complains) components = [comp for comp in components if comp.shape[1] > 0] Q = sp.hstack(components) elif location_type in ["cell_centers", "nodes"]: Q = interpolation_matrix(loc, *self.get_tensor(location_type)) elif location_type in [ "cell_centers_x", "cell_centers_y", "cell_centers_z" ]: Q = interpolation_matrix(loc, *self.get_tensor("CC")) Z = spzeros(loc.shape[0], self.nC) if location_type[-1] == "x": Q = sp.hstack([Q, Z, Z]) elif location_type[-1] == "y": Q = sp.hstack([Z, Q, Z]) elif location_type[-1] == "z": Q = sp.hstack([Z, Z, Q]) else: raise NotImplementedError("getInterpolationMat: location_type==" + location_type + " and mesh.dim==" + str(self.dim)) if zeros_outside: Q[indZeros, :] = 0 return Q.tocsr() def get_interpolation_matrix(self, loc, location_type="cell_centers", zeros_outside=False, **kwargs): """Produces linear interpolation matrix Parameters ---------- loc : numpy.ndarray Location of points to interpolate to location_type : str What to interpolate (see below) location_type can be:: 'Ex', 'edges_x' -> x-component of field defined on x edges 'Ey', 'edges_y' -> y-component of field defined on y edges 'Ez', 'edges_z' -> z-component of field defined on z edges 'Fx', 'faces_x' -> x-component of field defined on x faces 'Fy', 'faces_y' -> y-component of field defined on y faces 'Fz', 'faces_z' -> z-component of field defined on z faces 'N', 'nodes' -> scalar field defined on nodes 'CC', 'cell_centers' -> scalar field defined on cell centers 'CCVx', 'cell_centers_x' -> x-component of vector field defined on cell centers 'CCVy', 'cell_centers_y' -> y-component of vector field defined on cell centers 'CCVz', 'cell_centers_z' -> z-component of vector field defined on cell centers Returns ------- scipy.sparse.csr_matrix M, the interpolation matrix """ if "locType" in kwargs: warnings.warn( "The locType keyword argument has been deprecated, please use location_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) location_type = kwargs["locType"] if "zerosOutside" in kwargs: warnings.warn( "The zerosOutside keyword argument has been deprecated, please use zeros_outside. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) zeros_outside = kwargs["zerosOutside"] return self._getInterpolationMat(loc, location_type, zeros_outside) def _fastInnerProduct(self, projection_type, model=None, invert_model=False, invert_matrix=False): """Fast version of getFaceInnerProduct. This does not handle the case of a full tensor property. Parameters ---------- model : numpy.array material property (tensor properties are possible) at each cell center (nC, (1, 3, or 6)) projection_type : str 'edges' or 'faces' returnP : bool returns the projection matrices invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- scipy.sparse.csr_matrix M, the inner product matrix (nF, nF) """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError( "projection_type must be 'F' for faces or 'E' for edges") if model is None: model = np.ones(self.nC) if invert_model: model = 1.0 / model if is_scalar(model): model = model * np.ones(self.nC) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim # Isotropic? or anisotropic? if model.size == self.nC: Av = getattr(self, "ave" + projection_type + "2CC") Vprop = self.cell_volumes * mkvc(model) M = n_elements * sdiag(Av.T * Vprop) elif model.size == self.nC * self.dim: Av = getattr(self, "ave" + projection_type + "2CCV") # if cyl, then only certain components are relevant due to symmetry # for faces, x, z matters, for edges, y (which is theta) matters if self._meshType == "CYL": if projection_type == "E": model = model[:, 1] # this is the action of a projection mat elif projection_type == "F": model = model[:, [0, 2]] V = sp.kron(sp.identity(n_elements), sdiag(self.cell_volumes)) M = sdiag(Av.T * V * mkvc(model)) else: return None if invert_matrix: return sdinv(M) else: return M def _fastInnerProductDeriv(self, projection_type, model, invert_model=False, invert_matrix=False): """ Parameters ---------- projection_type : str 'E' or 'F' tensorType : TensorType type of the tensor invert_model : bool inverts the material property invert_matrix : bool inverts the matrix Returns ------- function dMdmu, the derivative of the inner product matrix """ projection_type = projection_type[0].upper() if projection_type not in ["F", "E"]: raise ValueError( "projection_type must be 'F' for faces or 'E' for edges") tensorType = TensorType(self, model) dMdprop = None if invert_matrix or invert_model: MI = self._fastInnerProduct(projection_type, model, invert_model=invert_model, invert_matrix=invert_matrix) # number of elements we are averaging (equals dim for regular # meshes, but for cyl, where we use symmetry, it is 1 for edge # variables and 2 for face variables) if self._meshType == "CYL": shape = getattr(self, "vn" + projection_type) n_elements = sum([1 if x != 0 else 0 for x in shape]) else: n_elements = self.dim if tensorType == 0: # isotropic, constant Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) ones = sp.csr_matrix( (np.ones(self.nC), (range(self.nC), np.zeros(self.nC))), shape=(self.nC, 1), ) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V * ones elif invert_matrix and invert_model: dMdprop = n_elements * (sdiag(MI.diagonal()**2) * Av.T * V * ones * sdiag(1.0 / model**2)) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal()**2) * Av.T * V) elif tensorType == 1: # isotropic, variable in space Av = getattr(self, "ave" + projection_type + "2CC") V = sdiag(self.cell_volumes) if not invert_matrix and not invert_model: dMdprop = n_elements * Av.T * V elif invert_matrix and invert_model: dMdprop = n_elements * (sdiag(MI.diagonal()**2) * Av.T * V * sdiag(1.0 / model**2)) elif invert_model: dMdprop = n_elements * Av.T * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = n_elements * (sdiag(-MI.diagonal()**2) * Av.T * V) elif tensorType == 2: # anisotropic Av = getattr(self, "ave" + projection_type + "2CCV") V = sp.kron(sp.identity(self.dim), sdiag(self.cell_volumes)) if self._meshType == "CYL": Zero = sp.csr_matrix((self.nC, self.nC)) Eye = sp.eye(self.nC) if projection_type == "E": P = sp.hstack([Zero, Eye, Zero]) # print(P.todense()) elif projection_type == "F": P = sp.vstack([ sp.hstack([Eye, Zero, Zero]), sp.hstack([Zero, Zero, Eye]) ]) # print(P.todense()) else: P = sp.eye(self.nC * self.dim) if not invert_matrix and not invert_model: dMdprop = Av.T * P * V elif invert_matrix and invert_model: dMdprop = (sdiag(MI.diagonal()**2) * Av.T * P * V * sdiag(1.0 / model**2)) elif invert_model: dMdprop = Av.T * P * V * sdiag(-1.0 / model**2) elif invert_matrix: dMdprop = sdiag(-MI.diagonal()**2) * Av.T * P * V if dMdprop is not None: def innerProductDeriv(v=None): if v is None: warnings.warn( "Depreciation Warning: TensorMesh.innerProductDeriv." " You should be supplying a vector. " "Use: sdiag(u)*dMdprop", DeprecationWarning, ) return dMdprop return sdiag(v) * dMdprop return innerProductDeriv else: return None # DEPRECATED @property def hx(self): """Width of cells in the x direction Returns ------- numpy.ndarray .. deprecated:: 0.5.0 `hx` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[0]`. """ warnings.warn("hx has been deprecated, please access as mesh.h[0]", DeprecationWarning) return self.h[0] @property def hy(self): """Width of cells in the y direction Returns ------- numpy.ndarray or None .. deprecated:: 0.5.0 `hy` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[1]`. """ warnings.warn("hy has been deprecated, please access as mesh.h[1]", DeprecationWarning) return None if self.dim < 2 else self.h[1] @property def hz(self): """Width of cells in the z direction Returns ------- numpy.ndarray or None .. deprecated:: 0.5.0 `hz` will be removed in discretize 1.0.0 to reduce namespace clutter, please use `mesh.h[2]`. """ warnings.warn("hz has been deprecated, please access as mesh.h[2]", DeprecationWarning) return None if self.dim < 3 else self.h[2] vectorNx = deprecate_property("nodes_x", "vectorNx", removal_version="1.0.0") vectorNy = deprecate_property("nodes_y", "vectorNy", removal_version="1.0.0") vectorNz = deprecate_property("nodes_z", "vectorNz", removal_version="1.0.0") vectorCCx = deprecate_property("cell_centers_x", "vectorCCx", removal_version="1.0.0") vectorCCy = deprecate_property("cell_centers_y", "vectorCCy", removal_version="1.0.0") vectorCCz = deprecate_property("cell_centers_z", "vectorCCz", removal_version="1.0.0") getInterpolationMat = deprecate_method("get_interpolation_matrix", "getInterpolationMat", removal_version="1.0.0") isInside = deprecate_method("is_inside", "isInside", removal_version="1.0.0") getTensor = deprecate_method("get_tensor", "getTensor", removal_version="1.0.0")
class TensorMesh( BaseTensorMesh, BaseRectangularMesh, DiffOperators, InnerProducts, TensorMeshIO ): """ TensorMesh is a mesh class that deals with tensor product meshes. Any Mesh that has a constant width along the entire axis such that it can defined by a single width vector, called 'h'. .. plot:: :include-source: import discretize hx = np.array([1, 1, 1]) hy = np.array([1, 2]) hz = np.array([1, 1, 1, 1]) mesh = discretize.TensorMesh([hx, hy, hz]) mesh.plot_grid() Example of a padded tensor mesh using :func:`discretize.utils.unpack_widths`: .. plot:: :include-source: import discretize mesh = discretize.TensorMesh([ [(10, 10, -1.3), (10, 40), (10, 10, 1.3)], [(10, 10, -1.3), (10, 20)] ]) mesh.plot_grid() For a quick tensor mesh on a (10x12x15) unit cube .. code:: python import discretize mesh = discretize.TensorMesh([10, 12, 15]) """ _meshType = "TENSOR" _aliases = { **DiffOperators._aliases, **BaseRectangularMesh._aliases, **BaseTensorMesh._aliases, } def __init__(self, h=None, origin=None, **kwargs): BaseTensorMesh.__init__(self, h=h, origin=origin, **kwargs) def __repr__(self): """Plain text representation.""" fmt = "\n {}: {:,} cells\n\n".format(type(self).__name__, self.nC) fmt += 22 * " " + "MESH EXTENT" + 13 * " " + "CELL WIDTH FACTOR\n" fmt += " dir nC min max min max " fmt += " max\n --- --- " + 27 * "-" + " " + 18 * "-" + " ------\n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " {}".format(name) fmt += " {:6}".format(iattr["nC"]) for p in ["min", "max"]: fmt += " {:13,.2f}".format(iattr[p]) for p in ["h_min", "h_max"]: fmt += " {:9,.2f}".format(iattr[p]) fmt += "{:8,.2f}".format(iattr["max_fact"]) fmt += "\n" # End row fmt += "\n" return fmt def _repr_html_(self): """HTML representation.""" style = " style='padding: 5px 20px 5px 20px;'" fmt = "<table>\n" fmt += " <tr>\n" fmt += " <td style='font-weight: bold; font-size: 1.2em; text-align" fmt += ": center;' colspan='3'>{}</td>\n".format(type(self).__name__) fmt += " <td style='font-size: 1.2em; text-align: center;'" fmt += "colspan='4'>{:,} cells</td>\n".format(self.nC) fmt += " </tr>\n" fmt += " <tr>\n" fmt += " <th></th>\n" fmt += " <th></th>\n" fmt += " <th colspan='2'" + style + ">MESH EXTENT</th>\n" fmt += " <th colspan='2'" + style + ">CELL WIDTH</th>\n" fmt += " <th" + style + ">FACTOR</th>\n" fmt += " </tr>\n" fmt += " <tr>\n" fmt += " <th" + style + ">dir</th>\n" fmt += " <th" + style + ">nC</th>\n" fmt += " <th" + style + ">min</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " <th" + style + ">min</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " <th" + style + ">max</th>\n" fmt += " </tr>\n" # Get attributes and put into table. attrs = self._repr_attributes() for i in range(self.dim): name = attrs["names"][i] iattr = attrs[name] fmt += " <tr>\n" # Start row fmt += " <td" + style + ">{}</td>\n".format(name) fmt += " <td" + style + ">{}</td>\n".format(iattr["nC"]) for p in ["min", "max", "h_min", "h_max", "max_fact"]: fmt += " <td" + style + ">{:,.2f}</td>\n".format(iattr[p]) fmt += " </tr>\n" # End row fmt += "</table>\n" return fmt # --------------- Geometries --------------------- @property def cell_volumes(self): """Construct cell volumes of the 3D model as 1d array.""" if getattr(self, "_cell_volumes", None) is None: vh = self.h # Compute cell volumes if self.dim == 1: self._cell_volumes = mkvc(vh[0]) elif self.dim == 2: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(vh[0], vh[1])) elif self.dim == 3: # Cell sizes in each direction self._cell_volumes = mkvc(np.outer(mkvc(np.outer(vh[0], vh[1])), vh[2])) return self._cell_volumes @property def face_x_areas(self): """ Area of the x-faces """ if getattr(self, "_face_x_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: areaFx = np.ones(n[0] + 1) elif self.dim == 2: areaFx = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: areaFx = np.outer(np.ones(n[0] + 1), mkvc(np.outer(vh[1], vh[2]))) self._face_x_areas = mkvc(areaFx) return self._face_x_areas @property def face_y_areas(self): """ Area of the y-faces """ if getattr(self, "_face_y_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1: raise Exception("1D meshes do not have y-Faces") elif self.dim == 2: areaFy = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: areaFy = np.outer(vh[0], mkvc(np.outer(np.ones(n[1] + 1), vh[2]))) self._face_y_areas = mkvc(areaFy) return self._face_y_areas @property def face_z_areas(self): """ Area of the z-faces """ if getattr(self, "_face_z_areas", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute areas of cell faces if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have z-Faces".format(self.dim)) elif self.dim == 3: areaFz = np.outer(vh[0], mkvc(np.outer(vh[1], np.ones(n[2] + 1)))) self._face_z_areas = mkvc(areaFz) return self._face_z_areas @property def face_areas(self): """Construct face areas of the 3D model as 1d array.""" if self.dim == 1: return self.face_x_areas elif self.dim == 2: return np.r_[self.face_x_areas, self.face_y_areas] elif self.dim == 3: return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas] @property def edge_x_lengths(self): """x-edge lengths""" if getattr(self, "_edge_x_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: edgeEx = vh[0] elif self.dim == 2: edgeEx = np.outer(vh[0], np.ones(n[1] + 1)) elif self.dim == 3: edgeEx = np.outer( vh[0], mkvc(np.outer(np.ones(n[1] + 1), np.ones(n[2] + 1))) ) self._edge_x_lengths = mkvc(edgeEx) return self._edge_x_lengths @property def edge_y_lengths(self): """y-edge lengths""" if getattr(self, "_edge_y_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1: raise Exception("1D meshes do not have y-edges") elif self.dim == 2: edgeEy = np.outer(np.ones(n[0] + 1), vh[1]) elif self.dim == 3: edgeEy = np.outer( np.ones(n[0] + 1), mkvc(np.outer(vh[1], np.ones(n[2] + 1))) ) self._edge_y_lengths = mkvc(edgeEy) return self._edge_y_lengths @property def edge_z_lengths(self): """z-edge lengths""" if getattr(self, "_edge_z_lengths", None) is None: # Ensure that we are working with column vectors vh = self.h # The number of cell centers in each direction n = self.vnC # Compute edge lengths if self.dim == 1 or self.dim == 2: raise Exception("{}D meshes do not have y-edges".format(self.dim)) elif self.dim == 3: edgeEz = np.outer( np.ones(n[0] + 1), mkvc(np.outer(np.ones(n[1] + 1), vh[2])) ) self._edge_z_lengths = mkvc(edgeEz) return self._edge_z_lengths @property def edge_lengths(self): """Construct edge legnths of the 3D model as 1d array.""" if self.dim == 1: return self.edge_x_lengths elif self.dim == 2: return np.r_[self.edge_x_lengths, self.edge_y_lengths] elif self.dim == 3: return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths] return self._edge @property def face_boundary_indices(self): """ Find indices of boundary faces in each direction """ if self.dim == 1: indxd = self.gridFx == min(self.gridFx) indxu = self.gridFx == max(self.gridFx) return indxd, indxu elif self.dim == 2: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0]) indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0]) indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1]) indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1]) indzd = self.gridFz[:, 2] == min(self.gridFz[:, 2]) indzu = self.gridFz[:, 2] == max(self.gridFz[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu @property def cell_boundary_indices(self): """ Find indices of boundary faces in each direction """ if self.dim == 1: indxd = self.gridCC == min(self.gridCC) indxu = self.gridCC == max(self.gridCC) return indxd, indxu elif self.dim == 2: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) return indxd, indxu, indyd, indyu elif self.dim == 3: indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0]) indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0]) indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1]) indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1]) indzd = self.gridCC[:, 2] == min(self.gridCC[:, 2]) indzu = self.gridCC[:, 2] == max(self.gridCC[:, 2]) return indxd, indxu, indyd, indyu, indzd, indzu def _repr_attributes(self): """Attributes for the representation of the mesh.""" attrs = {} attrs["names"] = ["x", "y", "z"][: self.dim] # Loop over dimensions. for i in range(self.dim): name = attrs["names"][i] # Name of this dimension attrs[name] = {} # Get min/max node. n_vector = getattr(self, "nodes_" + name) attrs[name]["min"] = np.nanmin(n_vector) attrs[name]["max"] = np.nanmax(n_vector) # Get min/max cell width. h_vector = self.h[i] attrs[name]["h_min"] = np.nanmin(h_vector) attrs[name]["h_max"] = np.nanmax(h_vector) # Get max stretching factor. if len(h_vector) < 2: attrs[name]["max_fact"] = 1.0 else: attrs[name]["max_fact"] = np.nanmax( np.r_[h_vector[:-1] / h_vector[1:], h_vector[1:] / h_vector[:-1]] ) # Add number of cells. attrs[name]["nC"] = self.shape_cells[i] return attrs # DEPRECATIONS vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0") areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0") areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0") areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0") area = deprecate_property("face_areas", "area", removal_version="1.0.0") edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0") edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0") edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0") edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0") faceBoundaryInd = deprecate_property( "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0" ) cellBoundaryInd = deprecate_property( "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0" )