class TreeMeshIO(object): @classmethod def read_UBC(TreeMesh, meshFile, directory=""): """Read UBC 3D OcTree mesh file Input: :param str meshFile: path to the UBC GIF OcTree mesh file to read :rtype: discretize.TreeMesh :return: The octree mesh """ fname = os.path.join(directory, meshFile) fileLines = np.genfromtxt(fname, dtype=str, delimiter="\n", comments="!") nCunderMesh = np.array(fileLines[0].split("!")[0].split(), dtype=int) tswCorn = np.array(fileLines[1].split("!")[0].split(), dtype=float) smallCell = np.array(fileLines[2].split("!")[0].split(), dtype=float) # Read the index array indArr = np.genfromtxt( (line.encode("utf8") for line in fileLines[4::]), dtype=np.int) nCunderMesh = nCunderMesh[:len( tswCorn)] # remove information related to core hs = [np.ones(nr) * sz for nr, sz in zip(nCunderMesh, smallCell)] origin = tswCorn origin[-1] -= np.sum(hs[-1]) ls = np.log2(nCunderMesh).astype(int) # if all ls are equal if min(ls) == max(ls): max_level = ls[0] else: max_level = min(ls) + 1 mesh = TreeMesh(hs, origin=origin) levels = indArr[:, -1] indArr = indArr[:, :-1] indArr -= 1 # shift by 1.... indArr = 2 * indArr + levels[:, None] # get cell center index indArr[:, -1] = 2 * nCunderMesh[-1] - indArr[:, -1] # switch direction of iz levels = max_level - np.log2(levels) # calculate level mesh.__setstate__((indArr, levels)) return mesh def read_model_UBC(mesh, file_name): """Read UBC OcTree model and get vector :param string file_name: path to the UBC GIF model file to read :rtype: numpy.ndarray :return: OcTree model """ if type(file_name) is list: out = {} for f in file_name: out[f] = mesh.read_model_UBC(f) return out modArr = np.loadtxt(file_name) ubc_order = mesh._ubc_order # order_ubc will re-order from treemesh ordering to UBC ordering # need the opposite operation un_order = np.empty_like(ubc_order) un_order[ubc_order] = np.arange(len(ubc_order)) model = modArr[un_order].copy() # ensure a contiguous array return model def write_UBC(mesh, file_name, models=None, directory=""): """Write UBC ocTree mesh and model files from a octree mesh and model. :param string file_name: File to write to :param dict models: Models in a dict, where each key is the file_name :param str directory: directory where to save model(s) """ uniform_hs = np.array([np.allclose(h, h[0]) for h in mesh.h]) if np.any(~uniform_hs): raise Exception("UBC form does not support variable cell widths") nCunderMesh = np.array([h.size for h in mesh.h], dtype=np.int64) tswCorn = mesh.origin.copy() tswCorn[-1] += np.sum(mesh.h[-1]) smallCell = np.array([h[0] for h in mesh.h]) nrCells = mesh.nC indArr, levels = mesh._ubc_indArr ubc_order = mesh._ubc_order indArr = indArr[ubc_order] levels = levels[ubc_order] # Write the UBC octree mesh file head = " ".join([f"{int(n)}" for n in nCunderMesh]) + " \n" head += " ".join([f"{v:.4f}" for v in tswCorn]) + " \n" head += " ".join([f"{v:.3f}" for v in smallCell]) + " \n" head += f"{int(nrCells)}" np.savetxt(file_name, np.c_[indArr, levels], fmt="%i", header=head, comments="") # Print the models if models is None: return if not isinstance(models, dict): raise TypeError("models must be a dict") for key in models: if not isinstance(key, str): raise TypeError( "The dict key must be a string representing the file name") mesh.write_model_UBC(key, models[key], directory=directory) def write_model_UBC(mesh, file_name, model, directory=""): """Writes a model associated with a TreeMesh to a UBC-GIF format model file. Input: :param str file_name: File to write to or just its name if directory is specified :param str directory: directory where the UBC GIF file lives :param numpy.ndarray model: The model """ if type(file_name) is list: for f, m in zip(file_name, model): mesh.write_model_UBC(f, m) else: ubc_order = mesh._ubc_order fname = os.path.join(directory, file_name) m = model[ubc_order] np.savetxt(fname, m) # DEPRECATED @classmethod def readUBC(TreeMesh, file_name, directory=""): warnings.warn( "TensorMesh.readUBC has been deprecated and will be removed in" "discretize 1.0.0. please use TensorMesh.read_UBC", DeprecationWarning, ) return TreeMesh.read_UBC(file_name, directory) readModelUBC = deprecate_method("read_model_UBC", "readModelUBC", removal_version="1.0.0") writeUBC = deprecate_method("write_UBC", "writeUBC", removal_version="1.0.0") writeModelUBC = deprecate_method("write_model_UBC", "writeModelUBC", 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. 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 TensorMeshIO(InterfaceTensorread_vtk): @classmethod def _readUBC_3DMesh(TensorMesh, file_name): """Read UBC GIF 3D tensor mesh and generate same dimension TensorMesh. Input: :param string file_name: path to the UBC GIF mesh file Output: :rtype: TensorMesh :return: The tensor mesh for the file_name. """ # Interal function to read cell size lines for the UBC mesh files. def readCellLine(line): line_list = [] for seg in line.split(): if "*" in seg: sp = seg.split("*") seg_arr = np.ones((int(sp[0]), )) * float(sp[1]) else: seg_arr = np.array([float(seg)], float) line_list.append(seg_arr) return np.concatenate(line_list) # Read the file as line strings, remove lines with comment = ! msh = np.genfromtxt(file_name, delimiter="\n", dtype=np.str, comments="!") # Fist line is the size of the model sizeM = np.array(msh[0].split(), dtype=float) # Second line is the South-West-Top corner coordinates. origin = np.array(msh[1].split(), dtype=float) # Read the cell sizes h1 = readCellLine(msh[2]) h2 = readCellLine(msh[3]) h3temp = readCellLine(msh[4]) # Invert the indexing of the vector to start from the bottom. h3 = h3temp[::-1] # Adjust the reference point to the bottom south west corner origin[2] = origin[2] - np.sum(h3) # Make the mesh tensMsh = TensorMesh([h1, h2, h3], origin=origin) return tensMsh @classmethod def _readUBC_2DMesh(TensorMesh, file_name): """Read UBC GIF 2DTensor mesh and generate 2D Tensor mesh in simpeg Input: :param string file_name: path to the UBC GIF mesh file Output: :rtype: TensorMesh :return: SimPEG TensorMesh 2D object """ fopen = open(file_name, "r") # Read down the file and unpack dx vector def unpackdx(fid, nrows): for ii in range(nrows): line = fid.readline() var = np.array(line.split(), dtype=float) if ii == 0: x0 = var[0] xvec = np.ones(int(var[2])) * (var[1] - var[0]) / int( var[2]) xend = var[1] else: xvec = np.hstack( (xvec, np.ones(int(var[1])) * (var[0] - xend) / int(var[1]))) xend = var[0] return x0, xvec # Start with dx block # First line specifies the number of rows for x-cells line = fopen.readline() # Strip comments lines while line.startswith("!"): line = fopen.readline() nl = np.array(line.split(), dtype=int) [x0, dx] = unpackdx(fopen, nl[0]) # Move down the file until reaching the z-block line = fopen.readline() if not line: line = fopen.readline() # End with dz block # First line specifies the number of rows for z-cells line = fopen.readline() nl = np.array(line.split(), dtype=int) [z0, dz] = unpackdx(fopen, nl[0]) # Flip z0 to be the bottom of the mesh for SimPEG z0 = -(z0 + sum(dz)) dz = dz[::-1] # Make the mesh tensMsh = TensorMesh([dx, dz], origin=(x0, z0)) fopen.close() return tensMsh @classmethod def read_UBC(TensorMesh, file_name, directory=""): """Wrapper to Read UBC GIF 2D and 3D tensor mesh and generate same dimension TensorMesh. Input: :param str file_name: path to the UBC GIF mesh file or just its name if directory is specified :param str directory: directory where the UBC GIF file lives Output: :rtype: TensorMesh :return: The tensor mesh for the file_name. """ # Check the expected mesh dimensions fname = os.path.join(directory, file_name) # Read the file as line strings, remove lines with comment = ! msh = np.genfromtxt(fname, delimiter="\n", dtype=np.str, comments="!", max_rows=1) # Fist line is the size of the model sizeM = np.array(msh.ravel()[0].split(), dtype=float) # Check if the mesh is a UBC 2D mesh if sizeM.shape[0] == 1: Tnsmsh = TensorMesh._readUBC_2DMesh(fname) # Check if the mesh is a UBC 3D mesh elif sizeM.shape[0] == 3: Tnsmsh = TensorMesh._readUBC_3DMesh(fname) else: raise Exception("File format not recognized") return Tnsmsh def _readModelUBC_2D(mesh, file_name): """ Read UBC GIF 2DTensor model and generate 2D Tensor model in simpeg Input: :param string file_name: path to the UBC GIF 2D model file Output: :rtype: numpy.ndarray :return: model with TensorMesh ordered """ # Open fileand skip header... assume that we know the mesh already obsfile = np.genfromtxt(file_name, delimiter=" \n", dtype=np.str, comments="!") dim = tuple(np.array(obsfile[0].split(), dtype=int)) if mesh.shape_cells != dim: raise Exception("Dimension of the model and mesh mismatch") model = [] for line in obsfile[1:]: model.extend([float(val) for val in line.split()]) model = np.asarray(model) if not len(model) == mesh.nC: raise Exception("""Something is not right, expected size is {:d} but unwrap vector is size {:d}""".format(mesh.nC, len(model))) return model.reshape(mesh.vnC, order="F")[:, ::-1].reshape(-1, order="F") def _readModelUBC_3D(mesh, file_name): """Read UBC 3DTensor mesh model and generate 3D Tensor mesh model Input: :param string file_name: path to the UBC GIF mesh file to read Output: :rtype: numpy.ndarray :return: model with TensorMesh ordered """ f = open(file_name, "r") model = np.array(list(map(float, f.readlines()))) f.close() nCx, nCy, nCz = mesh.shape_cells model = np.reshape(model, (nCz, nCx, nCy), order="F") model = model[::-1, :, :] model = np.transpose(model, (1, 2, 0)) model = mkvc(model) return model def read_model_UBC(mesh, file_name, directory=""): """Read UBC 2D or 3D Tensor mesh model and generate Tensor mesh model Input: :param str file_name: path to the UBC GIF mesh file to read or just its name if directory is specified :param str directory: directory where the UBC GIF file lives Output: :rtype: numpy.ndarray :return: model with TensorMesh ordered """ fname = os.path.join(directory, file_name) if mesh.dim == 3: model = mesh._readModelUBC_3D(fname) elif mesh.dim == 2: model = mesh._readModelUBC_2D(fname) else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") return model def write_model_UBC(mesh, file_name, model, directory=""): """Writes a model associated with a TensorMesh to a UBC-GIF format model file. Input: :param str file_name: File to write to or just its name if directory is specified :param str directory: directory where the UBC GIF file lives :param numpy.ndarray model: The model """ fname = os.path.join(directory, file_name) if mesh.dim == 3: # Reshape model to a matrix modelMat = mesh.reshape(model, "CC", "CC", "M") # Transpose the axes modelMatT = modelMat.transpose((2, 0, 1)) # Flip z to positive down modelMatTR = mkvc(modelMatT[::-1, :, :]) np.savetxt(fname, modelMatTR.ravel()) elif mesh.dim == 2: modelMat = mesh.reshape(model, "CC", "CC", "M").T[::-1] f = open(fname, "w") f.write("{:d} {:d}\n".format(*mesh.shape_cells)) f.close() f = open(fname, "ab") np.savetxt(f, modelMat) f.close() else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") def _writeUBC_3DMesh(mesh, file_name, comment_lines=""): """Writes a TensorMesh to a UBC-GIF format mesh file. Input: :param string file_name: File to write to :param dict models: A dictionary of the models """ if not mesh.dim == 3: raise Exception("Mesh must be 3D") s = comment_lines s += "{0:d} {1:d} {2:d}\n".format(*tuple(mesh.vnC)) # Have to it in the same operation or use mesh.origin.copy(), # otherwise the mesh.origin is updated. origin = mesh.origin + np.array([0, 0, mesh.h[2].sum()]) nCx, nCy, nCz = mesh.shape_cells s += "{0:.6f} {1:.6f} {2:.6f}\n".format(*tuple(origin)) s += ("%.6f " * nCx + "\n") % tuple(mesh.h[0]) s += ("%.6f " * nCy + "\n") % tuple(mesh.h[1]) s += ("%.6f " * nCz + "\n") % tuple(mesh.h[2][::-1]) f = open(file_name, "w") f.write(s) f.close() def _writeUBC_2DMesh(mesh, file_name, comment_lines=""): """Writes a TensorMesh to a UBC-GIF format mesh file. Input: :param string file_name: File to write to :param dict models: A dictionary of the models """ if not mesh.dim == 2: raise Exception("Mesh must be 2D") def writeF(fx, outStr=""): # Init i = 0 origin = True x0 = fx[i] f = fx[i] number_segment = 0 auxStr = "" while True: i = i + 1 if i >= fx.size: break dx = -f + fx[i] f = fx[i] n = 1 for j in range(i + 1, fx.size): if -f + fx[j] == dx: n += 1 i += 1 f = fx[j] else: break number_segment += 1 if origin: auxStr += "{:.10f} {:.10f} {:d} \n".format(x0, f, n) origin = False else: auxStr += "{:.10f} {:d} \n".format(f, n) auxStr = "{:d}\n".format(number_segment) + auxStr outStr += auxStr return outStr # Grab face coordinates fx = mesh.nodes_x fz = -mesh.nodes_y[::-1] # Create the string outStr = comment_lines outStr = writeF(fx, outStr=outStr) outStr += "\n" outStr = writeF(fz, outStr=outStr) # Write file f = open(file_name, "w") f.write(outStr) f.close() def write_UBC(mesh, file_name, models=None, directory="", comment_lines=""): """Writes a TensorMesh to a UBC-GIF format mesh file. Input: :param str file_name: File to write to :param str directory: directory where to save model :param dict models: A dictionary of the models :param str comment_lines: comment lines preceded with '!' to add """ fname = os.path.join(directory, file_name) if mesh.dim == 3: mesh._writeUBC_3DMesh(fname, comment_lines=comment_lines) elif mesh.dim == 2: mesh._writeUBC_2DMesh(fname, comment_lines=comment_lines) else: raise Exception("mesh must be a Tensor Mesh 2D or 3D") if models is None: return if not isinstance(models, dict): raise TypeError("models must be a dict") for key in models: if not isinstance(key, str): raise TypeError( "The dict key must be a string representing the file name") mesh.write_model_UBC(key, models[key], directory=directory) # DEPRECATED @classmethod def readUBC(TensorMesh, file_name, directory=""): warnings.warn( "TensorMesh.readUBC has been deprecated and will be removed in" "discretize 1.0.0. please use TensorMesh.read_UBC", DeprecationWarning, ) return TensorMesh.read_UBC(file_name, directory) readModelUBC = deprecate_method("read_model_UBC", "readModelUBC", removal_version="1.0.0") writeUBC = deprecate_method("write_UBC", "writeUBC", removal_version="1.0.0") writeModelUBC = deprecate_method("write_model_UBC", "writeModelUBC", removal_version="1.0.0")
class TreeMeshIO(object): """Class for managing the input/output of tree meshes and models. The ``TreeMeshIO`` class contains a set of class methods specifically for the :class:`~discretize.TreeMesh` class. These include: - Read/write tree meshes to file - Read/write models defined on tree meshes """ @classmethod def read_UBC(TreeMesh, meshFile, directory=""): """Read 3D tree mesh (OcTree mesh) from UBC-GIF formatted file. Parameters ---------- file_name : str or file name full path to the UBC-GIF formatted mesh file or just its name if directory is specified directory : str, optional directory where the UBC-GIF file lives Returns ------- discretize.TreeMesh The tree mesh """ fname = os.path.join(directory, meshFile) fileLines = np.genfromtxt(fname, dtype=str, delimiter="\n", comments="!") nCunderMesh = np.array(fileLines[0].split("!")[0].split(), dtype=int) tswCorn = np.array(fileLines[1].split("!")[0].split(), dtype=float) smallCell = np.array(fileLines[2].split("!")[0].split(), dtype=float) # Read the index array indArr = np.genfromtxt( (line.encode("utf8") for line in fileLines[4::]), dtype=np.int) nCunderMesh = nCunderMesh[:len( tswCorn)] # remove information related to core hs = [np.ones(nr) * sz for nr, sz in zip(nCunderMesh, smallCell)] origin = tswCorn origin[-1] -= np.sum(hs[-1]) ls = np.log2(nCunderMesh).astype(int) # if all ls are equal if min(ls) == max(ls): max_level = ls[0] else: max_level = min(ls) + 1 mesh = TreeMesh(hs, origin=origin) levels = indArr[:, -1] indArr = indArr[:, :-1] indArr -= 1 # shift by 1.... indArr = 2 * indArr + levels[:, None] # get cell center index indArr[:, -1] = 2 * nCunderMesh[-1] - indArr[:, -1] # switch direction of iz levels = max_level - np.log2(levels) # calculate level mesh.__setstate__((indArr, levels)) return mesh def read_model_UBC(mesh, file_name): """Read UBC-GIF formatted file model file for 3D tree mesh (OcTree). Parameters ---------- file_name : str or list of str full path to the UBC-GIF formatted model file or just its name if directory is specified. It can also be a list of file_names. directory : str directory where the UBC-GIF file lives (optional) Returns ------- (n_cells) numpy.ndarray or dict of [str, (n_cells) numpy.ndarray] The model defined on the mesh. If **file_name** is a ``dict``, it is a dictionary of models indexed by the file names. """ if type(file_name) is list: out = {} for f in file_name: out[f] = mesh.read_model_UBC(f) return out modArr = np.loadtxt(file_name) ubc_order = mesh._ubc_order # order_ubc will re-order from treemesh ordering to UBC ordering # need the opposite operation un_order = np.empty_like(ubc_order) un_order[ubc_order] = np.arange(len(ubc_order)) model = modArr[un_order].copy() # ensure a contiguous array return model def write_UBC(mesh, file_name, models=None, directory=""): """Write OcTree mesh (and models) to UBC-GIF formatted files. Parameters ---------- file_name : str full path for the output mesh file or just its name if directory is specified models : dict of [str, (n_cells) numpy.ndarray], optional The dictionary key is a string representing the model's name. Each model is a 1D numpy array of size (n_cells). directory : str, optional output directory (optional) """ uniform_hs = np.array([np.allclose(h, h[0]) for h in mesh.h]) if np.any(~uniform_hs): raise Exception("UBC form does not support variable cell widths") nCunderMesh = np.array([h.size for h in mesh.h], dtype=np.int64) tswCorn = mesh.origin.copy() tswCorn[-1] += np.sum(mesh.h[-1]) smallCell = np.array([h[0] for h in mesh.h]) nrCells = mesh.nC indArr, levels = mesh._ubc_indArr ubc_order = mesh._ubc_order indArr = indArr[ubc_order] levels = levels[ubc_order] # Write the UBC octree mesh file head = " ".join([f"{int(n)}" for n in nCunderMesh]) + " \n" head += " ".join([f"{v:.4f}" for v in tswCorn]) + " \n" head += " ".join([f"{v:.3f}" for v in smallCell]) + " \n" head += f"{int(nrCells)}" np.savetxt(file_name, np.c_[indArr, levels], fmt="%i", header=head, comments="") # Print the models if models is None: return if not isinstance(models, dict): raise TypeError("models must be a dict") for key in models: if not isinstance(key, str): raise TypeError( "The dict key must be a string representing the file name") mesh.write_model_UBC(key, models[key], directory=directory) def write_model_UBC(mesh, file_name, model, directory=""): """Write 3D tree model (OcTree) to UBC-GIF formatted file. Parameters ---------- file_name : str full path for the output mesh file or just its name if directory is specified model : (n_cells) numpy.ndarray model values defined for each cell directory : str output directory (optional) """ if type(file_name) is list: for f, m in zip(file_name, model): mesh.write_model_UBC(f, m) else: ubc_order = mesh._ubc_order fname = os.path.join(directory, file_name) m = model[ubc_order] np.savetxt(fname, m) # DEPRECATED @classmethod def readUBC(TreeMesh, file_name, directory=""): """*readUBC* has been deprecated and replaced by *read_UBC*""" warnings.warn( "TensorMesh.readUBC has been deprecated and will be removed in" "discretize 1.0.0. please use TensorMesh.read_UBC", FutureWarning, ) return TreeMesh.read_UBC(file_name, directory) readModelUBC = deprecate_method("read_model_UBC", "readModelUBC", removal_version="1.0.0", future_warn=True) writeUBC = deprecate_method("write_UBC", "writeUBC", removal_version="1.0.0", future_warn=True) writeModelUBC = deprecate_method("write_model_UBC", "writeModelUBC", removal_version="1.0.0", future_warn=True)
class BaseRectangularMesh(BaseMesh): """ BaseRectangularMesh """ _aliases = { **BaseMesh._aliases, **{ "vnC": "shape_cells", "vnN": "shape_nodes", "vnEx": "shape_edges_x", "vnEy": "shape_edges_y", "vnEz": "shape_edges_z", "vnFx": "shape_faces_x", "vnFy": "shape_faces_y", "vnFz": "shape_faces_z", }, } def __init__(self, n=None, origin=None, **kwargs): BaseMesh.__init__(self, n=n, origin=origin, **kwargs) @property def shape_cells(self): """The number of cells in each direction Returns ------- tuple of ints Notes ----- Also accessible as `vnC`. """ return tuple(self._n) @property def shape_nodes(self): """Number of nodes in each direction Returns ------- tuple of int Notes ----- Also accessible as `vnN`. """ return tuple(x + 1 for x in self.shape_cells) @property def shape_edges_x(self): """Number of x-edges in each direction Returns ------- tuple of int (nx_cells, ny_nodes, nz_nodes) Notes ----- Also accessible as `vnEx`. """ return self.shape_cells[:1] + self.shape_nodes[1:] @property def shape_edges_y(self): """Number of y-edges in each direction Returns ------- tuple of int or None (nx_nodes, ny_cells, nz_nodes), None if dim < 2 Notes ----- Also accessible as `vnEy`. """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sn[0], sc[1]) + sn[2:] # conditionally added if dim == 3! @property def shape_edges_z(self): """Number of z-edges in each direction Returns ------- tuple of int or None (nx_nodes, ny_nodes, nz_cells), None if dim < 3 Notes ----- Also accessible as `vnEz`. """ if self.dim < 3: return None return self.shape_nodes[:2] + self.shape_cells[2:] @property def shape_faces_x(self): """Number of x-faces in each direction Returns ------- tuple of int (nx_nodes, ny_cells, nz_cells) Notes ----- Also accessible as `vnFx`. """ return self.shape_nodes[:1] + self.shape_cells[1:] @property def shape_faces_y(self): """Number of y-faces in each direction Returns ------- tuple of int or None (nx_cells, ny_nodes, nz_cells), None if dim < 2 Notes ----- Also accessible as `vnFy`. """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sc[0], sn[1]) + sc[2:] @property def shape_faces_z(self): """Number of z-faces in each direction Returns ------- tuple of int or None (nx_cells, ny_cells, nz_nodes), None if dim < 3 Notes ----- Also accessible as `vnFz`. """ if self.dim < 3: return None return self.shape_cells[:2] + self.shape_nodes[2:] ################################## # Redo the numbering so they are dependent of the shape tuples # these should all inherit the parent's docstrings ################################## @property def n_cells(self): return int(np.prod(self.shape_cells)) @property def n_nodes(self): return int(np.prod(self.shape_nodes)) @property def n_edges_x(self): return int(np.prod(self.shape_edges_x)) @property def n_edges_y(self): if self.dim < 2: return return int(np.prod(self.shape_edges_y)) @property def n_edges_z(self): if self.dim < 3: return return int(np.prod(self.shape_edges_z)) @property def n_faces_x(self): return int(np.prod(self.shape_faces_x)) @property def n_faces_y(self): if self.dim < 2: return return int(np.prod(self.shape_faces_y)) @property def n_faces_z(self): if self.dim < 3: return return int(np.prod(self.shape_faces_z)) def reshape(self, x, x_type="cell_centers", out_type="cell_centers", format="V", **kwargs): """A quick reshape command that will do the best it can at giving you what you want. For example, you have a face variable, and you want the x component of it reshaped to a 3D matrix. `reshape` can fulfil your dreams:: mesh.reshape(V, 'F', 'Fx', 'M') | | | | | | | { | | | How: 'M' or ['V'] for a matrix | | | (ndgrid style) or a vector (n x dim) | | | } | | { | | What you want: ['CC'], 'N', | | 'F', 'Fx', 'Fy', 'Fz', | | 'E', 'Ex', 'Ey', or 'Ez' | | } | { | What is it: ['CC'], 'N', | 'F', 'Fx', 'Fy', 'Fz', | 'E', 'Ex', 'Ey', or 'Ez' | } { The input: as a list or ndarray } For example:: # Separates each component of the Ex grid into 3 matrices Xex, Yex, Zex = r(mesh.gridEx, 'Ex', 'Ex', 'M') # Given an edge vector, return just the x edges as a vector XedgeVector = r(edgeVector, 'E', 'Ex', 'V') # Separates each component of the edgeVector into 3 vectors eX, eY, eZ = r(edgeVector, 'E', 'E', 'V') """ if "xType" in kwargs: warnings.warn( "The xType keyword argument has been deprecated, please use x_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) x_type = kwargs["xType"] if "outType" in kwargs: warnings.warn( "The outType keyword argument has been deprecated, please use out_type. " "This will be removed in discretize 1.0.0", DeprecationWarning, ) out_type = kwargs["outType"] x_type = self._parse_location_type(x_type) out_type = self._parse_location_type(out_type) allowed_x_type = [ "cell_centers", "nodes", "faces", "faces_x", "faces_y", "faces_z", "edges", "edges_x", "edges_y", "edges_z" ] if not (isinstance(x, list) or isinstance(x, np.ndarray)): raise Exception("x must be either a list or a ndarray") if x_type not in allowed_x_type: raise Exception("x_type must be either '" + "', '".join(allowed_x_type) + "'") if out_type not in allowed_x_type: raise Exception("out_type must be either '" + "', '".join(allowed_x_type) + "'") if format not in ["M", "V"]: raise Exception("format must be either 'M' or 'V'") if out_type[:len(x_type)] != x_type: raise Exception("You cannot change types when reshaping.") if x_type not in out_type: raise Exception("You cannot change type of components.") if isinstance(x, list): for i, xi in enumerate(x): if not isinstance(x, np.ndarray): raise Exception("x[{0:d}] must be a numpy array".format(i)) if xi.size != x[0].size: raise Exception( "Number of elements in list must not change.") x_array = np.ones((x.size, len(x))) # Unwrap it and put it in a np array for i, xi in enumerate(x): x_array[:, i] = mkvc(xi) x = x_array if not isinstance(x, np.ndarray): raise Exception("x must be a numpy array") x = x[:] # make a copy. x_type_is_FE_xyz = (len(x_type) > 1 and x_type[0] in ["f", "e"] and x_type[-1] in ["x", "y", "z"]) def outKernal(xx, nn): """Returns xx as either a matrix (shape == nn) or a vector.""" if format == "M": return xx.reshape(nn, order="F") elif format == "V": return mkvc(xx) def switchKernal(xx): """Switches over the different options.""" if x_type in ["cell_centers", "nodes"]: nn = self.shape_cells if x_type == "cell_centers" else self.shape_nodes if xx.size != np.prod(nn): raise Exception("Number of elements must not change.") return outKernal(xx, nn) elif x_type in ["faces", "edges"]: # This will only deal with components of fields, # not full 'F' or 'E' xx = mkvc(xx) # unwrap it in case it is a matrix if x_type == "faces": nn = (self.nFx, self.nFy, self.nFz)[:self.dim] else: nn = (self.nEx, self.nEy, self.nEz)[:self.dim] nn = np.r_[0, nn] nx = [0, 0, 0] nx[0] = self.shape_faces_x if x_type == "faces" else self.shape_edges_x nx[1] = self.shape_faces_y if x_type == "faces" else self.shape_edges_y nx[2] = self.shape_faces_z if x_type == "faces" else self.shape_edges_z for dim, dimName in enumerate(["x", "y", "z"]): if dimName in out_type: if self.dim <= dim: raise Exception( "Dimensions of mesh not great enough for " "{}_{}".format(x_type, dimName)) if xx.size != np.sum(nn): raise Exception("Vector is not the right size.") start = np.sum(nn[:dim + 1]) end = np.sum(nn[:dim + 2]) return outKernal(xx[start:end], nx[dim]) elif x_type_is_FE_xyz: # This will deal with partial components (x, y or z) # lying on edges or faces if "x" in x_type: nn = self.shape_faces_x if "f" in x_type else self.shape_edges_x elif "y" in x_type: nn = self.shape_faces_y if "f" in x_type else self.shape_edges_y elif "z" in x_type: nn = self.shape_faces_z if "f" in x_type else self.shape_edges_z if xx.size != np.prod(nn): raise Exception( f"Vector is not the right size. Expected {np.prod(nn)}, got {xx.size}" ) return outKernal(xx, nn) # Check if we are dealing with a vector quantity isVectorQuantity = len(x.shape) == 2 and x.shape[1] == self.dim if out_type in ["faces", "edges"]: if isVectorQuantity: raise Exception( "Not sure what to do with a vector vector quantity..") outTypeCopy = out_type out = () for ii, dirName in enumerate(["x", "y", "z"][:self.dim]): out_type = outTypeCopy + "_" + dirName out += (switchKernal(x), ) return out elif isVectorQuantity: out = () for ii in range(x.shape[1]): out += (switchKernal(x[:, ii]), ) return out else: return switchKernal(x) # DEPRECATED r = deprecate_method("reshape", "r", removal_version="1.0.0") @property def nCx(self): """Number of cells in the x direction Returns ------- int .. deprecated:: 0.5.0 `nCx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[0]` to reduce namespace clutter. """ warnings.warn( "nCx has been deprecated, please access as mesh.shape_cells[0]", DeprecationWarning, ) return self.shape_cells[0] @property def nCy(self): """Number of cells in the y direction Returns ------- int or None None if dim < 2 .. deprecated:: 0.5.0 `nCy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[1]` to reduce namespace clutter. """ warnings.warn( "nCy has been deprecated, please access as mesh.shape_cells[1]", DeprecationWarning, ) if self.dim < 2: return None return self.shape_cells[1] @property def nCz(self): """Number of cells in the z direction Returns ------- int or None None if dim < 3 .. deprecated:: 0.5.0 `nCz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[2]` to reduce namespace clutter. """ warnings.warn( "nCz has been deprecated, please access as mesh.shape_cells[2]", DeprecationWarning, ) if self.dim < 3: return None return self.shape_cells[2] @property def nNx(self): """Number of nodes in the x-direction Returns ------- int .. deprecated:: 0.5.0 `nNx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[0]` to reduce namespace clutter. """ warnings.warn( "nNx has been deprecated, please access as mesh.shape_nodes[0]", DeprecationWarning, ) return self.shape_nodes[0] @property def nNy(self): """Number of nodes in the y-direction Returns ------- int or None None if dim < 2 .. deprecated:: 0.5.0 `nNy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[1]` to reduce namespace clutter. """ warnings.warn( "nNy has been deprecated, please access as mesh.shape_nodes[1]", DeprecationWarning, ) if self.dim < 2: return None return self.shape_nodes[1] @property def nNz(self): """Number of nodes in the z-direction Returns ------- int or None None if dim < 3 .. deprecated:: 0.5.0 `nNz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[2]` to reduce namespace clutter. """ warnings.warn( "nNz has been deprecated, please access as mesh.shape_nodes[2]", DeprecationWarning, ) if self.dim < 3: return None return self.shape_nodes[2]
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 BaseRectangularMesh(BaseRegularMesh): """ Base rectangular mesh class for the ``discretize`` package. The ``BaseRectangularMesh`` class acts as an extension of the :class:`~discretize.base.BaseRegularMesh` classes with a regular structure. """ _aliases = { **BaseRegularMesh._aliases, **{ "vnN": "shape_nodes", "vnEx": "shape_edges_x", "vnEy": "shape_edges_y", "vnEz": "shape_edges_z", "vnFx": "shape_faces_x", "vnFy": "shape_faces_y", "vnFz": "shape_faces_z", }, } @property def shape_nodes(self): """Returns the number of nodes along each axis direction This property returns a tuple containing the number of nodes along each axis direction. The length of the tuple is equal to the dimension of the mesh; i.e. 1, 2 or 3. Returns ------- (dim) tuple of int Number of nodes along each axis direction Notes ----- Property also accessible as using the shorthand **vnN** """ return tuple(x + 1 for x in self.shape_cells) @property def shape_edges_x(self): """Number of x-edges along each axis direction This property returns a tuple containing the number of x-edges along each axis direction. The length of the tuple is equal to the dimension of the mesh; i.e. 1, 2 or 3. Returns ------- (dim) tuple of int Number of x-edges along each axis direction - *1D mesh:* `(n_cells_x)` - *2D mesh:* `(n_cells_x, n_nodes_y)` - *3D mesh:* `(n_cells_x, n_nodes_y, n_nodes_z)` Notes ----- Property also accessible as using the shorthand **vnEx** """ return self.shape_cells[:1] + self.shape_nodes[1:] @property def shape_edges_y(self): """Number of y-edges along each axis direction This property returns a tuple containing the number of y-edges along each axis direction. If `dim` is 1, there are no y-edges. Returns ------- None or (dim) tuple of int Number of y-edges along each axis direction - *1D mesh: None* - *2D mesh:* `(n_nodes_x, n_cells_y)` - *3D mesh:* `(n_nodes_x, n_cells_y, n_nodes_z)` Notes ----- Property also accessible as using the shorthand **vnEy** """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sn[0], sc[1]) + sn[2:] # conditionally added if dim == 3! @property def shape_edges_z(self): """Number of z-edges along each axis direction This property returns a tuple containing the number of z-edges along each axis direction. There are only z-edges if `dim` is 3. Returns ------- None or (dim) tuple of int Number of z-edges along each axis direction. - *1D mesh: None* - *2D mesh: None* - *3D mesh:* `(n_nodes_x, n_nodes_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnEz** """ if self.dim < 3: return None return self.shape_nodes[:2] + self.shape_cells[2:] @property def shape_faces_x(self): """Number of x-faces along each axis direction This property returns a tuple containing the number of x-faces along each axis direction. Returns ------- (dim) tuple of int Number of x-faces along each axis direction - *1D mesh:* `(n_nodes_x)` - *2D mesh:* `(n_nodes_x, n_cells_y)` - *3D mesh:* `(n_nodes_x, n_cells_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnFx** """ return self.shape_nodes[:1] + self.shape_cells[1:] @property def shape_faces_y(self): """Number of y-faces along each axis direction This property returns a tuple containing the number of y-faces along each axis direction. If `dim` is 1, there are no y-edges. Returns ------- None or (dim) tuple of int Number of y-faces along each axis direction - *1D mesh: None* - *2D mesh:* `(n_cells_x, n_nodes_y)` - *3D mesh:* `(n_cells_x, n_nodes_y, n_cells_z)` Notes ----- Property also accessible as using the shorthand **vnFy** """ if self.dim < 2: return None sc = self.shape_cells sn = self.shape_nodes return (sc[0], sn[1]) + sc[2:] @property def shape_faces_z(self): """Number of z-faces along each axis direction This property returns a tuple containing the number of z-faces along each axis direction. There are only z-faces if `dim` is 3. Returns ------- None or (dim) tuple of int Number of z-faces along each axis direction. - *1D mesh: None* - *2D mesh: None* - *3D mesh:* (n_cells_x, n_cells_y, n_nodes_z) Notes ----- Property also accessible as using the shorthand **vnFz** """ if self.dim < 3: return None return self.shape_cells[:2] + self.shape_nodes[2:] ################################## # Redo the numbering so they are dependent of the shape tuples # these should all inherit the parent's docstrings ################################## @property def n_cells(self): return int(np.prod(self.shape_cells)) @property def n_nodes(self): return int(np.prod(self.shape_nodes)) @property def n_edges_x(self): return int(np.prod(self.shape_edges_x)) @property def n_edges_y(self): if self.dim < 2: return return int(np.prod(self.shape_edges_y)) @property def n_edges_z(self): if self.dim < 3: return return int(np.prod(self.shape_edges_z)) @property def n_faces_x(self): return int(np.prod(self.shape_faces_x)) @property def n_faces_y(self): if self.dim < 2: return return int(np.prod(self.shape_faces_y)) @property def n_faces_z(self): if self.dim < 3: return return int(np.prod(self.shape_faces_z)) def reshape( self, x, x_type="cell_centers", out_type="cell_centers", format="V", **kwargs ): """General reshape method for tensor quantities **Reshape** is a quick command that will do its best to reshape discrete quantities living on meshes than inherit the :class:`discretize.base_mesh.RectangularMesh` class. For example, you may have a 1D array defining a vector on mesh faces, and you would like to extract the x-component and reshaped it to a 3D matrix. Parameters ---------- x : numpy.ndarray or list of numpy.ndarray The input quantity. , ndarray (tensor) or a list x_type : {'CC', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'} Defines the locations on the mesh where input parameter *x* lives. out_type : str Defines the output quantity. Choice depends on your input for *x_type*: - *x_type* = 'CC' ---> *out_type* = 'CC' - *x_type* = 'N' ---> *out_type* = 'N' - *x_type* = 'F' ---> *out_type* = {'F', 'Fx', 'Fy', 'Fz'} - *x_type* = 'E' ---> *out_type* = {'E', 'Ex', 'Ey', 'Ez'} format : str The dimensions of quantity being returned - *V:* return a vector (1D array) or a list of vectors - *M:* return matrix (nD array) or a list of matrices """ if "xType" in kwargs: warnings.warn( "The xType keyword argument has been deprecated, please use x_type. " "This will be removed in discretize 1.0.0", FutureWarning, ) x_type = kwargs["xType"] if "outType" in kwargs: warnings.warn( "The outType keyword argument has been deprecated, please use out_type. " "This will be removed in discretize 1.0.0", FutureWarning, ) out_type = kwargs["outType"] x_type = self._parse_location_type(x_type) out_type = self._parse_location_type(out_type) allowed_x_type = [ "cell_centers", "nodes", "faces", "faces_x", "faces_y", "faces_z", "edges", "edges_x", "edges_y", "edges_z", ] if not (isinstance(x, list) or isinstance(x, np.ndarray)): raise Exception("x must be either a list or a ndarray") if x_type not in allowed_x_type: raise Exception( "x_type must be either '" + "', '".join(allowed_x_type) + "'" ) if out_type not in allowed_x_type: raise Exception( "out_type must be either '" + "', '".join(allowed_x_type) + "'" ) if format not in ["M", "V"]: raise Exception("format must be either 'M' or 'V'") if out_type[: len(x_type)] != x_type: raise Exception("You cannot change types when reshaping.") if x_type not in out_type: raise Exception("You cannot change type of components.") if isinstance(x, list): for i, xi in enumerate(x): if not isinstance(x, np.ndarray): raise Exception("x[{0:d}] must be a numpy array".format(i)) if xi.size != x[0].size: raise Exception("Number of elements in list must not change.") x_array = np.ones((x.size, len(x))) # Unwrap it and put it in a np array for i, xi in enumerate(x): x_array[:, i] = mkvc(xi) x = x_array if not isinstance(x, np.ndarray): raise Exception("x must be a numpy array") x = x[:] # make a copy. x_type_is_FE_xyz = ( len(x_type) > 1 and x_type[0] in ["f", "e"] and x_type[-1] in ["x", "y", "z"] ) def outKernal(xx, nn): """Returns xx as either a matrix (shape == nn) or a vector.""" if format == "M": return xx.reshape(nn, order="F") elif format == "V": return mkvc(xx) def switchKernal(xx): """Switches over the different options.""" if x_type in ["cell_centers", "nodes"]: nn = self.shape_cells if x_type == "cell_centers" else self.shape_nodes if xx.size != np.prod(nn): raise Exception("Number of elements must not change.") return outKernal(xx, nn) elif x_type in ["faces", "edges"]: # This will only deal with components of fields, # not full 'F' or 'E' xx = mkvc(xx) # unwrap it in case it is a matrix if x_type == "faces": nn = (self.nFx, self.nFy, self.nFz)[: self.dim] else: nn = (self.nEx, self.nEy, self.nEz)[: self.dim] nn = np.r_[0, nn] nx = [0, 0, 0] nx[0] = self.shape_faces_x if x_type == "faces" else self.shape_edges_x nx[1] = self.shape_faces_y if x_type == "faces" else self.shape_edges_y nx[2] = self.shape_faces_z if x_type == "faces" else self.shape_edges_z for dim, dimName in enumerate(["x", "y", "z"]): if dimName in out_type: if self.dim <= dim: raise Exception( "Dimensions of mesh not great enough for " "{}_{}".format(x_type, dimName) ) if xx.size != np.sum(nn): raise Exception("Vector is not the right size.") start = np.sum(nn[: dim + 1]) end = np.sum(nn[: dim + 2]) return outKernal(xx[start:end], nx[dim]) elif x_type_is_FE_xyz: # This will deal with partial components (x, y or z) # lying on edges or faces if "x" in x_type: nn = self.shape_faces_x if "f" in x_type else self.shape_edges_x elif "y" in x_type: nn = self.shape_faces_y if "f" in x_type else self.shape_edges_y elif "z" in x_type: nn = self.shape_faces_z if "f" in x_type else self.shape_edges_z if xx.size != np.prod(nn): raise Exception( f"Vector is not the right size. Expected {np.prod(nn)}, got {xx.size}" ) return outKernal(xx, nn) # Check if we are dealing with a vector quantity isVectorQuantity = len(x.shape) == 2 and x.shape[1] == self.dim if out_type in ["faces", "edges"]: if isVectorQuantity: raise Exception("Not sure what to do with a vector vector quantity..") outTypeCopy = out_type out = () for ii, dirName in enumerate(["x", "y", "z"][: self.dim]): out_type = outTypeCopy + "_" + dirName out += (switchKernal(x),) return out elif isVectorQuantity: out = () for ii in range(x.shape[1]): out += (switchKernal(x[:, ii]),) return out else: return switchKernal(x) # DEPRECATED r = deprecate_method("reshape", "r", removal_version="1.0.0", future_warn=True) @property def nCx(self): """Number of cells in the x direction Returns ------- int .. deprecated:: 0.5.0 `nCx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[0]` to reduce namespace clutter. """ warnings.warn( "nCx has been deprecated, please access as mesh.shape_cells[0]", FutureWarning, ) return self.shape_cells[0] @property def nCy(self): """Number of cells in the y direction Returns ------- int or None None if dim < 2 .. deprecated:: 0.5.0 `nCy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[1]` to reduce namespace clutter. """ warnings.warn( "nCy has been deprecated, please access as mesh.shape_cells[1]", FutureWarning, ) if self.dim < 2: return None return self.shape_cells[1] @property def nCz(self): """Number of cells in the z direction Returns ------- int or None None if dim < 3 .. deprecated:: 0.5.0 `nCz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_cells[2]` to reduce namespace clutter. """ warnings.warn( "nCz has been deprecated, please access as mesh.shape_cells[2]", FutureWarning, ) if self.dim < 3: return None return self.shape_cells[2] @property def nNx(self): """Number of nodes in the x-direction Returns ------- int .. deprecated:: 0.5.0 `nNx` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[0]` to reduce namespace clutter. """ warnings.warn( "nNx has been deprecated, please access as mesh.shape_nodes[0]", FutureWarning, ) return self.shape_nodes[0] @property def nNy(self): """Number of nodes in the y-direction Returns ------- int or None None if dim < 2 .. deprecated:: 0.5.0 `nNy` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[1]` to reduce namespace clutter. """ warnings.warn( "nNy has been deprecated, please access as mesh.shape_nodes[1]", FutureWarning, ) if self.dim < 2: return None return self.shape_nodes[1] @property def nNz(self): """Number of nodes in the z-direction Returns ------- int or None None if dim < 3 .. deprecated:: 0.5.0 `nNz` will be removed in discretize 1.0.0, it is replaced by `mesh.shape_nodes[2]` to reduce namespace clutter. """ warnings.warn( "nNz has been deprecated, please access as mesh.shape_nodes[2]", FutureWarning, ) if self.dim < 3: return None return self.shape_nodes[2]