Exemplo n.º 1
0
class TreeMesh(_TreeMesh, BaseTensorMesh, InnerProducts, TreeMeshIO):
    """
    TreeMesh is a class for adaptive QuadTree (2D) and OcTree (3D) meshes.
    """

    _meshType = "TREE"
    _aliases = {
        **BaseTensorMesh._aliases,
        **DiffOperators._aliases,
        **{
            "ntN": "n_total_nodes",
            "ntEx": "n_total_edges_x",
            "ntEy": "n_total_edges_y",
            "ntEz": "n_total_edges_z",
            "ntE": "n_total_edges",
            "ntFx": "n_total_faces_x",
            "ntFy": "n_total_faces_y",
            "ntFz": "n_total_faces_z",
            "ntF": "n_total_faces",
            "nhN": "n_hanging_nodes",
            "nhEx": "n_hanging_edges_x",
            "nhEy": "n_hanging_edges_y",
            "nhEz": "n_hanging_edges_z",
            "nhE": "n_hanging_edges",
            "nhFx": "n_hanging_faces_x",
            "nhFy": "n_hanging_faces_y",
            "nhFz": "n_hanging_faces_z",
            "nhF": "n_hanging_faces",
            "gridhN": "hanging_nodes",
            "gridhFx": "hanging_faces_x",
            "gridhFy": "hanging_faces_y",
            "gridhFz": "hanging_faces_z",
            "gridhEx": "hanging_edges_x",
            "gridhEy": "hanging_edges_y",
            "gridhEz": "hanging_edges_z",
        },
    }

    # inheriting stuff from BaseTensorMesh that isn't defined in _QuadTree
    def __init__(self, h=None, origin=None, **kwargs):
        if "x0" in kwargs:
            origin = kwargs.pop("x0")
        BaseTensorMesh.__init__(
            self, h, origin
        )  # TODO:, **kwargs) # pass the kwargs for copy/paste

        nx = len(self.h[0])
        ny = len(self.h[1])
        nz = len(self.h[2]) if self.dim == 3 else 2

        def is_pow2(num):
            return ((num & (num - 1)) == 0) and num != 0

        if not (is_pow2(nx) and is_pow2(ny) and is_pow2(nz)):
            raise ValueError("length of cell width vectors must be a power of 2")
        # Now can initialize cpp tree parent
        _TreeMesh.__init__(self, self.h, self.origin)

        if "cell_levels" in kwargs.keys() and "cell_indexes" in kwargs.keys():
            inds = kwargs.pop("cell_indexes")
            levels = kwargs.pop("cell_levels")
            self.__setstate__((inds, levels))

    def __repr__(self):
        """Plain text representation."""
        mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad"))

        top = "\n" + mesh_name + ": {0:2.2f}% filled\n\n".format(self.fill * 100)

        # Number of cells per level
        level_count = self._count_cells_per_index()
        non_zero_levels = np.nonzero(level_count)[0]
        cell_display = ["Level : Number of cells"]
        cell_display.append("-----------------------")
        for level in non_zero_levels:
            cell_display.append("{:^5} : {:^15}".format(level, level_count[level]))
        cell_display.append("-----------------------")
        cell_display.append("Total : {:^15}".format(self.nC))

        extent_display = ["            Mesh Extent       "]
        extent_display.append("        min     ,     max     ")
        extent_display.append("   ---------------------------")
        dim_label = {0: "x", 1: "y", 2: "z"}
        for dim in range(self.dim):
            n_vector = getattr(self, "nodes_" + dim_label[dim])
            extent_display.append(
                "{}: {:^13},{:^13}".format(dim_label[dim], n_vector[0], n_vector[-1])
            )

        for i, line in enumerate(extent_display):
            if i == len(cell_display):
                cell_display.append(" " * (len(cell_display[0]) - 3 - len(line)))
            cell_display[i] += 3 * " " + line

        h_display = ["     Cell Widths    "]
        h_display.append("    min   ,   max   ")
        h_display.append("-" * (len(h_display[0])))
        h_gridded = self.h_gridded
        mins = np.min(h_gridded, axis=0)
        maxs = np.max(h_gridded, axis=0)
        for dim in range(self.dim):
            h_display.append("{:^10}, {:^10}".format(mins[dim], maxs[dim]))

        for i, line in enumerate(h_display):
            if i == len(cell_display):
                cell_display.append(" " * len(cell_display[0]))
            cell_display[i] += 3 * " " + line

        return top + "\n".join(cell_display)

    def _repr_html_(self):
        """html representation"""
        mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad"))
        level_count = self._count_cells_per_index()
        non_zero_levels = np.nonzero(level_count)[0]
        dim_label = {0: "x", 1: "y", 2: "z"}
        h_gridded = self.h_gridded
        mins = np.min(h_gridded, axis=0)
        maxs = np.max(h_gridded, axis=0)

        style = " style='padding: 5px 20px 5px 20px;'"
        # Cell level table:
        cel_tbl = "<table>\n"
        cel_tbl += "<tr>\n"
        cel_tbl += "<th" + style + ">Level</th>\n"
        cel_tbl += "<th" + style + ">Number of cells</th>\n"
        cel_tbl += "</tr>\n"
        for level in non_zero_levels:
            cel_tbl += "<tr>\n"
            cel_tbl += "<td" + style + ">{}</td>\n".format(level)
            cel_tbl += "<td" + style + ">{}</td>\n".format(level_count[level])
            cel_tbl += "</tr>\n"
        cel_tbl += "<tr>\n"
        cel_tbl += (
            "<td style='font-weight: bold; padding: 5px 20px 5px 20px;'> Total </td>\n"
        )
        cel_tbl += "<td" + style + "> {} </td>\n".format(self.nC)
        cel_tbl += "</tr>\n"
        cel_tbl += "</table>\n"

        det_tbl = "<table>\n"
        det_tbl += "<tr>\n"
        det_tbl += "<th></th>\n"
        det_tbl += "<th" + style + " colspan='2'>Mesh extent</th>\n"
        det_tbl += "<th" + style + " colspan='2'>Cell widths</th>\n"
        det_tbl += "</tr>\n"

        det_tbl += "<tr>\n"
        det_tbl += "<th></th>\n"
        det_tbl += "<th" + style + ">min</th>\n"
        det_tbl += "<th" + style + ">max</th>\n"
        det_tbl += "<th" + style + ">min</th>\n"
        det_tbl += "<th" + style + ">max</th>\n"
        det_tbl += "</tr>\n"
        for dim in range(self.dim):
            n_vector = getattr(self, "nodes_" + dim_label[dim])
            det_tbl += "<tr>\n"
            det_tbl += "<td" + style + ">{}</td>\n".format(dim_label[dim])
            det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[0])
            det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[-1])
            det_tbl += "<td" + style + ">{}</td>\n".format(mins[dim])
            det_tbl += "<td" + style + ">{}</td>\n".format(maxs[dim])
            det_tbl += "</tr>\n"
        det_tbl += "</table>\n"

        full_tbl = "<table>\n"
        full_tbl += "<tr>\n"
        full_tbl += "<td style='font-weight: bold; font-size: 1.2em; text-align: center;'>{}</td>\n".format(
            mesh_name
        )
        full_tbl += "<td style='font-size: 1.2em; text-align: center;' colspan='2'>{0:2.2f}% filled</td>\n".format(
            100 * self.fill
        )
        full_tbl += "</tr>\n"
        full_tbl += "<tr>\n"

        full_tbl += "<td>\n"
        full_tbl += cel_tbl
        full_tbl += "</td>\n"

        full_tbl += "<td>\n"
        full_tbl += det_tbl
        full_tbl += "</td>\n"

        full_tbl += "</tr>\n"
        full_tbl += "</table>\n"

        return full_tbl

    @properties.validator("origin")
    def _origin_validator(self, change):
        self._set_origin(change["value"])

    @property
    def vntF(self):
        """Total number of hanging and non-hanging faces in a [nx,ny,nz] form"""
        return [self.ntFx, self.ntFy] + ([] if self.dim == 2 else [self.ntFz])

    @property
    def vntE(self):
        """Total number of hanging and non-hanging edges in a [nx,ny,nz] form"""
        return [self.ntEx, self.ntEy] + ([] if self.dim == 2 else [self.ntEz])

    @property
    def stencil_cell_gradient(self):
        if getattr(self, "_stencil_cell_gradient", None) is None:

            self._stencil_cell_gradient = sp.vstack(
                [self.stencil_cell_gradient_x, self.stencil_cell_gradient_y]
            )
            if self.dim == 3:
                self._stencil_cell_gradient = sp.vstack(
                    [self._stencil_cell_gradient, self.stencil_cell_gradient_z]
                )

        return self._stencil_cell_gradient

    @property
    def cell_gradient(self):
        """
        Cell centered Gradient operator built off of the faceDiv operator.
        Grad =  - (Mf)^{-1} * Div * diag (volume)
        """
        if getattr(self, "_cell_gradient", None) is None:

            i_s = self.face_boundary_indices

            ix = np.ones(self.nFx)
            ix[i_s[0]] = 0.0
            ix[i_s[1]] = 0.0
            Pafx = sp.diags(ix)

            iy = np.ones(self.nFy)
            iy[i_s[2]] = 0.0
            iy[i_s[3]] = 0.0
            Pafy = sp.diags(iy)

            MfI = self.get_face_inner_product(invMat=True)

            if self.dim == 2:
                Pi = sp.block_diag([Pafx, Pafy])

            elif self.dim == 3:
                iz = np.ones(self.nFz)
                iz[i_s[4]] = 0.0
                iz[i_s[5]] = 0.0
                Pafz = sp.diags(iz)
                Pi = sp.block_diag([Pafx, Pafy, Pafz])

            self._cell_gradient = (
                -Pi * MfI * self.face_divergence.T * sp.diags(self.cell_volumes)
            )

        return self._cell_gradient

    @property
    def cell_gradient_x(self):
        """
        Cell centered Gradient operator in x-direction (Gradx)
        Grad = sp.vstack((Gradx, Grady, Gradz))
        """
        if getattr(self, "_cell_gradient_x", None) is None:

            nFx = self.nFx
            i_s = self.face_boundary_indices

            ix = np.ones(self.nFx)
            ix[i_s[0]] = 0.0
            ix[i_s[1]] = 0.0
            Pafx = sp.diags(ix)

            MfI = self.get_face_inner_product(invMat=True)
            MfIx = sp.diags(MfI.diagonal()[:nFx])

            self._cell_gradient_x = (
                -Pafx * MfIx * self.face_x_divergence.T * sp.diags(self.cell_volumes)
            )

        return self._cell_gradient_x

    @property
    def cell_gradient_y(self):
        """
        Cell centered Gradient operator in y-direction (Grady)
        Grad = sp.vstack((Gradx, Grady, Gradz))
        """
        if getattr(self, "_cell_gradient_y", None) is None:

            nFx = self.nFx
            nFy = self.nFy
            i_s = self.face_boundary_indices

            iy = np.ones(self.nFy)
            iy[i_s[2]] = 0.0
            iy[i_s[3]] = 0.0
            Pafy = sp.diags(iy)

            MfI = self.get_face_inner_product(invMat=True)
            MfIy = sp.diags(MfI.diagonal()[nFx : nFx + nFy])

            self._cell_gradient_y = (
                -Pafy * MfIy * self.face_y_divergence.T * sp.diags(self.cell_volumes)
            )

        return self._cell_gradient_y

    @property
    def cell_gradient_z(self):
        """
        Cell centered Gradient operator in z-direction (Gradz)
        Grad = sp.vstack((Gradx, Grady, Gradz))
        """
        if self.dim == 2:
            raise TypeError("z derivative not defined in 2D")
        if getattr(self, "_cell_gradient_z", None) is None:

            nFx = self.nFx
            nFy = self.nFy
            i_s = self.face_boundary_indices

            iz = np.ones(self.nFz)
            iz[i_s[4]] = 0.0
            iz[i_s[5]] = 0.0
            Pafz = sp.diags(iz)

            MfI = self.get_face_inner_product(invMat=True)
            MfIz = sp.diags(MfI.diagonal()[nFx + nFy :])

            self._cell_gradient_z = (
                -Pafz * MfIz * self.face_z_divergence.T * sp.diags(self.cell_volumes)
            )

        return self._cell_gradient_z

    @property
    def face_x_divergence(self):
        if getattr(self, "_face_x_divergence", None) is None:
            self._face_x_divergence = self.face_divergence[:, : self.nFx]
        return self._face_x_divergence

    @property
    def face_y_divergence(self):
        if getattr(self, "_face_y_divergence", None) is None:
            self._face_y_divergence = self.face_divergence[:, self.nFx : self.nFx + self.nFy]
        return self._face_y_divergence

    @property
    def face_z_divergence(self):
        if getattr(self, "_face_z_divergence", None) is None:
            self._face_z_divergence = self.face_divergence[:, self.nFx + self.nFy :]
        return self._face_z_divergence

    def point2index(self, locs):
        """Finds cells that contain the given points.
        Returns an array of index values of the cells that contain the given
        points

        Parameters
        ----------
        locs: array_like of shape (N, dim)
            points to search for the location of

        Returns
        -------
        numpy.array of integers of length(N)
            Cell indices that contain the points
        """
        locs = as_array_n_by_dim(locs, self.dim)
        inds = self._get_containing_cell_indexes(locs)
        return inds

    def cell_levels_by_index(self, indices):
        """Fast function to return a list of levels for the given cell indices

        Parameters
        ----------
        index: array_like of length (N)
            Cell indexes to query

        Returns
        -------
        numpy.array of length (N)
            Levels for the cells.
        """

        return self._cell_levels_by_indexes(indices)

    def get_interpolation_matrix(
        self, locs, location_type="CC", zeros_outside=False, **kwargs
    ):
        """Produces interpolation matrix

        Parameters
        ----------
        loc : numpy.ndarray
            Location of points to interpolate to

        location_type: str
            What to interpolate

            location_type can be::

                'Ex'    -> x-component of field defined on edges
                'Ey'    -> y-component of field defined on edges
                'Ez'    -> z-component of field defined on edges
                'Fx'    -> x-component of field defined on faces
                'Fy'    -> y-component of field defined on faces
                'Fz'    -> z-component of field defined on faces
                'N'     -> scalar field defined on nodes
                'CC'    -> scalar field defined on cell centers

        Returns
        -------
        scipy.sparse.csr_matrix
            M, the interpolation matrix

        """
        if "locType" in kwargs:
            warnings.warn(
                "The locType keyword argument has been deprecated, please use location_type. "
                "This will be removed in discretize 1.0.0",
                DeprecationWarning,
            )
            location_type = kwargs["locType"]
        if "zerosOutside" in kwargs:
            warnings.warn(
                "The zerosOutside keyword argument has been deprecated, please use zeros_outside. "
                "This will be removed in discretize 1.0.0",
                DeprecationWarning,
            )
            zeros_outside = kwargs["zerosOutside"]
        locs = as_array_n_by_dim(locs, self.dim)
        if location_type not in ["N", "CC", "Ex", "Ey", "Ez", "Fx", "Fy", "Fz"]:
            raise Exception("location_type must be one of N, CC, Ex, Ey, Ez, Fx, Fy, or Fz")

        if self.dim == 2 and location_type in ["Ez", "Fz"]:
            raise Exception("Unable to interpolate from Z edges/face in 2D")

        locs = np.require(np.atleast_2d(locs), dtype=np.float64, requirements="C")

        if location_type == "N":
            Av = self._getNodeIntMat(locs, zeros_outside)
        elif location_type in ["Ex", "Ey", "Ez"]:
            Av = self._getEdgeIntMat(locs, zeros_outside, location_type[1])
        elif location_type in ["Fx", "Fy", "Fz"]:
            Av = self._getFaceIntMat(locs, zeros_outside, location_type[1])
        elif location_type in ["CC"]:
            Av = self._getCellIntMat(locs, zeros_outside)
        return Av

    @property
    def permute_cells(self):
        """Permutation matrix re-ordering of cells sorted by x, then y, then z"""
        # TODO: cache these?
        P = np.lexsort(self.gridCC.T)  # sort by x, then y, then z
        return sp.identity(self.nC).tocsr()[P]

    @property
    def permute_faces(self):
        """Permutation matrix re-ordering of faces sorted by x, then y, then z"""
        # TODO: cache these?
        Px = np.lexsort(self.gridFx.T)
        Py = np.lexsort(self.gridFy.T) + self.nFx
        if self.dim == 2:
            P = np.r_[Px, Py]
        else:
            Pz = np.lexsort(self.gridFz.T) + (self.nFx + self.nFy)
            P = np.r_[Px, Py, Pz]
        return sp.identity(self.nF).tocsr()[P]

    @property
    def permute_edges(self):
        """Permutation matrix re-ordering of edges sorted by x, then y, then z"""
        # TODO: cache these?
        Px = np.lexsort(self.gridEx.T)
        Py = np.lexsort(self.gridEy.T) + self.nEx
        if self.dim == 2:
            P = np.r_[Px, Py]
        if self.dim == 3:
            Pz = np.lexsort(self.gridEz.T) + (self.nEx + self.nEy)
            P = np.r_[Px, Py, Pz]
        return sp.identity(self.nE).tocsr()[P]

    def serialize(self):
        serial = BaseTensorMesh.serialize(self)
        inds, levels = self.__getstate__()
        serial["cell_indexes"] = inds.tolist()
        serial["cell_levels"] = levels.tolist()
        return serial

    @classmethod
    def deserialize(cls, serial):
        mesh = cls(**serial)
        return mesh

    def __reduce__(self):
        return TreeMesh, (self.h, self.origin), self.__getstate__()

    cellGrad = deprecate_property("cell_gradient", "cellGrad", removal_version="1.0.0")
    cellGradx = deprecate_property(
        "cell_gradient_x", "cellGradx", removal_version="1.0.0"
    )
    cellGrady = deprecate_property(
        "cell_gradient_y", "cellGrady", removal_version="1.0.0"
    )
    cellGradz = deprecate_property(
        "cell_gradient_z", "cellGradz", removal_version="1.0.0"
    )
    cellGradStencil = deprecate_property(
        "cell_gradient_stencil", "cellGradStencil", removal_version="1.0.0"
    )
    nodalGrad = deprecate_property(
        "nodal_gradient", "nodalGrad", removal_version="1.0.0"
    )
    nodalLaplacian = deprecate_property(
        "nodal_laplacian", "nodalLaplacian", removal_version="1.0.0"
    )
    faceDiv = deprecate_property("face_divergence", "faceDiv", removal_version="1.0.0")
    faceDivx = deprecate_property(
        "face_x_divergence", "faceDivx", removal_version="1.0.0"
    )
    faceDivy = deprecate_property(
        "face_y_divergence", "faceDivy", removal_version="1.0.0"
    )
    faceDivz = deprecate_property(
        "face_z_divergence", "faceDivz", removal_version="1.0.0"
    )
    edgeCurl = deprecate_property("edge_curl", "edgeCurl", removal_version="1.0.0")
    maxLevel = deprecate_property("max_used_level", "maxLevel", removal_version="1.0.0")
    vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0")
    areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0")
    areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0")
    areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0")
    area = deprecate_property("face_areas", "area", removal_version="1.0.0")
    edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0")
    edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0")
    edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0")
    edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0")
    permuteCC = deprecate_property(
        "permute_cells", "permuteCC", removal_version="1.0.0"
    )
    permuteF = deprecate_property("permute_faces", "permuteF", removal_version="1.0.0")
    permuteE = deprecate_property("permute_edges", "permuteE", removal_version="1.0.0")
    faceBoundaryInd = deprecate_property(
        "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0"
    )
    cellBoundaryInd = deprecate_property(
        "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0"
    )
    _aveCC2FxStencil = deprecate_property(
        "average_cell_to_total_face_x", "_aveCC2FxStencil", removal_version="1.0.0"
    )
    _aveCC2FyStencil = deprecate_property(
        "average_cell_to_total_face_y", "_aveCC2FyStencil", removal_version="1.0.0"
    )
    _aveCC2FzStencil = deprecate_property(
        "average_cell_to_total_face_z", "_aveCC2FzStencil", removal_version="1.0.0"
    )
    _cellGradStencil = deprecate_property("stencil_cell_gradient", "_cellGradStencil", removal_version="1.0.0")
    _cellGradxStencil = deprecate_property("stencil_cell_gradient_x", "_cellGradxStencil", removal_version="1.0.0")
    _cellGradyStencil = deprecate_property("stencil_cell_gradient_y", "_cellGradyStencil", removal_version="1.0.0")
    _cellGradzStencil = deprecate_property("stencil_cell_gradient_z", "_cellGradzStencil", removal_version="1.0.0")
Exemplo n.º 2
0
class TensorMesh(
    BaseTensorMesh,
    BaseRectangularMesh,
    DiffOperators,
    InnerProducts,
    TensorMeshIO,
    InterfaceMixins,
):
    """
    Tensor mesh class.

    Tensor meshes are numerical grids whose cell centers, nodes, faces, edges, widths,
    volumes, etc... can be directly expressed as tensor products. The axes defining
    coordinates of the mesh are orthogonal. And cell properties along one axis do
    not vary with respect to the position along any other axis.

    Parameters
    ----------
    h : (dim) iterable of int, numpy.ndarray, or tuple
        Defines the cell widths along each axis. The length of the iterable object is
        equal to the dimension of the mesh (1, 2 or 3). For a 3D mesh, the list would
        have the form *[hx, hy, hz]* .

        Along each axis, the user has 3 choices for defining the cells widths:

        - :class:`int` -> A unit interval is equally discretized into `N` cells.
        - :class:`numpy.ndarray` -> The widths are explicity given for each cell
        - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])*
          where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional)
          is a padding factor denoting exponential increase/decrease in the cell width
          for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]*

    origin : (dim) iterable, default: 0
        Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost
        corner. By default, the mesh is anchored such that its origin is at *[0, 0, 0]* .

        For each dimension (x, y or z), The user may set the origin 2 ways:

        - a ``scalar`` which explicitly defines origin along that dimension.
        - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along
          each axis is the first node location ('0'), in the center ('C') or the last
          node location ('N') (see Examples).

    See Also
    --------
    utils.unpack_widths :
        The function used to expand a tuple to generate widths.

    Examples
    --------
    An example of a 2D tensor mesh is shown below. Here we use a list of tuple to
    define the discretization along the x-axis and a numpy array to define the
    discretization along the y-axis. We also use a string argument to center the
    x-axis about x = 0 and set the top of the mesh to y = 0.

    >>> from discretize import TensorMesh
    >>> import matplotlib.pyplot as plt

    >>> ncx = 10      # number of core mesh cells in x
    >>> dx = 5        # base cell width x
    >>> npad_x = 3    # number of padding cells in x
    >>> exp_x = 1.25  # expansion rate of padding cells in x
    >>> ncy = 24      # total number of mesh cells in y
    >>> dy = 5        # base cell width y

    >>> hx = [(dx, npad_x, -exp_x), (dx, ncx), (dx, npad_x, exp_x)]
    >>> hy = dy * np.ones(ncy)
    >>> mesh = TensorMesh([hx, hy], origin='CN')

    >>> fig = plt.figure(figsize=(5,5))
    >>> ax = fig.add_subplot(111)
    >>> mesh.plot_grid(ax=ax)
    >>> plt.show()
    """

    _meshType = "TENSOR"
    _aliases = {
        **DiffOperators._aliases,
        **BaseRectangularMesh._aliases,
        **BaseTensorMesh._aliases,
    }

    def __repr__(self):
        """Plain text representation."""
        fmt = "\n  {}: {:,} cells\n\n".format(type(self).__name__, self.nC)
        fmt += 22 * " " + "MESH EXTENT" + 13 * " " + "CELL WIDTH      FACTOR\n"
        fmt += "  dir    nC        min           max         min       max "
        fmt += "     max\n  ---   ---  " + 27 * "-" + "  " + 18 * "-" + "  ------\n"

        # Get attributes and put into table.
        attrs = self._repr_attributes()
        for i in range(self.dim):
            name = attrs["names"][i]
            iattr = attrs[name]
            fmt += "   {}".format(name)
            fmt += " {:6}".format(iattr["nC"])
            for p in ["min", "max"]:
                fmt += " {:13,.2f}".format(iattr[p])
            for p in ["h_min", "h_max"]:
                fmt += " {:9,.2f}".format(iattr[p])
            fmt += "{:8,.2f}".format(iattr["max_fact"])
            fmt += "\n"  # End row

        fmt += "\n"
        return fmt

    def _repr_html_(self):
        """HTML representation."""
        style = " style='padding: 5px 20px 5px 20px;'"

        fmt = "<table>\n"
        fmt += "  <tr>\n"
        fmt += "    <td style='font-weight: bold; font-size: 1.2em; text-align"
        fmt += ": center;' colspan='3'>{}</td>\n".format(type(self).__name__)
        fmt += "    <td style='font-size: 1.2em; text-align: center;'"
        fmt += "colspan='4'>{:,} cells</td>\n".format(self.nC)
        fmt += "  </tr>\n"

        fmt += "  <tr>\n"
        fmt += "    <th></th>\n"
        fmt += "    <th></th>\n"
        fmt += "    <th colspan='2'" + style + ">MESH EXTENT</th>\n"
        fmt += "    <th colspan='2'" + style + ">CELL WIDTH</th>\n"
        fmt += "    <th" + style + ">FACTOR</th>\n"
        fmt += "  </tr>\n"

        fmt += "  <tr>\n"
        fmt += "    <th" + style + ">dir</th>\n"
        fmt += "    <th" + style + ">nC</th>\n"
        fmt += "    <th" + style + ">min</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "    <th" + style + ">min</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "  </tr>\n"

        # Get attributes and put into table.
        attrs = self._repr_attributes()
        for i in range(self.dim):
            name = attrs["names"][i]
            iattr = attrs[name]
            fmt += "  <tr>\n"  # Start row
            fmt += "    <td" + style + ">{}</td>\n".format(name)
            fmt += "    <td" + style + ">{}</td>\n".format(iattr["nC"])
            for p in ["min", "max", "h_min", "h_max", "max_fact"]:
                fmt += "    <td" + style + ">{:,.2f}</td>\n".format(iattr[p])
            fmt += "  </tr>\n"  # End row

        fmt += "</table>\n"
        return fmt

    # --------------- Geometries ---------------------
    @property
    def cell_volumes(self):
        """Return cell volumes

        Calling this property will compute and return the volumes of the tensor
        mesh cells.

        Returns
        -------
        (n_cells) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* Returns the cell widths
            - *2D:* Returns the cell areas
            - *3D:* Returns the cell volumes

        """
        if getattr(self, "_cell_volumes", None) is None:
            vh = self.h
            # Compute cell volumes
            if self.dim == 1:
                self._cell_volumes = mkvc(vh[0])
            elif self.dim == 2:
                # Cell sizes in each direction
                self._cell_volumes = mkvc(np.outer(vh[0], vh[1]))
            elif self.dim == 3:
                # Cell sizes in each direction
                self._cell_volumes = mkvc(np.outer(mkvc(np.outer(vh[0], vh[1])), vh[2]))
        return self._cell_volumes

    @property
    def face_x_areas(self):
        """Returns the areas of the x-faces

        Calling this property will compute and return the areas of faces
        whose normal vector is along the x-axis.

        Returns
        -------
        (n_faces_x) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* Numpy array of ones whose length is equal to the number of nodes
            - *2D:* Areas of x-faces (equivalent to the lengths of y-edges)
            - *3D:* Areas of x-faces
        """
        if getattr(self, "_face_x_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1:
                areaFx = np.ones(n[0] + 1)
            elif self.dim == 2:
                areaFx = np.outer(np.ones(n[0] + 1), vh[1])
            elif self.dim == 3:
                areaFx = np.outer(np.ones(n[0] + 1), mkvc(np.outer(vh[1], vh[2])))
            self._face_x_areas = mkvc(areaFx)
        return self._face_x_areas

    @property
    def face_y_areas(self):
        """Returns the areas of the y-faces

        Calling this property will compute and return the areas of faces
        whose normal vector is along the y-axis. Note that only 2D and 3D
        tensor meshes have z-faces.

        Returns
        -------
        (n_faces_y) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* N/A since 1D meshes do not have y-faces
            - *2D:* Areas of y-faces (equivalent to the lengths of x-edges)
            - *3D:* Areas of y-faces
        """
        if getattr(self, "_face_y_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1:
                raise Exception("1D meshes do not have y-Faces")
            elif self.dim == 2:
                areaFy = np.outer(vh[0], np.ones(n[1] + 1))
            elif self.dim == 3:
                areaFy = np.outer(vh[0], mkvc(np.outer(np.ones(n[1] + 1), vh[2])))
            self._face_y_areas = mkvc(areaFy)
        return self._face_y_areas

    @property
    def face_z_areas(self):
        """Returns the areas of the z-faces

        Calling this property will compute and return the areas of faces
        whose normal vector is along the z-axis. Note that only 3D tensor
        meshes will have z-faces.

        Returns
        -------
        (n_faces_z) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* N/A since 1D meshes do not have z-faces
            - *2D:* N/A since 2D meshes do not have z-faces
            - *3D:* Areas of z-faces
        """
        if getattr(self, "_face_z_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1 or self.dim == 2:
                raise Exception("{}D meshes do not have z-Faces".format(self.dim))
            elif self.dim == 3:
                areaFz = np.outer(vh[0], mkvc(np.outer(vh[1], np.ones(n[2] + 1))))
            self._face_z_areas = mkvc(areaFz)
        return self._face_z_areas

    @property
    def face_areas(self):
        """Returns the areas of all faces in the mesh

        Calling this property will compute and return the areas of all
        faces as a 1D numpy array. The returned quantity is ordered x-face
        areas, then y-face areas, then z-face areas.

        Returns
        -------
        (n_faces) numpy.ndarray
            The length of the quantity returned depends on the dimensions of the mesh:

            - *1D:* returns the x-face areas
            - *2D:* returns the x-face and y-face areas in order; i.e. y-edge and x-edge lengths, respectively
            - *3D:* returns the x, y and z-face areas in order
        """
        if self.dim == 1:
            return self.face_x_areas
        elif self.dim == 2:
            return np.r_[self.face_x_areas, self.face_y_areas]
        elif self.dim == 3:
            return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas]

    @property
    def edge_x_lengths(self):
        """Returns the x-edge lengths

        Calling this property will compute and return the lengths of edges
        parallel to the x-axis.

        Returns
        -------
        (n_edges_x) numpy.ndarray
            X-edge lengths
        """
        if getattr(self, "_edge_x_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1:
                edgeEx = vh[0]
            elif self.dim == 2:
                edgeEx = np.outer(vh[0], np.ones(n[1] + 1))
            elif self.dim == 3:
                edgeEx = np.outer(
                    vh[0], mkvc(np.outer(np.ones(n[1] + 1), np.ones(n[2] + 1)))
                )
            self._edge_x_lengths = mkvc(edgeEx)
        return self._edge_x_lengths

    @property
    def edge_y_lengths(self):
        """Returns the y-edge lengths

        Calling this property will compute and return the lengths of edges
        parallel to the y-axis.

        Returns
        -------
        (n_edges_y) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* N/A since 1D meshes do not have y-edges
            - *2D:* Returns y-edge lengths
            - *3D:* Returns y-edge lengths
        """
        if getattr(self, "_edge_y_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1:
                raise Exception("1D meshes do not have y-edges")
            elif self.dim == 2:
                edgeEy = np.outer(np.ones(n[0] + 1), vh[1])
            elif self.dim == 3:
                edgeEy = np.outer(
                    np.ones(n[0] + 1), mkvc(np.outer(vh[1], np.ones(n[2] + 1)))
                )
            self._edge_y_lengths = mkvc(edgeEy)
        return self._edge_y_lengths

    @property
    def edge_z_lengths(self):
        """Returns the z-edge lengths

        Calling this property will compute and return the lengths of edges
        parallel to the z-axis.

        Returns
        -------
        (n_edges_z) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:

            - *1D:* N/A since 1D meshes do not have z-edges
            - *2D:* N/A since 2D meshes do not have z-edges
            - *3D:* Returns z-edge lengths
        """
        if getattr(self, "_edge_z_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1 or self.dim == 2:
                raise Exception("{}D meshes do not have y-edges".format(self.dim))
            elif self.dim == 3:
                edgeEz = np.outer(
                    np.ones(n[0] + 1), mkvc(np.outer(np.ones(n[1] + 1), vh[2]))
                )
            self._edge_z_lengths = mkvc(edgeEz)
        return self._edge_z_lengths

    @property
    def edge_lengths(self):
        """Returns the lengths of all edges in the mesh

        Calling this property will compute and return the lengths of all
        edges in the mesh. The returned quantity is ordered x-edge lengths,
        then y-edge lengths, then z-edge lengths.

        Returns
        -------
        (n_edges) numpy.ndarray
            The length of the quantity returned depends on the dimensions of the mesh:

            - *1D:* returns the x-edge lengths
            - *2D:* returns the x-edge and y-edge lengths in order
            - *3D:* returns the x, y and z-edge lengths in order
        """
        if self.dim == 1:
            return self.edge_x_lengths
        elif self.dim == 2:
            return np.r_[self.edge_x_lengths, self.edge_y_lengths]
        elif self.dim == 3:
            return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths]
        return self._edge

    @property
    def face_boundary_indices(self):
        """Returns the indices of the x, (y and z) boundary faces

        For x, (y and z) faces, this property returns the indices of the faces
        on the boundaries. That is, the property returns the indices of the x-faces
        that lie on the x-boundary; likewise for y and z. Note that each
        Cartesian direction will have both a lower and upper boundary,
        and the property will return the indices corresponding to the lower
        and upper boundaries separately.

        E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total).
        In this case, the return is a list of length 4 organized
        [ind_Bx1, ind_Bx2, ind_By1, ind_By2]::

                       By2
                + ------------- +
                |               |
                |               |
            Bx1 |               | Bx2
                |               |
                |               |
                + ------------- +
                       By1


        Returns
        -------
        (dim * 2) list of numpy.ndarray of bool
            The length of list returned depends on the dimension of the mesh.
            And the length of each array containing the indices depends on the
            number of faces in each direction. For 1D, 2D and 3D
            tensor meshes, the returns take the following form:

            - *1D:* returns [ind_Bx1, ind_Bx2]
            - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2]
            - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2]

        Examples
        --------
        Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices
        of the x and y-boundary faces. In this case there are 3 x-faces on each
        x-boundary, and there are 4 y-faces on each y-boundary.

        >>> from discretize import TensorMesh
        >>> import numpy as np
        >>> import matplotlib.pyplot as plt

        >>> hx = [1, 1, 1, 1]
        >>> hy = [2, 2, 2]
        >>> mesh = TensorMesh([hx, hy])
        >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.face_boundary_indices

        >>> ax = plt.subplot(111)
        >>> mesh.plot_grid(ax=ax)
        >>> ax.scatter(*mesh.faces_x[ind_Bx1].T)
        >>> plt.show()
        """
        if self.dim == 1:
            indxd = self.gridFx == min(self.gridFx)
            indxu = self.gridFx == max(self.gridFx)
            return indxd, indxu
        elif self.dim == 2:
            indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0])
            indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0])
            indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1])
            indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1])
            return indxd, indxu, indyd, indyu
        elif self.dim == 3:
            indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0])
            indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0])
            indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1])
            indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1])
            indzd = self.gridFz[:, 2] == min(self.gridFz[:, 2])
            indzu = self.gridFz[:, 2] == max(self.gridFz[:, 2])
            return indxd, indxu, indyd, indyu, indzd, indzu

    @property
    def cell_boundary_indices(self):
        """Returns the indices of the x, (y and z) boundary cells

        This property returns the indices of the cells on the x, (y and z)
        boundaries, respectively. Note that each axis direction will
        have both a lower and upper boundary. The property will
        return the indices corresponding to the lower and upper
        boundaries separately.

        E.g. for a 2D domain, there are 2 x-boundaries and 2 y-boundaries (4 in total).
        In this case, the return is a list of length 4 organized
        [ind_Bx1, ind_Bx2, ind_By1, ind_By2]::

                       By2
                + ------------- +
                |               |
                |               |
            Bx1 |               | Bx2
                |               |
                |               |
                + ------------- +
                       By1


        Returns
        -------
        (2 * dim) list of numpy.ndarray of bool
            The length of list returned depends on the dimension of the mesh (= 2 x dim).
            And the length of each array containing the indices is equal to
            the number of cells in the mesh. For 1D, 2D and 3D
            tensor meshes, the returns take the following form:

            - *1D:* returns [ind_Bx1, ind_Bx2]
            - *2D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2]
            - *3D:* returns [ind_Bx1, ind_Bx2, ind_By1, ind_By2, ind_Bz1, ind_Bz2]

        Examples
        --------
        Here, we construct a 4 by 3 cell 2D tensor mesh and return the indices
        of the x and y-boundary cells. In this case there are 3 cells touching
        each x-boundary, and there are 4 cells touching each y-boundary.

        >>> from discretize import TensorMesh
        >>> import numpy as np
        >>> import matplotlib.pyplot as plt

        >>> hx = [1, 1, 1, 1]
        >>> hy = [2, 2, 2]
        >>> mesh = TensorMesh([hx, hy])
        >>> ind_Bx1, ind_Bx2, ind_By1, ind_By2 = mesh.cell_boundary_indices

        >>> ax = plt.subplot(111)
        >>> mesh.plot_grid(ax=ax)
        >>> ax.scatter(*mesh.cell_centers[ind_Bx1].T)
        >>> plt.show()
        """
        if self.dim == 1:
            indxd = self.gridCC == min(self.gridCC)
            indxu = self.gridCC == max(self.gridCC)
            return indxd, indxu
        elif self.dim == 2:
            indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0])
            indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0])
            indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1])
            indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1])
            return indxd, indxu, indyd, indyu
        elif self.dim == 3:
            indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0])
            indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0])
            indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1])
            indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1])
            indzd = self.gridCC[:, 2] == min(self.gridCC[:, 2])
            indzu = self.gridCC[:, 2] == max(self.gridCC[:, 2])
            return indxd, indxu, indyd, indyu, indzd, indzu

    def _repr_attributes(self):
        """Attributes for the representation of the mesh."""

        attrs = {}
        attrs["names"] = ["x", "y", "z"][: self.dim]

        # Loop over dimensions.
        for i in range(self.dim):
            name = attrs["names"][i]  # Name of this dimension
            attrs[name] = {}

            # Get min/max node.
            n_vector = getattr(self, "nodes_" + name)
            attrs[name]["min"] = np.nanmin(n_vector)
            attrs[name]["max"] = np.nanmax(n_vector)

            # Get min/max cell width.
            h_vector = self.h[i]
            attrs[name]["h_min"] = np.nanmin(h_vector)
            attrs[name]["h_max"] = np.nanmax(h_vector)

            # Get max stretching factor.
            if len(h_vector) < 2:
                attrs[name]["max_fact"] = 1.0
            else:
                attrs[name]["max_fact"] = np.nanmax(
                    np.r_[h_vector[:-1] / h_vector[1:], h_vector[1:] / h_vector[:-1]]
                )

            # Add number of cells.
            attrs[name]["nC"] = self.shape_cells[i]

        return attrs

    # DEPRECATIONS
    vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", future_warn=False)
    areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0", future_warn=False)
    areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0", future_warn=False)
    areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0", future_warn=False)
    area = deprecate_property("face_areas", "area", removal_version="1.0.0", future_warn=False)
    edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0", future_warn=False)
    edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0", future_warn=False)
    edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0", future_warn=False)
    edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0", future_warn=False)
    faceBoundaryInd = deprecate_property(
        "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0", future_warn=False
    )
    cellBoundaryInd = deprecate_property(
        "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0", future_warn=False
    )
Exemplo n.º 3
0
class CurvilinearMesh(
    BaseRectangularMesh, DiffOperators, InnerProducts, InterfaceMixins
):
    """Curvilinear mesh class.

    Curvilinear meshes are numerical grids whose cells are general quadrilaterals (2D)
    or cuboid (3D); unlike tensor meshes (see :class:`~discretize.TensorMesh`) whose
    cells are rectangles or rectangular prisms. That being said, the combinatorial
    structure (i.e. connectivity of mesh cells) of curvilinear meshes is the same as
    tensor meshes.

    Parameters
    ----------
    node_list : list of array_like
        List :class:`array_like` containing the gridded x, y (and z) node locations.

        - For a 2D curvilinear mesh, *node_list* = [X, Y] where X and Y have shape
          (``n_nodes_x``, ``n_nodes_y``)
        - For a 3D curvilinear mesh, *node_list* = [X, Y, Z] where X, Y and Z have shape
          (``n_nodes_x``, ``n_nodes_y``, ``n_nodes_z``)


    Examples
    --------
    Using the :py:func:`~discretize.utils.example_curvilinear_grid` utility,
    we provide an example of a curvilinear mesh.

    >>> from discretize import CurvilinearMesh
    >>> from discretize.utils import example_curvilinear_grid
    >>> import matplotlib.pyplot as plt

    The example grid slightly rotates the nodes in the center of the mesh,

    >>> x, y = example_curvilinear_grid([10, 10], "rotate")
    >>> x.shape
    (11, 11)
    >>> y.shape
    (11, 11)
    >>> curvilinear_mesh = CurvilinearMesh([x, y])
    >>> curvilinear_mesh.shape_nodes
    (11, 11)

    >>> fig = plt.figure(figsize=(5,5))
    >>> ax = fig.add_subplot(111)
    >>> curvilinear_mesh.plot_grid(ax=ax)
    >>> plt.show()
    """

    _meshType = "Curv"
    _aliases = {
        **DiffOperators._aliases,
        **BaseRectangularMesh._aliases,
        **{
            "gridCC": "cell_centers",
            "gridN": "nodes",
            "gridFx": "faces_x",
            "gridFy": "faces_y",
            "gridFz": "faces_z",
            "gridEx": "edges_x",
            "gridEy": "edges_y",
            "gridEz": "edges_z",
        },
    }
    _items = {"node_list"}

    def __init__(self, node_list, **kwargs):

        if "nodes" in kwargs:
            node_list = kwargs.pop("nodes")

        node_list = tuple(np.asarray(item, dtype=np.float64) for item in node_list)
        # check shapes of each node array match
        dim = len(node_list)
        if dim not in [2, 3]:
            raise ValueError(
                f"Only supports 2 and 3 dimensional meshes, saw a node_list of length {dim}"
            )
        for i, nodes in enumerate(node_list):
            if len(nodes.shape) != dim:
                raise ValueError(
                    f"Unexpected shape of item in node list, expect array with {dim} dimensions, got {len(nodes.shape)}"
                )
            if node_list[0].shape != nodes.shape:
                raise ValueError(
                    f"The shape of nodes are not consistent, saw {node_list[0].shape} and {nodes.shape}"
                )
        self._node_list = tuple(node_list)

        # Save nodes to private variable _nodes as vectors
        self._nodes = np.ones((self.node_list[0].size, dim))
        for i, nodes in enumerate(self.node_list):
            self._nodes[:, i] = mkvc(nodes)

        shape_cells = (n - 1 for n in self.node_list[0].shape)

        # absorb the rest of kwargs, and do not pass to super
        super().__init__(shape_cells, origin=self.nodes[0])

    @property
    def node_list(self):
        """Returns the gridded x, y (and z) node locations used to create the mesh.

        Returns
        -------
        (dim) list of numpy.ndarray
            Gridded x, y (and z) node locations used to create the mesh.

                - *2D:* return is a list [X, Y] where X and Y have shape (n_nodes_x, n_nodes_y)
                - *3D:* return is a list [X, Y, Z] where X, Y and Z have shape (n_nodes_x, n_nodes_y, n_nodes_z)

        """
        return self._node_list

    @classmethod
    def deserialize(cls, value, **kwargs):
        if "nodes" in value:
            value["node_list"] = value.pop("nodes")
        return super().deserialize(value, **kwargs)

    @property
    def cell_centers(self):
        """Gridded cell center locations (staggered grid)

        For 2D or 3D curvilinear meshes, this property returns a numpy array
        of shape (n_cells, dim) containing the gridded cell center locations.
        The bottom-front-leftmost cell is the first cell.

        Returns
        -------
        (n_cells, dim) numpy.ndarray of float
            The shape of the output array is the number of cells by the dimension.

        """
        if getattr(self, "_cell_centers", None) is None:
            self._cell_centers = np.concatenate(
                [self.aveN2CC * self.gridN[:, i] for i in range(self.dim)]
            ).reshape((-1, self.dim), order="F")
        return self._cell_centers

    @property
    def nodes(self):
        """Gridded node locations (staggered grid)

        For 2D or 3D curvilinear meshes, this property returns a numpy array
        of shape (n_nodes, dim) containing the gridded node locations.
        The bottom-front-leftmost node is the first node.

        Returns
        -------
        (n_nodes, dim) numpy.ndarray of float
            Gridded node locations
        """
        if getattr(self, "_nodes", None) is None:
            raise Exception("Someone deleted this. I blame you.")
        return self._nodes

    @property
    def faces_x(self):
        """Gridded x-face locations (staggered grid)

        This property returns a numpy array of shape (n_faces_x, dim)
        containing gridded locations for all x-faces in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the x-faces are faces whose normal
        vectors are primarily along the x-direction. For highly irregular
        meshes however, this is not the case; see the examples below.

        Returns
        -------
        (n_faces_x, dim) numpy.ndarray of float
            Gridded x-face locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the x-faces have normal vectors that are
        primarily along the x-direction.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> x_faces = mesh1.faces_x

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r')
        >>> ax1.legend(['Mesh', 'X-faces'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the x-faces are not defined by normal vectors along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> x_faces = mesh2.faces_x

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r')
        >>> ax2.legend(['Mesh', 'X-faces'], fontsize=16)
        >>> plt.plot()
        """

        if getattr(self, "_faces_x", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N]
                self._faces_x = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [
                    mkvc(
                        0.25
                        * (
                            n[:, :-1, :-1]
                            + n[:, :-1, 1:]
                            + n[:, 1:, :-1]
                            + n[:, 1:, 1:]
                        )
                    )
                    for n in N
                ]
                self._faces_x = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_x

    @property
    def faces_y(self):
        """Gridded y-face locations (staggered grid)

        This property returns a numpy array of shape (n_faces_y, dim)
        containing gridded locations for all y-faces in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the y-faces are faces whose normal
        vectors are primarily along the y-direction. For highly irregular
        meshes however, this is not the case; see the examples below.

        Returns
        -------
        (n_faces_y, dim) numpy.ndarray of float
            Gridded y-face locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the y-faces have normal vectors that are
        primarily along the x-direction.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> y_faces = mesh1.faces_y

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r')
        >>> ax1.legend(['Mesh', 'Y-faces'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the y-faces are not defined by normal vectors along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> y_faces = mesh2.faces_y

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'r')
        >>> ax2.legend(['Mesh', 'Y-faces'], fontsize=16)
        >>> plt.plot()
        """

        if getattr(self, "_faces_y", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N]
                self._faces_y = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [
                    mkvc(
                        0.25
                        * (
                            n[:-1, :, :-1]
                            + n[:-1, :, 1:]
                            + n[1:, :, :-1]
                            + n[1:, :, 1:]
                        )
                    )
                    for n in N
                ]
                self._faces_y = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_y

    @property
    def faces_z(self):
        """Gridded z-face locations (staggered grid)

        This property returns a numpy array of shape (n_faces_z, dim)
        containing gridded locations for all z-faces in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the z-faces are faces whose normal
        vectors are primarily along the z-direction. For highly irregular
        meshes however, this is not the case.

        Returns
        -------
        (n_faces_z, dim) numpy.ndarray of float
            Gridded z-face locations (staggered grid)
        """

        if getattr(self, "_faces_z", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            XYZ = [
                mkvc(
                    0.25
                    * (n[:-1, :-1, :] + n[:-1, 1:, :] + n[1:, :-1, :] + n[1:, 1:, :])
                )
                for n in N
            ]
            self._faces_z = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_z

    @property
    def faces(self):
        """Gridded face locations (staggered grid)

        This property returns a numpy array of shape (n_faces, dim)
        containing gridded locations for all faces in the
        mesh (staggered grid). This is equivalent to calling
        np.r_[faces_x, faces_y, faces_z].

        Returns
        -------
        (n_faces, dim) numpy.ndarray of float
            Gridded face locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the x and y-faces have normal vectors that are
        primarily along the x and y-directions, respectively.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> faces = mesh1.faces
        >>> x_faces = faces[:mesh1.n_faces_x]
        >>> y_faces = faces[mesh1.n_faces_x:]

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r')
        >>> ax1.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g')
        >>> ax1.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the y-faces are not defined by normal vectors along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> faces = mesh2.faces
        >>> x_faces = faces[:mesh2.n_faces_x]
        >>> y_faces = faces[mesh2.n_faces_x:]

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(x_faces[:, 0], x_faces[:, 1], 30, 'r')
        >>> ax2.scatter(y_faces[:, 0], y_faces[:, 1], 30, 'g')
        >>> ax2.legend(['Mesh', 'X-faces', 'Y-faces'], fontsize=16)
        >>> plt.plot()
        """
        faces = np.r_[self.faces_x, self.faces_y]
        if self.dim > 2:
            faces = np.r_[faces, self.faces_z]
        return faces

    @property
    def edges_x(self):
        """Gridded x-edge locations (staggered grid)

        This property returns a numpy array of shape (n_edges_x, dim)
        containing gridded locations for all x-edges in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the x-edges are edges oriented
        primarily along the x-direction. For highly irregular
        meshes however, this is not the case; see the examples below.

        Returns
        -------
        (n_edges_x, dim) numpy.ndarray of float
            Gridded x-edge locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the x-edges are primarily oriented along the x-direction.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> x_edges = mesh1.edges_x

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r')
        >>> ax1.legend(['Mesh', 'X-edges'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the x-edges are not aligned primarily along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> x_edges = mesh2.edges_x

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r')
        >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16)
        >>> plt.plot()
        """
        if getattr(self, "_edges_x", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N]
                self._edges_x = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [mkvc(0.5 * (n[:-1, :, :] + n[1:, :, :])) for n in N]
                self._edges_x = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_x

    @property
    def edges_y(self):
        """Gridded y-edge locations (staggered grid)

        This property returns a numpy array of shape (n_edges_y, dim)
        containing gridded locations for all y-edges in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the y-edges are edges oriented
        primarily along the y-direction. For highly irregular
        meshes however, this is not the case; see the examples below.

        Returns
        -------
        (n_edges_y, dim) numpy.ndarray of float
            Gridded y-edge locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the y-edges are primarily oriented along the y-direction.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> y_edges = mesh1.edges_y

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r')
        >>> ax1.legend(['Mesh', 'Y-edges'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the y-edges are not aligned primarily along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> y_edges = mesh2.edges_y

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'r')
        >>> ax2.legend(['Mesh', 'X-edges'], fontsize=16)
        >>> plt.plot()
        """
        if getattr(self, "_edges_y", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N]
                self._edges_y = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [mkvc(0.5 * (n[:, :-1, :] + n[:, 1:, :])) for n in N]
                self._edges_y = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_y

    @property
    def edges_z(self):
        """Gridded z-edge locations (staggered grid)

        This property returns a numpy array of shape (n_edges_z, dim)
        containing gridded locations for all z-edges in the
        mesh (staggered grid). For curvilinear meshes whose structure
        is minimally staggered, the z-edges are faces whose normal
        vectors are primarily along the z-direction. For highly irregular
        meshes however, this is not the case.

        Returns
        -------
        (n_edges_z, dim) numpy.ndarray of float
            Gridded z-edge locations (staggered grid)
        """
        if getattr(self, "_edges_z", None) is None and self.dim == 3:
            N = self.reshape(self.gridN, "N", "N", "M")
            XYZ = [mkvc(0.5 * (n[:, :, :-1] + n[:, :, 1:])) for n in N]
            self._edges_z = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_z

    @property
    def edges(self):
        """Gridded edge locations (staggered grid)

        This property returns a numpy array of shape (n_edges, dim)
        containing gridded locations for all edges in the
        mesh (staggered grid). This is equivalent to calling
        np.r_[edges_x, edges_y, edges_z].

        Returns
        -------
        (n_edges, dim) numpy.ndarray of float
            Gridded edge locations (staggered grid)

        Examples
        --------
        Here, we provide an example of a minimally staggered curvilinear mesh.
        In this case, the x and y-edges have normal vectors that are
        primarily along the x and y-directions, respectively.

        >>> from discretize import CurvilinearMesh
        >>> from discretize.utils import example_curvilinear_grid, mkvc
        >>> from matplotlib import pyplot as plt

        >>> x, y = example_curvilinear_grid([10, 10], "rotate")
        >>> mesh1 = CurvilinearMesh([x, y])
        >>> edges = mesh1.edges
        >>> x_edges = edges[:mesh1.n_edges_x]
        >>> y_edges = edges[mesh1.n_edges_x:]

        >>> fig1 = plt.figure(figsize=(5, 5))
        >>> ax1 = fig1.add_subplot(111)
        >>> mesh1.plot_grid(ax=ax1)
        >>> ax1.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r')
        >>> ax1.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g')
        >>> ax1.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16)
        >>> plt.plot()

        Here, we provide an example of a highly irregular curvilinear mesh.
        In this case, the y-edges are not defined by normal vectors along
        a particular direction.

        >>> x, y = example_curvilinear_grid([10, 10], "sphere")
        >>> mesh2 = CurvilinearMesh([x, y])
        >>> edges = mesh2.edges
        >>> x_edges = edges[:mesh2.n_edges_x]
        >>> y_edges = edges[mesh2.n_edges_x:]

        >>> fig2 = plt.figure(figsize=(5, 5))
        >>> ax2 = fig2.add_subplot(111)
        >>> mesh2.plot_grid(ax=ax2)
        >>> ax2.scatter(x_edges[:, 0], x_edges[:, 1], 30, 'r')
        >>> ax2.scatter(y_edges[:, 0], y_edges[:, 1], 30, 'g')
        >>> ax2.legend(['Mesh', 'X-edges', 'Y-edges'], fontsize=16)
        >>> plt.show()
        """
        edges = np.r_[self.edges_x, self.edges_y]
        if self.dim > 2:
            edges = np.r_[edges, self.edges_z]
        return edges

    @property
    def boundary_nodes(self):
        """Gridded boundary node locations

        This property returns a numpy array of shape
        (n_boundary_nodes, dim) containing the gridded locations
        of the nodes on the boundary of the mesh.

        Returns
        -------
        (n_boundary_nodes, dim) numpy.ndarray of float
            Gridded boundary node locations
        """
        return self.nodes[make_boundary_bool(self.shape_nodes)]

    @property
    def boundary_edges(self):
        """Gridded boundary edge locations

        This property returns a numpy array of shape
        (n_boundary_edges, dim) containing the gridded locations
        of the edges on the boundary of the mesh. The returned
        quantity is organized *np.r_[edges_x, edges_y, edges_z]* .

        Returns
        -------
        (n_boundary_edges, dim) numpy.ndarray of float
            Gridded boundary edge locations
        """
        if self.dim == 2:
            ex = self.edges_x[make_boundary_bool(self.shape_edges_x, dir="y")]
            ey = self.edges_y[make_boundary_bool(self.shape_edges_y, dir="x")]
            return np.r_[ex, ey]
        elif self.dim == 3:
            ex = self.edges_x[make_boundary_bool(self.shape_edges_x, dir="yz")]
            ey = self.edges_y[make_boundary_bool(self.shape_edges_y, dir="xz")]
            ez = self.edges_z[make_boundary_bool(self.shape_edges_z, dir="xy")]
            return np.r_[ex, ey, ez]

    @property
    def boundary_faces(self):
        """Gridded locations of non-hanging x-faces

        This property returns a numpy array of shape (n_faces_x, dim)
        containing gridded locations for all non-hanging x-faces.

        Returns
        -------
        (n_faces_x, dim) numpy.ndarray of float
            Gridded locations of all non-hanging x-faces
        """
        fx = self.faces_x[make_boundary_bool(self.shape_faces_x, dir="x")]
        fy = self.faces_y[make_boundary_bool(self.shape_faces_y, dir="y")]
        if self.dim == 2:
            return np.r_[fx, fy]
        elif self.dim == 3:
            fz = self.faces_z[make_boundary_bool(self.shape_faces_z, dir="z")]
            return np.r_[fx, fy, fz]

    @property
    def boundary_face_outward_normals(self):
        """Outward normals of boundary faces

        For all boundary faces in the mesh, this property returns
        the unit vectors denoting the outward normals to the boundary.
        The returned quantity is a numpy array of shape
        (n_boundary_faces, dim).

        Returns
        -------
        (n_boundary_faces, dim) numpy.ndarray of float
            Outward normals of boundary faces
        """
        is_bxm = np.zeros(self.shape_faces_x, order="F", dtype=bool)
        is_bxm[0, :] = True
        is_bxm = is_bxm.reshape(-1, order="F")

        is_bym = np.zeros(self.shape_faces_y, order="F", dtype=bool)
        is_bym[:, 0] = True
        is_bym = is_bym.reshape(-1, order="F")

        is_b = np.r_[
            make_boundary_bool(self.shape_faces_x, dir="x"),
            make_boundary_bool(self.shape_faces_y, dir="y"),
        ]
        switch = np.r_[is_bxm, is_bym]
        if self.dim == 3:
            is_bzm = np.zeros(self.shape_faces_z, order="F", dtype=bool)
            is_bzm[:, :, 0] = True
            is_bzm = is_bzm.reshape(-1, order="F")

            is_b = np.r_[is_b, make_boundary_bool(self.shape_faces_z, dir="z")]
            switch = np.r_[switch, is_bzm]
        face_normals = self.face_normals.copy()
        face_normals[switch] *= -1
        return face_normals[is_b]

    # --------------- Geometries ---------------------
    #
    #
    # ------------------- 2D -------------------------
    #
    #         node(i,j)          node(i,j+1)
    #              A -------------- B
    #              |                |
    #              |    cell(i,j)   |
    #              |        I       |
    #              |                |
    #             D -------------- C
    #         node(i+1,j)        node(i+1,j+1)
    #
    # ------------------- 3D -------------------------
    #
    #
    #             node(i,j,k+1)       node(i,j+1,k+1)
    #                 E --------------- F
    #                /|               / |
    #               / |              /  |
    #              /  |             /   |
    #       node(i,j,k)         node(i,j+1,k)
    #            A -------------- B     |
    #            |    H ----------|---- G
    #            |   /cell(i,j)   |   /
    #            |  /     I       |  /
    #            | /              | /
    #            D -------------- C
    #       node(i+1,j,k)      node(i+1,j+1,k)

    @property
    def cell_volumes(self):
        """Return cell volumes

        Calling this property will compute and return a 1D array
        containing the volumes of mesh cells.

        Returns
        -------
        (n_cells) numpy.ndarray
            The quantity returned depends on the dimensions of the mesh:
                - *2D:* Returns the cell areas
                - *3D:* Returns the cell volumes
        """

        if getattr(self, "_cell_volumes", None) is None:
            if self.dim == 2:
                A, B, C, D = index_cube("ABCD", self.vnN)
                normal, area = face_info(
                    np.c_[self.gridN, np.zeros((self.nN, 1))], A, B, C, D
                )
                self._cell_volumes = area
            elif self.dim == 3:
                # Each polyhedron can be decomposed into 5 tetrahedrons
                # However, this presents a choice so we may as well divide in
                # two ways and average.
                A, B, C, D, E, F, G, H = index_cube("ABCDEFGH", self.vnN)

                vol1 = (
                    volume_tetrahedron(self.gridN, A, B, D, E)
                    + volume_tetrahedron(self.gridN, B, E, F, G)  # cutted edge top
                    + volume_tetrahedron(self.gridN, B, D, E, G)  # cutted edge top
                    + volume_tetrahedron(self.gridN, B, C, D, G)  # middle
                    + volume_tetrahedron(self.gridN, D, E, G, H)  # cutted edge bottom
                )  # cutted edge bottom

                vol2 = (
                    volume_tetrahedron(self.gridN, A, F, B, C)
                    + volume_tetrahedron(self.gridN, A, E, F, H)  # cutted edge top
                    + volume_tetrahedron(self.gridN, A, H, F, C)  # cutted edge top
                    + volume_tetrahedron(self.gridN, C, H, D, A)  # middle
                    + volume_tetrahedron(self.gridN, C, G, H, F)  # cutted edge bottom
                )  # cutted edge bottom

                self._cell_volumes = (vol1 + vol2) / 2
        return self._cell_volumes

    @property
    def face_areas(self):
        """Returns the areas of all faces in the mesh

        Calling this property will compute and return the areas of all
        faces as a 1D numpy array. The returned quantity is ordered x-face
        areas, then y-face areas, then z-face areas.

        Returns
        -------
        (n_faces) numpy.ndarray
            The length of the quantity returned depends on the dimensions of the mesh:

            - *1D:* returns the x-face areas
            - *2D:* returns the x-face and y-face areas in order; i.e. y-edge
              and x-edge lengths, respectively
            - *3D:* returns the x, y and z-face areas in order
        """
        if (
            getattr(self, "_face_areas", None) is None
            or getattr(self, "_normals", None) is None
        ):
            # Compute areas of cell faces
            if self.dim == 2:
                xy = self.gridN
                A, B = index_cube("AB", self.vnN, self.vnFx)
                edge1 = xy[B, :] - xy[A, :]
                normal1 = np.c_[edge1[:, 1], -edge1[:, 0]]
                area1 = _length2D(edge1)
                A, D = index_cube("AD", self.vnN, self.vnFy)
                # Note that we are doing A-D to make sure the normal points the
                # right way.
                # Think about it. Look at the picture. Normal points towards C
                # iff you do this.
                edge2 = xy[A, :] - xy[D, :]
                normal2 = np.c_[edge2[:, 1], -edge2[:, 0]]
                area2 = _length2D(edge2)
                self._face_areas = np.r_[mkvc(area1), mkvc(area2)]
                self._normals = [_normalize2D(normal1), _normalize2D(normal2)]

            elif self.dim == 3:

                A, E, F, B = index_cube("AEFB", self.vnN, self.vnFx)
                normal1, area1 = face_info(
                    self.gridN, A, E, F, B, average=False, normalizeNormals=False
                )

                A, D, H, E = index_cube("ADHE", self.vnN, self.vnFy)
                normal2, area2 = face_info(
                    self.gridN, A, D, H, E, average=False, normalizeNormals=False
                )

                A, B, C, D = index_cube("ABCD", self.vnN, self.vnFz)
                normal3, area3 = face_info(
                    self.gridN, A, B, C, D, average=False, normalizeNormals=False
                )

                self._face_areas = np.r_[mkvc(area1), mkvc(area2), mkvc(area3)]
                self._normals = [normal1, normal2, normal3]
        return self._face_areas

    @property
    def face_normals(self):
        """Gridded average face normals for all mesh faces.

        This property computes and returns a numpy array of shape
        (n_faces, dim) containing the normal vectors for
        all mesh faces. For 3D meshes, there are 4 nodes in which
        cross-products can be used to compute the normal vector.
        In this case, the average normal vector is returned so there
        is only 1 vector per face.

        Returns
        -------
        (n_faces, dim) numpy.ndarray of shape
            Gridded average face normals for all mesh faces.
        """

        if getattr(self, "_normals", None) is None:
            self.face_areas  # calling .face_areas will create the face normals
        if self.dim == 2:
            return _normalize2D(np.r_[self._normals[0], self._normals[1]])
        elif self.dim == 3:
            normal1 = (
                self._normals[0][0]
                + self._normals[0][1]
                + self._normals[0][2]
                + self._normals[0][3]
            ) / 4
            normal2 = (
                self._normals[1][0]
                + self._normals[1][1]
                + self._normals[1][2]
                + self._normals[1][3]
            ) / 4
            normal3 = (
                self._normals[2][0]
                + self._normals[2][1]
                + self._normals[2][2]
                + self._normals[2][3]
            ) / 4
            return _normalize3D(np.r_[normal1, normal2, normal3])

    @property
    def edge_lengths(self):
        """Returns the lengths of all edges in the mesh

        Calling this property will compute and return the lengths of all
        edges in the mesh. The returned quantity is ordered x-edge lengths,
        then y-edge lengths, then z-edge lengths.

        Returns
        -------
        (n_edges) numpy.ndarray
            The length of the quantity returned depends on the dimensions of the mesh:

            - *1D:* returns the x-edge lengths
            - *2D:* returns the x-edge and y-edge lengths in order
            - *3D:* returns the x, y and z-edge lengths in order
        """
        if getattr(self, "_edge_lengths", None) is None:
            if self.dim == 2:
                xy = self.gridN
                A, D = index_cube("AD", self.vnN, self.vnEx)
                edge1 = xy[D, :] - xy[A, :]
                A, B = index_cube("AB", self.vnN, self.vnEy)
                edge2 = xy[B, :] - xy[A, :]
                self._edge_lengths = np.r_[
                    mkvc(_length2D(edge1)), mkvc(_length2D(edge2))
                ]
                self._edge_tangents = (
                    np.r_[edge1, edge2] / np.c_[self._edge_lengths, self._edge_lengths]
                )
            elif self.dim == 3:
                xyz = self.gridN
                A, D = index_cube("AD", self.vnN, self.vnEx)
                edge1 = xyz[D, :] - xyz[A, :]
                A, B = index_cube("AB", self.vnN, self.vnEy)
                edge2 = xyz[B, :] - xyz[A, :]
                A, E = index_cube("AE", self.vnN, self.vnEz)
                edge3 = xyz[E, :] - xyz[A, :]
                self._edge_lengths = np.r_[
                    mkvc(_length3D(edge1)),
                    mkvc(_length3D(edge2)),
                    mkvc(_length3D(edge3)),
                ]
                self._edge_tangents = (
                    np.r_[edge1, edge2, edge3]
                    / np.c_[self._edge_lengths, self._edge_lengths, self._edge_lengths]
                )
        return self._edge_lengths

    @property
    def edge_tangents(self):
        """Gridded edge tangents directions for all mesh edges

        This property computes and returns a numpy array of shape
        (n_edges, dim) containing the edge tangent directions for
        all mesh edges.

        Returns
        -------
        (n_edges, dim) numpy.ndarray of shape
            Gridded edge tangent directions for all mesh edges
        """
        if getattr(self, "_edge_tangents", None) is None:
            self.edge_lengths  # calling .edge_lengths will create the tangents
        return self._edge_tangents

    # DEPRECATIONS
    vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0", future_warn=False)
    area = deprecate_property("face_areas", "area", removal_version="1.0.0", future_warn=False)
    edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0", future_warn=False)
Exemplo n.º 4
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)
Exemplo n.º 5
0
class TreeMesh(_TreeMesh, BaseTensorMesh, InnerProducts, DiffOperators,
               TreeMeshIO, InterfaceMixins):
    """Class for QuadTree (2D) and OcTree (3D) meshes.

    Tree meshes are numerical grids where the dimensions of each cell are powers of 2
    larger than some base cell dimension. Unlike the :class:`~discretize.TensorMesh`
    class, gridded locations and numerical operators for instances of ``TreeMesh``
    cannot be simply constructed using tensor products. Furthermore, each cell
    is an instance of ``TreeMesh`` is an instance of the
    :class:`~discretize.tree_mesh.TreeCell` .

    Parameters
    ----------
    h : (dim) iterable of int, numpy.ndarray, or tuple
        Defines the cell widths of the *underlying tensor mesh* along each axis. The
        length of the iterable object is equal to the dimension of the mesh (2 or 3).
        For a 3D mesh, the list would have the form *[hx, hy, hz]*. The number of cells
        along each axis **must be a power of 2** .

        Along each axis, the user has 3 choices for defining the cells widths for the
        underlying tensor mesh:

        - :class:`int` -> A unit interval is equally discretized into `N` cells.
        - :class:`numpy.ndarray` -> The widths are explicity given for each cell
        - the widths are defined as a :class:`list` of :class:`tuple` of the form *(dh, nc, [npad])*
          where *dh* is the cell width, *nc* is the number of cells, and *npad* (optional)
          is a padding factor denoting exponential increase/decrease in the cell width
          for each cell; e.g. *[(2., 10, -1.3), (2., 50), (2., 10, 1.3)]*

    origin : (dim) iterable, default: 0
        Define the origin or 'anchor point' of the mesh; i.e. the bottom-left-frontmost
        corner. By default, the mesh is anchored such that its origin is at [0, 0, 0].

        For each dimension (x, y or z), The user may set the origin 2 ways:

        - a ``scalar`` which explicitly defines origin along that dimension.
        - **{'0', 'C', 'N'}** a :class:`str` specifying whether the zero coordinate along
          each axis is the first node location ('0'), in the center ('C') or the last
          node location ('N') (see Examples).

    Examples
    --------
    Here we generate a basic 2D tree mesh.

    >>> from discretize import TreeMesh
    >>> import numpy as np
    >>> import matplotlib.pyplot as plt

    Define base mesh (domain and finest discretization),

    >>> dh = 5    # minimum cell width (base mesh cell width)
    >>> nbc = 64  # number of base mesh cells
    >>> h = dh * np.ones(nbc)
    >>> mesh = TreeMesh([h, h])

    Define corner points for a rectangular box, and subdived the mesh within the box
    to the maximum refinement level.

    >>> x0s = [120.0, 80.0]
    >>> x1s = [240.0, 160.0]
    >>> levels = [mesh.max_level]
    >>> mesh.refine_box(x0s, x1s, levels)

    >>> mesh.plot_grid()
    >>> plt.show()
    """

    _meshType = "TREE"
    _aliases = {
        **BaseTensorMesh._aliases,
        **DiffOperators._aliases,
        **{
            "ntN": "n_total_nodes",
            "ntEx": "n_total_edges_x",
            "ntEy": "n_total_edges_y",
            "ntEz": "n_total_edges_z",
            "ntE": "n_total_edges",
            "ntFx": "n_total_faces_x",
            "ntFy": "n_total_faces_y",
            "ntFz": "n_total_faces_z",
            "ntF": "n_total_faces",
            "nhN": "n_hanging_nodes",
            "nhEx": "n_hanging_edges_x",
            "nhEy": "n_hanging_edges_y",
            "nhEz": "n_hanging_edges_z",
            "nhE": "n_hanging_edges",
            "nhFx": "n_hanging_faces_x",
            "nhFy": "n_hanging_faces_y",
            "nhFz": "n_hanging_faces_z",
            "nhF": "n_hanging_faces",
            "gridhN": "hanging_nodes",
            "gridhFx": "hanging_faces_x",
            "gridhFy": "hanging_faces_y",
            "gridhFz": "hanging_faces_z",
            "gridhEx": "hanging_edges_x",
            "gridhEy": "hanging_edges_y",
            "gridhEz": "hanging_edges_z",
        },
    }
    _items = {"h", "origin", "cell_state"}

    # inheriting stuff from BaseTensorMesh that isn't defined in _QuadTree
    def __init__(self, h=None, origin=None, **kwargs):
        if "x0" in kwargs:
            origin = kwargs.pop("x0")
        super().__init__(h=h, origin=origin)

        cell_state = kwargs.pop("cell_state", None)
        cell_indexes = kwargs.pop("cell_indexes", None)
        cell_levels = kwargs.pop("cell_levels", None)
        if cell_state is None:
            if cell_indexes is not None and cell_levels is not None:
                cell_state = {}
                cell_state["indexes"] = cell_indexes
                cell_state["levels"] = cell_levels
        if cell_state is not None:
            indexes = cell_state["indexes"]
            levels = cell_state["levels"]
            self.__setstate__((indexes, levels))

    def __repr__(self):
        """Plain text representation."""
        mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad"))

        top = "\n" + mesh_name + ": {0:2.2f}% filled\n\n".format(
            self.fill * 100)

        # Number of cells per level
        level_count = self._count_cells_per_index()
        non_zero_levels = np.nonzero(level_count)[0]
        cell_display = ["Level : Number of cells"]
        cell_display.append("-----------------------")
        for level in non_zero_levels:
            cell_display.append("{:^5} : {:^15}".format(
                level, level_count[level]))
        cell_display.append("-----------------------")
        cell_display.append("Total : {:^15}".format(self.nC))

        extent_display = ["            Mesh Extent       "]
        extent_display.append("        min     ,     max     ")
        extent_display.append("   ---------------------------")
        dim_label = {0: "x", 1: "y", 2: "z"}
        for dim in range(self.dim):
            n_vector = getattr(self, "nodes_" + dim_label[dim])
            extent_display.append("{}: {:^13},{:^13}".format(
                dim_label[dim], n_vector[0], n_vector[-1]))

        for i, line in enumerate(extent_display):
            if i == len(cell_display):
                cell_display.append(" " *
                                    (len(cell_display[0]) - 3 - len(line)))
            cell_display[i] += 3 * " " + line

        h_display = ["     Cell Widths    "]
        h_display.append("    min   ,   max   ")
        h_display.append("-" * (len(h_display[0])))
        h_gridded = self.h_gridded
        mins = np.min(h_gridded, axis=0)
        maxs = np.max(h_gridded, axis=0)
        for dim in range(self.dim):
            h_display.append("{:^10}, {:^10}".format(mins[dim], maxs[dim]))

        for i, line in enumerate(h_display):
            if i == len(cell_display):
                cell_display.append(" " * len(cell_display[0]))
            cell_display[i] += 3 * " " + line

        return top + "\n".join(cell_display)

    def _repr_html_(self):
        """html representation"""
        mesh_name = "{0!s}TreeMesh".format(("Oc" if self.dim == 3 else "Quad"))
        level_count = self._count_cells_per_index()
        non_zero_levels = np.nonzero(level_count)[0]
        dim_label = {0: "x", 1: "y", 2: "z"}
        h_gridded = self.h_gridded
        mins = np.min(h_gridded, axis=0)
        maxs = np.max(h_gridded, axis=0)

        style = " style='padding: 5px 20px 5px 20px;'"
        # Cell level table:
        cel_tbl = "<table>\n"
        cel_tbl += "<tr>\n"
        cel_tbl += "<th" + style + ">Level</th>\n"
        cel_tbl += "<th" + style + ">Number of cells</th>\n"
        cel_tbl += "</tr>\n"
        for level in non_zero_levels:
            cel_tbl += "<tr>\n"
            cel_tbl += "<td" + style + ">{}</td>\n".format(level)
            cel_tbl += "<td" + style + ">{}</td>\n".format(level_count[level])
            cel_tbl += "</tr>\n"
        cel_tbl += "<tr>\n"
        cel_tbl += (
            "<td style='font-weight: bold; padding: 5px 20px 5px 20px;'> Total </td>\n"
        )
        cel_tbl += "<td" + style + "> {} </td>\n".format(self.nC)
        cel_tbl += "</tr>\n"
        cel_tbl += "</table>\n"

        det_tbl = "<table>\n"
        det_tbl += "<tr>\n"
        det_tbl += "<th></th>\n"
        det_tbl += "<th" + style + " colspan='2'>Mesh extent</th>\n"
        det_tbl += "<th" + style + " colspan='2'>Cell widths</th>\n"
        det_tbl += "</tr>\n"

        det_tbl += "<tr>\n"
        det_tbl += "<th></th>\n"
        det_tbl += "<th" + style + ">min</th>\n"
        det_tbl += "<th" + style + ">max</th>\n"
        det_tbl += "<th" + style + ">min</th>\n"
        det_tbl += "<th" + style + ">max</th>\n"
        det_tbl += "</tr>\n"
        for dim in range(self.dim):
            n_vector = getattr(self, "nodes_" + dim_label[dim])
            det_tbl += "<tr>\n"
            det_tbl += "<td" + style + ">{}</td>\n".format(dim_label[dim])
            det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[0])
            det_tbl += "<td" + style + ">{}</td>\n".format(n_vector[-1])
            det_tbl += "<td" + style + ">{}</td>\n".format(mins[dim])
            det_tbl += "<td" + style + ">{}</td>\n".format(maxs[dim])
            det_tbl += "</tr>\n"
        det_tbl += "</table>\n"

        full_tbl = "<table>\n"
        full_tbl += "<tr>\n"
        full_tbl += "<td style='font-weight: bold; font-size: 1.2em; text-align: center;'>{}</td>\n".format(
            mesh_name)
        full_tbl += "<td style='font-size: 1.2em; text-align: center;' colspan='2'>{0:2.2f}% filled</td>\n".format(
            100 * self.fill)
        full_tbl += "</tr>\n"
        full_tbl += "<tr>\n"

        full_tbl += "<td>\n"
        full_tbl += cel_tbl
        full_tbl += "</td>\n"

        full_tbl += "<td>\n"
        full_tbl += det_tbl
        full_tbl += "</td>\n"

        full_tbl += "</tr>\n"
        full_tbl += "</table>\n"

        return full_tbl

    @BaseTensorMesh.origin.setter
    def origin(self, value):
        # first use the BaseTensorMesh to set the origin to handle "0, C, N"
        BaseTensorMesh.origin.fset(self, value)
        # then update the TreeMesh with the hidden value
        self._set_origin(self._origin)

    @property
    def vntF(self):
        """
        Vector number of total faces along each axis

        This property returns the total number of hanging and
        non-hanging faces along each axis direction. The returned
        quantity is a list of integers of the form [nFx,nFy,nFz].

        Returns
        -------
        list of int
            Vector number of total faces along each axis
        """
        return [self.ntFx, self.ntFy] + ([] if self.dim == 2 else [self.ntFz])

    @property
    def vntE(self):
        """
        Vector number of total edges along each axis

        This property returns the total number of hanging and
        non-hanging edges along each axis direction. The returned
        quantity is a list of integers of the form [nEx,nEy,nEz].

        Returns
        -------
        list of int
            Vector number of total edges along each axis
        """
        return [self.ntEx, self.ntEy] + ([] if self.dim == 2 else [self.ntEz])

    @property
    def stencil_cell_gradient(self):
        if getattr(self, "_stencil_cell_gradient", None) is None:

            self._stencil_cell_gradient = sp.vstack(
                [self.stencil_cell_gradient_x, self.stencil_cell_gradient_y])
            if self.dim == 3:
                self._stencil_cell_gradient = sp.vstack([
                    self._stencil_cell_gradient, self.stencil_cell_gradient_z
                ])

        return self._stencil_cell_gradient

    @property
    def cell_gradient(self):
        if getattr(self, "_cell_gradient", None) is None:

            i_s = self.face_boundary_indices

            ix = np.ones(self.nFx)
            ix[i_s[0]] = 0.0
            ix[i_s[1]] = 0.0
            Pafx = sp.diags(ix)

            iy = np.ones(self.nFy)
            iy[i_s[2]] = 0.0
            iy[i_s[3]] = 0.0
            Pafy = sp.diags(iy)

            MfI = self.get_face_inner_product(invMat=True)

            if self.dim == 2:
                Pi = sp.block_diag([Pafx, Pafy])

            elif self.dim == 3:
                iz = np.ones(self.nFz)
                iz[i_s[4]] = 0.0
                iz[i_s[5]] = 0.0
                Pafz = sp.diags(iz)
                Pi = sp.block_diag([Pafx, Pafy, Pafz])

            self._cell_gradient = (-Pi * MfI * self.face_divergence.T *
                                   sp.diags(self.cell_volumes))

        return self._cell_gradient

    @property
    def cell_gradient_x(self):
        if getattr(self, "_cell_gradient_x", None) is None:

            nFx = self.nFx
            i_s = self.face_boundary_indices

            ix = np.ones(self.nFx)
            ix[i_s[0]] = 0.0
            ix[i_s[1]] = 0.0
            Pafx = sp.diags(ix)

            MfI = self.get_face_inner_product(invMat=True)
            MfIx = sp.diags(MfI.diagonal()[:nFx])

            self._cell_gradient_x = (-Pafx * MfIx * self.face_x_divergence.T *
                                     sp.diags(self.cell_volumes))

        return self._cell_gradient_x

    @property
    def cell_gradient_y(self):
        if getattr(self, "_cell_gradient_y", None) is None:

            nFx = self.nFx
            nFy = self.nFy
            i_s = self.face_boundary_indices

            iy = np.ones(self.nFy)
            iy[i_s[2]] = 0.0
            iy[i_s[3]] = 0.0
            Pafy = sp.diags(iy)

            MfI = self.get_face_inner_product(invMat=True)
            MfIy = sp.diags(MfI.diagonal()[nFx:nFx + nFy])

            self._cell_gradient_y = (-Pafy * MfIy * self.face_y_divergence.T *
                                     sp.diags(self.cell_volumes))

        return self._cell_gradient_y

    @property
    def cell_gradient_z(self):
        if self.dim == 2:
            raise TypeError("z derivative not defined in 2D")
        if getattr(self, "_cell_gradient_z", None) is None:

            nFx = self.nFx
            nFy = self.nFy
            i_s = self.face_boundary_indices

            iz = np.ones(self.nFz)
            iz[i_s[4]] = 0.0
            iz[i_s[5]] = 0.0
            Pafz = sp.diags(iz)

            MfI = self.get_face_inner_product(invMat=True)
            MfIz = sp.diags(MfI.diagonal()[nFx + nFy:])

            self._cell_gradient_z = (-Pafz * MfIz * self.face_z_divergence.T *
                                     sp.diags(self.cell_volumes))

        return self._cell_gradient_z

    @property
    def face_x_divergence(self):
        if getattr(self, "_face_x_divergence", None) is None:
            self._face_x_divergence = self.face_divergence[:, :self.nFx]
        return self._face_x_divergence

    @property
    def face_y_divergence(self):
        if getattr(self, "_face_y_divergence", None) is None:
            self._face_y_divergence = self.face_divergence[:,
                                                           self.nFx:self.nFx +
                                                           self.nFy]
        return self._face_y_divergence

    @property
    def face_z_divergence(self):
        if getattr(self, "_face_z_divergence", None) is None:
            self._face_z_divergence = self.face_divergence[:, self.nFx +
                                                           self.nFy:]
        return self._face_z_divergence

    def point2index(self, locs):
        """Finds cells that contain the given points.
        Returns an array of index values of the cells that contain the given
        points

        Parameters
        ----------
        locs: (N, dim) array_like
            points to search for the location of

        Returns
        -------
        (N) array_like of int
            Cell indices that contain the points
        """
        locs = as_array_n_by_dim(locs, self.dim)
        inds = self._get_containing_cell_indexes(locs)
        return inds

    def cell_levels_by_index(self, indices):
        """Fast function to return a list of levels for the given cell indices

        Parameters
        ----------
        index: (N) array_like
            Cell indexes to query

        Returns
        -------
        (N) numpy.ndarray of int
            Levels for the cells.
        """

        return self._cell_levels_by_indexes(indices)

    def get_interpolation_matrix(self,
                                 locs,
                                 location_type="CC",
                                 zeros_outside=False,
                                 **kwargs):
        """Produces interpolation matrix

        Parameters
        ----------
        loc : (N, dim) array_like
            Location of points to interpolate to

        location_type: str, optional
            What to interpolate

            location_type can be:

            - 'CC'    -> scalar field defined on cell centers
            - 'Ex'    -> x-component of field defined on edges
            - 'Ey'    -> y-component of field defined on edges
            - 'Ez'    -> z-component of field defined on edges
            - 'Fx'    -> x-component of field defined on faces
            - 'Fy'    -> y-component of field defined on faces
            - 'Fz'    -> z-component of field defined on faces
            - 'N'     -> scalar field defined on nodes

        Returns
        -------
        (N, n_loc_type) scipy.sparse.csr_matrix
            the interpolation matrix

        """
        if "locType" in kwargs:
            warnings.warn(
                "The locType keyword argument has been deprecated, please use location_type. "
                "This will be removed in discretize 1.0.0",
                DeprecationWarning,
            )
            location_type = kwargs["locType"]
        if "zerosOutside" in kwargs:
            warnings.warn(
                "The zerosOutside keyword argument has been deprecated, please use zeros_outside. "
                "This will be removed in discretize 1.0.0",
                DeprecationWarning,
            )
            zeros_outside = kwargs["zerosOutside"]
        locs = as_array_n_by_dim(locs, self.dim)
        if location_type not in [
                "N", "CC", "Ex", "Ey", "Ez", "Fx", "Fy", "Fz"
        ]:
            raise Exception(
                "location_type must be one of N, CC, Ex, Ey, Ez, Fx, Fy, or Fz"
            )

        if self.dim == 2 and location_type in ["Ez", "Fz"]:
            raise Exception("Unable to interpolate from Z edges/face in 2D")

        locs = np.require(np.atleast_2d(locs),
                          dtype=np.float64,
                          requirements="C")

        if location_type == "N":
            Av = self._getNodeIntMat(locs, zeros_outside)
        elif location_type in ["Ex", "Ey", "Ez"]:
            Av = self._getEdgeIntMat(locs, zeros_outside, location_type[1])
        elif location_type in ["Fx", "Fy", "Fz"]:
            Av = self._getFaceIntMat(locs, zeros_outside, location_type[1])
        elif location_type in ["CC"]:
            Av = self._getCellIntMat(locs, zeros_outside)
        return Av

    @property
    def permute_cells(self):
        """Permutation matrix re-ordering of cells sorted by x, then y, then z

        Returns
        -------
        (n_cells, n_cells) scipy.sparse.csr_matrix
        """
        # TODO: cache these?
        P = np.lexsort(self.gridCC.T)  # sort by x, then y, then z
        return sp.identity(self.nC).tocsr()[P]

    @property
    def permute_faces(self):
        """Permutation matrix re-ordering of faces sorted by x, then y, then z

        Returns
        -------
        (n_faces, n_faces) scipy.sparse.csr_matrix
        """
        # TODO: cache these?
        Px = np.lexsort(self.gridFx.T)
        Py = np.lexsort(self.gridFy.T) + self.nFx
        if self.dim == 2:
            P = np.r_[Px, Py]
        else:
            Pz = np.lexsort(self.gridFz.T) + (self.nFx + self.nFy)
            P = np.r_[Px, Py, Pz]
        return sp.identity(self.nF).tocsr()[P]

    @property
    def permute_edges(self):
        """Permutation matrix re-ordering of edges sorted by x, then y, then z

        Returns
        -------
        (n_edges, n_edges) scipy.sparse.csr_matrix
        """
        # TODO: cache these?
        Px = np.lexsort(self.gridEx.T)
        Py = np.lexsort(self.gridEy.T) + self.nEx
        if self.dim == 2:
            P = np.r_[Px, Py]
        if self.dim == 3:
            Pz = np.lexsort(self.gridEz.T) + (self.nEx + self.nEy)
            P = np.r_[Px, Py, Pz]
        return sp.identity(self.nE).tocsr()[P]

    @property
    def cell_state(self):
        """ The current state of the cells on the mesh.

        This represents the x, y, z indices of the cells in the base tensor mesh, as
        well as their levels. It can be used to reconstruct the mesh.

        Returns
        -------
        dict
            dictionary with two entries:

            - ``"indexes"``: the indexes of the cells
            - ``"levels"``: the levels of the cells
        """
        indexes, levels = self.__getstate__()
        return {"indexes": indexes.tolist(), "levels": levels.tolist()}

    def validate(self):
        return self.finalized

    def equals(self, other):
        try:
            if self.finalized and other.finalized:
                return super().equals(other)
        except AttributeError:
            pass
        return False

    def __reduce__(self):
        return TreeMesh, (self.h, self.origin), self.__getstate__()

    cellGrad = deprecate_property("cell_gradient",
                                  "cellGrad",
                                  removal_version="1.0.0",
                                  future_warn=False)
    cellGradx = deprecate_property("cell_gradient_x",
                                   "cellGradx",
                                   removal_version="1.0.0",
                                   future_warn=False)
    cellGrady = deprecate_property("cell_gradient_y",
                                   "cellGrady",
                                   removal_version="1.0.0",
                                   future_warn=False)
    cellGradz = deprecate_property("cell_gradient_z",
                                   "cellGradz",
                                   removal_version="1.0.0",
                                   future_warn=False)
    cellGradStencil = deprecate_property("cell_gradient_stencil",
                                         "cellGradStencil",
                                         removal_version="1.0.0",
                                         future_warn=False)
    nodalGrad = deprecate_property("nodal_gradient",
                                   "nodalGrad",
                                   removal_version="1.0.0",
                                   future_warn=False)
    nodalLaplacian = deprecate_property("nodal_laplacian",
                                        "nodalLaplacian",
                                        removal_version="1.0.0",
                                        future_warn=False)
    faceDiv = deprecate_property("face_divergence",
                                 "faceDiv",
                                 removal_version="1.0.0",
                                 future_warn=False)
    faceDivx = deprecate_property("face_x_divergence",
                                  "faceDivx",
                                  removal_version="1.0.0",
                                  future_warn=False)
    faceDivy = deprecate_property("face_y_divergence",
                                  "faceDivy",
                                  removal_version="1.0.0",
                                  future_warn=False)
    faceDivz = deprecate_property("face_z_divergence",
                                  "faceDivz",
                                  removal_version="1.0.0",
                                  future_warn=False)
    edgeCurl = deprecate_property("edge_curl",
                                  "edgeCurl",
                                  removal_version="1.0.0",
                                  future_warn=False)
    maxLevel = deprecate_property("max_used_level",
                                  "maxLevel",
                                  removal_version="1.0.0",
                                  future_warn=False)
    vol = deprecate_property("cell_volumes",
                             "vol",
                             removal_version="1.0.0",
                             future_warn=False)
    areaFx = deprecate_property("face_x_areas",
                                "areaFx",
                                removal_version="1.0.0",
                                future_warn=False)
    areaFy = deprecate_property("face_y_areas",
                                "areaFy",
                                removal_version="1.0.0",
                                future_warn=False)
    areaFz = deprecate_property("face_z_areas",
                                "areaFz",
                                removal_version="1.0.0",
                                future_warn=False)
    area = deprecate_property("face_areas",
                              "area",
                              removal_version="1.0.0",
                              future_warn=False)
    edgeEx = deprecate_property("edge_x_lengths",
                                "edgeEx",
                                removal_version="1.0.0",
                                future_warn=False)
    edgeEy = deprecate_property("edge_y_lengths",
                                "edgeEy",
                                removal_version="1.0.0",
                                future_warn=False)
    edgeEz = deprecate_property("edge_z_lengths",
                                "edgeEz",
                                removal_version="1.0.0",
                                future_warn=False)
    edge = deprecate_property("edge_lengths",
                              "edge",
                              removal_version="1.0.0",
                              future_warn=False)
    permuteCC = deprecate_property("permute_cells",
                                   "permuteCC",
                                   removal_version="1.0.0",
                                   future_warn=False)
    permuteF = deprecate_property("permute_faces",
                                  "permuteF",
                                  removal_version="1.0.0",
                                  future_warn=False)
    permuteE = deprecate_property("permute_edges",
                                  "permuteE",
                                  removal_version="1.0.0",
                                  future_warn=False)
    faceBoundaryInd = deprecate_property("face_boundary_indices",
                                         "faceBoundaryInd",
                                         removal_version="1.0.0",
                                         future_warn=False)
    cellBoundaryInd = deprecate_property("cell_boundary_indices",
                                         "cellBoundaryInd",
                                         removal_version="1.0.0",
                                         future_warn=False)
    _aveCC2FxStencil = deprecate_property("average_cell_to_total_face_x",
                                          "_aveCC2FxStencil",
                                          removal_version="1.0.0",
                                          future_warn=False)
    _aveCC2FyStencil = deprecate_property("average_cell_to_total_face_y",
                                          "_aveCC2FyStencil",
                                          removal_version="1.0.0",
                                          future_warn=False)
    _aveCC2FzStencil = deprecate_property("average_cell_to_total_face_z",
                                          "_aveCC2FzStencil",
                                          removal_version="1.0.0",
                                          future_warn=False)
    _cellGradStencil = deprecate_property("stencil_cell_gradient",
                                          "_cellGradStencil",
                                          removal_version="1.0.0",
                                          future_warn=False)
    _cellGradxStencil = deprecate_property("stencil_cell_gradient_x",
                                           "_cellGradxStencil",
                                           removal_version="1.0.0",
                                           future_warn=False)
    _cellGradyStencil = deprecate_property("stencil_cell_gradient_y",
                                           "_cellGradyStencil",
                                           removal_version="1.0.0",
                                           future_warn=False)
    _cellGradzStencil = deprecate_property("stencil_cell_gradient_z",
                                           "_cellGradzStencil",
                                           removal_version="1.0.0",
                                           future_warn=False)
class CurvilinearMesh(BaseRectangularMesh, DiffOperators, InnerProducts):
    """CurvilinearMesh is a mesh class that deals with curvilinear meshes.

    Example of a curvilinear mesh:

    .. plot::
        :include-source:

        import discretize
        X, Y = discretize.utils.exampleLrmGrid([3,3],'rotate')
        mesh = discretize.CurvilinearMesh([X, Y])
        mesh.plot_grid(show_it=True)
    """

    _meshType = "Curv"
    _aliases = {
        **DiffOperators._aliases,
        **BaseRectangularMesh._aliases,
        **{
            "gridCC": "cell_centers",
            "gridN": "nodes",
            "gridFx": "faces_x",
            "gridFy": "faces_y",
            "gridFz": "faces_z",
            "gridEx": "edges_x",
            "gridEy": "edges_y",
            "gridEz": "edges_z",
        },
    }

    node_list = properties.List(
        "List of arrays describing the node locations",
        prop=properties.Array(
            "node locations in an n-dimensional array",
            shape={("*", "*"), ("*", "*", "*")},
        ),
        min_length=2,
        max_length=3,
    )

    def __init__(self, node_list=None, **kwargs):

        if 'nodes' in kwargs:
            node_list = kwargs.pop('nodes')
        self.node_list = node_list

        if "_n" in kwargs.keys():
            n = kwargs.pop("_n")
            if np.any(n != np.array(self.node_list[0].shape) - 1):
                raise ValueError(
                    "Unexpected n-values. {} was provided, {} was expected".format(
                        n, np.array(self.node_list[0].shape) - 1
                    )
                )
        else:
            n = np.array(self.node_list[0].shape) - 1

        BaseRectangularMesh.__init__(self, n, **kwargs)

        # Save nodes to private variable _nodes as vectors
        self._nodes = np.ones((self.node_list[0].size, self.dim))
        for i, node_i in enumerate(self.node_list):
            self._nodes[:, i] = mkvc(node_i.astype(float))
        self.origin = self.nodes.min(axis=0)

    @properties.validator("node_list")
    def _check_nodes(self, change):
        if len(change["value"]) <= 1:
            raise ValueError("len(node) must be greater than 1")

        for i, change["value"][i] in enumerate(change["value"]):
            if change["value"][i].shape != change["value"][0].shape:
                raise ValueError(
                    "change['value'][{0:d}] is not the same shape as "
                    "change['value'][0]".format(i)
                )

        if len(change["value"][0].shape) != len(change["value"]):
            raise ValueError("Dimension mismatch")

    @classmethod
    def deserialize(cls, value, **kwargs):
        if "nodes" in value:
            value["node_list"] = value.pop("nodes")
        return super().deserialize(value, **kwargs)

    @property
    def cell_centers(self):
        """
        Cell-centered grid
        """
        if getattr(self, "_cell_centers", None) is None:
            self._cell_centers = np.concatenate(
                [self.aveN2CC * self.gridN[:, i] for i in range(self.dim)]
            ).reshape((-1, self.dim), order="F")
        return self._cell_centers

    @property
    def nodes(self):
        """
        Nodal grid.
        """
        if getattr(self, "_nodes", None) is None:
            raise Exception("Someone deleted this. I blame you.")
        return self._nodes

    @property
    def faces_x(self):
        """
        Face staggered grid in the x direction.
        """

        if getattr(self, "_faces_x", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N]
                self._faces_x = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [
                    mkvc(
                        0.25
                        * (
                            n[:, :-1, :-1]
                            + n[:, :-1, 1:]
                            + n[:, 1:, :-1]
                            + n[:, 1:, 1:]
                        )
                    )
                    for n in N
                ]
                self._faces_x = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_x

    @property
    def faces_y(self):
        """
        Face staggered grid in the y direction.
        """

        if getattr(self, "_faces_y", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N]
                self._faces_y = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [
                    mkvc(
                        0.25
                        * (
                            n[:-1, :, :-1]
                            + n[:-1, :, 1:]
                            + n[1:, :, :-1]
                            + n[1:, :, 1:]
                        )
                    )
                    for n in N
                ]
                self._faces_y = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_y

    @property
    def faces_z(self):
        """
        Face staggered grid in the y direction.
        """

        if getattr(self, "_faces_z", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            XYZ = [
                mkvc(
                    0.25
                    * (n[:-1, :-1, :] + n[:-1, 1:, :] + n[1:, :-1, :] + n[1:, 1:, :])
                )
                for n in N
            ]
            self._faces_z = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._faces_z

    @property
    def edges_x(self):
        """
        Edge staggered grid in the x direction.
        """
        if getattr(self, "_edges_x", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:-1, :] + n[1:, :])) for n in N]
                self._edges_x = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [mkvc(0.5 * (n[:-1, :, :] + n[1:, :, :])) for n in N]
                self._edges_x = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_x

    @property
    def edges_y(self):
        """
        Edge staggered grid in the y direction.
        """
        if getattr(self, "_edges_y", None) is None:
            N = self.reshape(self.gridN, "N", "N", "M")
            if self.dim == 2:
                XY = [mkvc(0.5 * (n[:, :-1] + n[:, 1:])) for n in N]
                self._edges_y = np.c_[XY[0], XY[1]]
            elif self.dim == 3:
                XYZ = [mkvc(0.5 * (n[:, :-1, :] + n[:, 1:, :])) for n in N]
                self._edges_y = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_y

    @property
    def edges_z(self):
        """
        Edge staggered grid in the z direction.
        """
        if getattr(self, "_edges_z", None) is None and self.dim == 3:
            N = self.reshape(self.gridN, "N", "N", "M")
            XYZ = [mkvc(0.5 * (n[:, :, :-1] + n[:, :, 1:])) for n in N]
            self._edges_z = np.c_[XYZ[0], XYZ[1], XYZ[2]]
        return self._edges_z

    # --------------- Geometries ---------------------
    #
    #
    # ------------------- 2D -------------------------
    #
    #         node(i,j)          node(i,j+1)
    #              A -------------- B
    #              |                |
    #              |    cell(i,j)   |
    #              |        I       |
    #              |                |
    #             D -------------- C
    #         node(i+1,j)        node(i+1,j+1)
    #
    # ------------------- 3D -------------------------
    #
    #
    #             node(i,j,k+1)       node(i,j+1,k+1)
    #                 E --------------- F
    #                /|               / |
    #               / |              /  |
    #              /  |             /   |
    #       node(i,j,k)         node(i,j+1,k)
    #            A -------------- B     |
    #            |    H ----------|---- G
    #            |   /cell(i,j)   |   /
    #            |  /     I       |  /
    #            | /              | /
    #            D -------------- C
    #       node(i+1,j,k)      node(i+1,j+1,k)

    @property
    def cell_volumes(self):
        """
        Construct cell volumes of the 3D model as 1d array
        """

        if getattr(self, "_cell_volumes", None) is None:
            if self.dim == 2:
                A, B, C, D = index_cube("ABCD", self.vnN)
                normal, area = face_info(
                    np.c_[self.gridN, np.zeros((self.nN, 1))], A, B, C, D
                )
                self._cell_volumes = area
            elif self.dim == 3:
                # Each polyhedron can be decomposed into 5 tetrahedrons
                # However, this presents a choice so we may as well divide in
                # two ways and average.
                A, B, C, D, E, F, G, H = index_cube("ABCDEFGH", self.vnN)

                vol1 = (
                    volume_tetrahedron(self.gridN, A, B, D, E)
                    + volume_tetrahedron(self.gridN, B, E, F, G)  # cutted edge top
                    + volume_tetrahedron(self.gridN, B, D, E, G)  # cutted edge top
                    + volume_tetrahedron(self.gridN, B, C, D, G)  # middle
                    + volume_tetrahedron(self.gridN, D, E, G, H)  # cutted edge bottom
                )  # cutted edge bottom

                vol2 = (
                    volume_tetrahedron(self.gridN, A, F, B, C)
                    + volume_tetrahedron(self.gridN, A, E, F, H)  # cutted edge top
                    + volume_tetrahedron(self.gridN, A, H, F, C)  # cutted edge top
                    + volume_tetrahedron(self.gridN, C, H, D, A)  # middle
                    + volume_tetrahedron(self.gridN, C, G, H, F)  # cutted edge bottom
                )  # cutted edge bottom

                self._cell_volumes = (vol1 + vol2) / 2
        return self._cell_volumes

    @property
    def face_areas(self):
        """
        Area of the faces
        """
        if (
            getattr(self, "_face_areas", None) is None
            or getattr(self, "_normals", None) is None
        ):
            # Compute areas of cell faces
            if self.dim == 2:
                xy = self.gridN
                A, B = index_cube("AB", self.vnN, self.vnFx)
                edge1 = xy[B, :] - xy[A, :]
                normal1 = np.c_[edge1[:, 1], -edge1[:, 0]]
                area1 = _length2D(edge1)
                A, D = index_cube("AD", self.vnN, self.vnFy)
                # Note that we are doing A-D to make sure the normal points the
                # right way.
                # Think about it. Look at the picture. Normal points towards C
                # iff you do this.
                edge2 = xy[A, :] - xy[D, :]
                normal2 = np.c_[edge2[:, 1], -edge2[:, 0]]
                area2 = _length2D(edge2)
                self._face_areas = np.r_[mkvc(area1), mkvc(area2)]
                self._normals = [_normalize2D(normal1), _normalize2D(normal2)]

            elif self.dim == 3:

                A, E, F, B = index_cube("AEFB", self.vnN, self.vnFx)
                normal1, area1 = face_info(
                    self.gridN, A, E, F, B, average=False, normalizeNormals=False
                )

                A, D, H, E = index_cube("ADHE", self.vnN, self.vnFy)
                normal2, area2 = face_info(
                    self.gridN, A, D, H, E, average=False, normalizeNormals=False
                )

                A, B, C, D = index_cube("ABCD", self.vnN, self.vnFz)
                normal3, area3 = face_info(
                    self.gridN, A, B, C, D, average=False, normalizeNormals=False
                )

                self._face_areas = np.r_[mkvc(area1), mkvc(area2), mkvc(area3)]
                self._normals = [normal1, normal2, normal3]
        return self._face_areas

    @property
    def face_normals(self):
        """
        Face normals: calling this will average
        the computed normals so that there is one
        per face. This is especially relevant in
        3D, as there are up to 4 different normals
        for each face that will be different.

        To reshape the normals into a matrix and get the y component::

            NyX, NyY, NyZ = M.reshape(M.face_normals, 'F', 'Fy', 'M')
        """

        if getattr(self, "_normals", None) is None:
            self.face_areas  # calling .face_areas will create the face normals
        if self.dim == 2:
            return _normalize2D(np.r_[self._normals[0], self._normals[1]])
        elif self.dim == 3:
            normal1 = (
                self._normals[0][0]
                + self._normals[0][1]
                + self._normals[0][2]
                + self._normals[0][3]
            ) / 4
            normal2 = (
                self._normals[1][0]
                + self._normals[1][1]
                + self._normals[1][2]
                + self._normals[1][3]
            ) / 4
            normal3 = (
                self._normals[2][0]
                + self._normals[2][1]
                + self._normals[2][2]
                + self._normals[2][3]
            ) / 4
            return _normalize3D(np.r_[normal1, normal2, normal3])

    @property
    def edge_lengths(self):
        """Edge lengths"""
        if getattr(self, "_edge_lengths", None) is None:
            if self.dim == 2:
                xy = self.gridN
                A, D = index_cube("AD", self.vnN, self.vnEx)
                edge1 = xy[D, :] - xy[A, :]
                A, B = index_cube("AB", self.vnN, self.vnEy)
                edge2 = xy[B, :] - xy[A, :]
                self._edge_lengths = np.r_[mkvc(_length2D(edge1)), mkvc(_length2D(edge2))]
                self._edge_tangents = np.r_[edge1, edge2] / np.c_[self._edge_lengths, self._edge_lengths]
            elif self.dim == 3:
                xyz = self.gridN
                A, D = index_cube("AD", self.vnN, self.vnEx)
                edge1 = xyz[D, :] - xyz[A, :]
                A, B = index_cube("AB", self.vnN, self.vnEy)
                edge2 = xyz[B, :] - xyz[A, :]
                A, E = index_cube("AE", self.vnN, self.vnEz)
                edge3 = xyz[E, :] - xyz[A, :]
                self._edge_lengths = np.r_[
                    mkvc(_length3D(edge1)),
                    mkvc(_length3D(edge2)),
                    mkvc(_length3D(edge3)),
                ]
                self._edge_tangents = (
                    np.r_[edge1, edge2, edge3]
                    / np.c_[self._edge_lengths, self._edge_lengths, self._edge_lengths]
                )
        return self._edge_lengths

    @property
    def edge_tangents(self):
        """Edge tangents"""
        if getattr(self, "_edge_tangents", None) is None:
            self.edge_lengths  # calling .edge_lengths will create the tangents
        return self._edge_tangents

    # DEPRECATIONS
    vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0")
    area = deprecate_property("face_areas", "area", removal_version="1.0.0")
    edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0")
Exemplo n.º 7
0
class BaseMesh(properties.HasProperties, InterfaceMixins):
    """
    BaseMesh does all the counting you don't want to do.
    BaseMesh should be inherited by meshes with a regular structure.
    """

    _REGISTRY = {}
    _aliases = {
        "nC": "n_cells",
        "nN": "n_nodes",
        "nEx": "n_edges_x",
        "nEy": "n_edges_y",
        "nEz": "n_edges_z",
        "nE": "n_edges",
        "vnE": "n_edges_per_direction",
        "nFx": "n_faces_x",
        "nFy": "n_faces_y",
        "nFz": "n_faces_z",
        "nF": "n_faces",
        "vnF": "n_faces_per_direction",
    }

    # Properties
    _n = properties.Tuple(
        "Tuple of number of cells in each direction (dim, )",
        prop=properties.Integer("Number of cells along a particular direction",
                                cast=True,
                                min=1),
        min_length=1,
        max_length=3,
        coerce=True,
        required=True,
    )

    origin = properties.Array(
        "origin of the mesh (dim, )",
        dtype=(float, int),
        shape=("*", ),
        required=True,
    )

    # Instantiate the class
    def __init__(self, n=None, origin=None, **kwargs):
        if n is not None:
            self._n = n  # number of dimensions

        if "x0" in kwargs:
            origin = kwargs.pop('x0')
        if origin is None:
            self.origin = np.zeros(len(self._n))
        else:
            self.origin = origin

        super(BaseMesh, self).__init__(**kwargs)

    def __getattr__(self, name):
        if name == "_aliases":
            raise AttributeError  # http://nedbatchelder.com/blog/201010/surprising_getattr_recursion.html
        name = self._aliases.get(name, name)
        return object.__getattribute__(self, name)

    @property
    def x0(self):
        return self.origin

    @x0.setter
    def x0(self, val):
        self.origin = val

    @classmethod
    def deserialize(cls, value, **kwargs):
        if "x0" in value:
            value["origin"] = value.pop("x0")
        return super().deserialize(value, **kwargs)

    # Validators
    @properties.validator("_n")
    def _check_n_shape(self, change):
        if change["previous"] != properties.undefined:
            # _n can only be set once
            if change["previous"] != change["value"]:
                raise AttributeError(
                    "Cannot change n. Instead, create a new mesh")
        else:
            # check that if h has been set, sizes still agree
            if getattr(self, "h", None) is not None and len(self.h) > 0:
                for i in range(len(change["value"])):
                    if len(self.h[i]) != change["value"][i]:
                        raise properties.ValidationError(
                            "Mismatched shape of n. Expected {}, len(h[{}]), got "
                            "{}".format(len(self.h[i]), i, change["value"][i]))

            # check that if nodes have been set for curvi mesh, sizes still
            # agree
            if getattr(self, "node_list",
                       None) is not None and len(self.node_list) > 0:
                for i in range(len(change["value"])):
                    if self.node_list[0].shape[i] - 1 != change["value"][i]:
                        raise properties.ValidationError(
                            "Mismatched shape of n. Expected {}, len(node_list[{}]), "
                            "got {}".format(self.node_list[0].shape[i] - 1, i,
                                            change["value"][i]))

    @properties.validator("origin")
    def _check_origin(self, change):
        if not (not isinstance(change["value"], properties.utils.Sentinel)
                and change["value"] is not None):
            raise Exception("n must be set prior to setting origin")

        if len(self._n) != len(change["value"]):
            raise Exception(
                "Dimension mismatch. origin has length {} != len(n) which is "
                "{}".format(len(self.origin), len(self._n)))

    @property
    def dim(self):
        """The dimension of the mesh (1, 2, or 3).

        Returns
        -------
        int
            dimension of the mesh
        """
        return len(self._n)

    @property
    def n_cells(self):
        """Total number of cells in the mesh.

        Returns
        -------
        int
            number of cells in the mesh

        Notes
        -----
        Also accessible as `nC`.

        Examples
        --------
        >>> import discretize
        >>> import numpy as np
        >>> import matplotlib.pyplot as plt
        >>> mesh = discretize.TensorMesh([np.ones(n) for n in [2,3]])
        >>> mesh.plot_grid(centers=True, show_it=True)
        >>> print(mesh.n_cells)
        """
        return int(np.prod(self._n))

    def __len__(self):
        """The number of cells on the mesh."""
        return self.n_cells

    @property
    def n_nodes(self):
        """Total number of nodes

        Returns
        -------
        int
            number of nodes in the mesh

        Notes
        -----
        Also accessible as `nN`.

        Examples
        --------
        >>> import discretize
        >>> import numpy as np
        >>> import matplotlib.pyplot as plt
        >>> mesh = discretize.TensorMesh([np.ones(n) for n in [2,3]])
        >>> mesh.plot_grid(nodes=True, show_it=True)
        >>> print(mesh.n_nodes)
        """
        return int(np.prod(x + 1 for x in self._n))

    @property
    def n_edges_x(self):
        """Number of x-edges

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nEx`.

        """
        return int(np.prod(x + y for x, y in zip(self._n, (0, 1, 1))))

    @property
    def n_edges_y(self):
        """Number of y-edges

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nEy`.

        """
        if self.dim < 2:
            return None
        return int(np.prod(x + y for x, y in zip(self._n, (1, 0, 1))))

    @property
    def n_edges_z(self):
        """Number of z-edges

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nEz`.

        """
        if self.dim < 3:
            return None
        return int(np.prod(x + y for x, y in zip(self._n, (1, 1, 0))))

    @property
    def n_edges_per_direction(self):
        """The number of edges in each direction

        Returns
        -------
        n_edges_per_direction : tuple
            [n_edges_x, n_edges_y, n_edges_z], (dim, )

        Notes
        -----
        Also accessible as `vnE`.

        Examples
        --------
        >>> import discretize
        >>> import matplotlib.pyplot as plt
        >>> import numpy as np
        >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]])
        >>> M.plot_grid(edges=True, show_it=True)
        """
        return tuple(x
                     for x in [self.n_edges_x, self.n_edges_y, self.n_edges_z]
                     if x is not None)

    @property
    def n_edges(self):
        """Total number of edges.

        Returns
        -------
        int
            sum([n_edges_x, n_edges_y, n_edges_z])

        Notes
        -----
        Also accessible as `nE`.

        """
        n = self.n_edges_x
        if self.dim > 1:
            n += self.n_edges_y
        if self.dim > 2:
            n += self.n_edges_z
        return n

    @property
    def n_faces_x(self):
        """Number of x-faces

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nFx`.
        """
        return int(np.prod(x + y for x, y in zip(self._n, (1, 0, 0))))

    @property
    def n_faces_y(self):
        """Number of y-faces

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nFy`.
        """
        if self.dim < 2:
            return None
        return int(np.prod(x + y for x, y in zip(self._n, (0, 1, 0))))

    @property
    def n_faces_z(self):
        """Number of z-faces

        Returns
        -------
        int

        Notes
        -----
        Also accessible as `nFz`.
        """
        if self.dim < 3:
            return None
        return int(np.prod(x + y for x, y in zip(self._n, (0, 0, 1))))

    @property
    def n_faces_per_direction(self):
        """The number of faces in each direction

        Returns
        -------
        n_faces_per_direction : tuple
            [n_faces_x, n_faces_y, n_faces_z], (dim, )

        Notes
        -----
        Also accessible as `vnF`.

        Examples
        --------
        >>> import discretize
        >>> import numpy as np
        >>> import matplotlib.pyplot as plt
        >>> M = discretize.TensorMesh([np.ones(n) for n in [2,3]])
        >>> M.plot_grid(faces=True, show_it=True)
        """
        return tuple(x
                     for x in [self.n_faces_x, self.n_faces_y, self.n_faces_z]
                     if x is not None)

    @property
    def n_faces(self):
        """Total number of faces.

        Returns
        -------
        int
            sum([n_faces_x, n_faces_y, n_faces_z])

        Notes
        -----
        Also accessible as `nF`.

        """
        n = self.n_faces_x
        if self.dim > 1:
            n += self.n_faces_y
        if self.dim > 2:
            n += self.n_faces_z
        return n

    @property
    def face_normals(self):
        """Face Normals

        Returns
        -------
        numpy.ndarray
            normals, (n_faces, dim)
        """
        if self.dim == 2:
            nX = np.c_[np.ones(self.n_faces_x), np.zeros(self.n_faces_x)]
            nY = np.c_[np.zeros(self.n_faces_y), np.ones(self.n_faces_y)]
            return np.r_[nX, nY]
        elif self.dim == 3:
            nX = np.c_[np.ones(self.n_faces_x),
                       np.zeros(self.n_faces_x),
                       np.zeros(self.n_faces_x), ]
            nY = np.c_[np.zeros(self.n_faces_y),
                       np.ones(self.n_faces_y),
                       np.zeros(self.n_faces_y), ]
            nZ = np.c_[np.zeros(self.n_faces_z),
                       np.zeros(self.n_faces_z),
                       np.ones(self.n_faces_z), ]
            return np.r_[nX, nY, nZ]

    @property
    def edge_tangents(self):
        """Edge Tangents

        Returns
        -------
        numpy.ndarray
            normals, (n_edges, dim)
        """
        if self.dim == 2:
            tX = np.c_[np.ones(self.n_edges_x), np.zeros(self.n_edges_x)]
            tY = np.c_[np.zeros(self.n_edges_y), np.ones(self.n_edges_y)]
            return np.r_[tX, tY]
        elif self.dim == 3:
            tX = np.c_[np.ones(self.n_edges_x),
                       np.zeros(self.n_edges_x),
                       np.zeros(self.n_edges_x), ]
            tY = np.c_[np.zeros(self.n_edges_y),
                       np.ones(self.n_edges_y),
                       np.zeros(self.n_edges_y), ]
            tZ = np.c_[np.zeros(self.n_edges_z),
                       np.zeros(self.n_edges_z),
                       np.ones(self.n_edges_z), ]
            return np.r_[tX, tY, tZ]

    def project_face_vector(self, face_vector):
        """Project vectors onto the faces of the mesh.

        Given a vector, face_vector, in cartesian coordinates, this will project
        it onto the mesh using the normals

        Parameters
        ----------
        face_vector : numpy.ndarray
            face vector with shape (n_faces, dim)

        Returns
        -------
        numpy.ndarray
            projected face vector, (n_faces, )

        """
        if not isinstance(face_vector, np.ndarray):
            raise Exception("face_vector must be an ndarray")
        if not (len(face_vector.shape) == 2 and face_vector.shape[0]
                == self.n_faces and face_vector.shape[1] == self.dim):
            raise Exception(
                "face_vector must be an ndarray of shape (n_faces x dim)")
        return np.sum(face_vector * self.face_normals, 1)

    def project_edge_vector(self, edge_vector):
        """Project vectors onto the edges of the mesh

        Given a vector, edge_vector, in cartesian coordinates, this will project
        it onto the mesh using the tangents

        Parameters
        ----------
        edge_vector : numpy.ndarray
            edge vector with shape (n_edges, dim)

        Returns
        -------
        numpy.ndarray
            projected edge vector, (n_edges, )

        """
        if not isinstance(edge_vector, np.ndarray):
            raise Exception("edge_vector must be an ndarray")
        if not (len(edge_vector.shape) == 2 and edge_vector.shape[0]
                == self.n_edges and edge_vector.shape[1] == self.dim):
            raise Exception(
                "edge_vector must be an ndarray of shape (nE x dim)")
        return np.sum(edge_vector * self.edge_tangents, 1)

    def save(self, file_name="mesh.json", verbose=False, **kwargs):
        """
        Save the mesh to json
        :param str file: file_name for saving the casing properties
        :param str directory: working directory for saving the file
        """

        if 'filename' in kwargs:
            file_name = kwargs['filename']
            warnings.warn(
                "The filename keyword argument has been deprecated, please use file_name. "
                "This will be removed in discretize 1.0.0",
                DeprecationWarning,
            )
        f = os.path.abspath(
            file_name)  # make sure we are working with abs path
        with open(f, "w") as outfile:
            json.dump(self.serialize(), outfile)

        if verbose:
            print("Saved {}".format(f))

        return f

    def copy(self):
        """
        Make a copy of the current mesh
        """
        return properties.copy(self)

    axis_u = properties.Vector3(
        "Vector orientation of u-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.",
        default="X",
        length=1,
    )
    axis_v = properties.Vector3(
        "Vector orientation of v-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.",
        default="Y",
        length=1,
    )
    axis_w = properties.Vector3(
        "Vector orientation of w-direction. For more details see the docs for the :attr:`~discretize.base.BaseMesh.rotation_matrix` property.",
        default="Z",
        length=1,
    )

    @properties.validator
    def _validate_orientation(self):
        """Check if axes are orthogonal"""
        tol = 1E-6
        if not (np.abs(self.axis_u.dot(self.axis_v) < tol)
                and np.abs(self.axis_v.dot(self.axis_w) < tol)
                and np.abs(self.axis_w.dot(self.axis_u) < tol)):
            raise ValueError("axis_u, axis_v, and axis_w must be orthogonal")
        return True

    @property
    def reference_is_rotated(self):
        """True if the axes are rotated from the traditional <X,Y,Z> system
        with vectors of :math:`(1,0,0)`, :math:`(0,1,0)`, and :math:`(0,0,1)`
        """
        if (np.allclose(self.axis_u,
                        (1, 0, 0)) and np.allclose(self.axis_v, (0, 1, 0))
                and np.allclose(self.axis_w, (0, 0, 1))):
            return False
        return True

    @property
    def rotation_matrix(self):
        """Builds a rotation matrix to transform coordinates from their coordinate
        system into a conventional cartesian system. This is built off of the
        three `axis_u`, `axis_v`, and `axis_w` properties; these mapping
        coordinates use the letters U, V, and W (the three letters preceding X,
        Y, and Z in the alphabet) to define the projection of the X, Y, and Z
        durections. These UVW vectors describe the placement and transformation
        of the mesh's coordinate sytem assuming at most 3 directions.

        Why would you want to use these UVW mapping vectors the this
        `rotation_matrix` property? They allow us to define the realationship
        between local and global coordinate systems and provide a tool for
        switching between the two while still maintaing the connectivity of the
        mesh's cells. For a visual example of this, please see the figure in the
        docs for the :class:`~discretize.mixins.vtk_mod.InterfaceVTK`.
        """
        return np.array([self.axis_u, self.axis_v, self.axis_w])

    reference_system = properties.String(
        "The type of coordinate reference frame. Can take on the values " +
        "cartesian, cylindrical, or spherical. Abbreviations of these are allowed.",
        default="cartesian",
        change_case="lower",
    )

    @properties.validator
    def _validate_reference_system(self):
        """Check if the reference system is of a known type."""
        choices = ["cartesian", "cylindrical", "spherical"]
        # Here are a few abbreviations that users can harnes
        abrevs = {
            "car": choices[0],
            "cart": choices[0],
            "cy": choices[1],
            "cyl": choices[1],
            "sph": choices[2],
        }
        # Get the name and fix it if it is abbreviated
        self.reference_system = abrevs.get(self.reference_system,
                                           self.reference_system)
        if self.reference_system not in choices:
            raise ValueError("Coordinate system ({}) unknown.".format(
                self.reference_system))
        return True

    def _parse_location_type(self, location_type):
        if len(location_type) == 0:
            return location_type
        elif location_type[0] == "F":
            if len(location_type) > 1:
                return "faces_" + location_type[-1]
            else:
                return "faces"
        elif location_type[0] == "E":
            if len(location_type) > 1:
                return "edges_" + location_type[-1]
            else:
                return "edges"
        elif location_type[0] == "N":
            return "nodes"
        elif location_type[0] == "C":
            if len(location_type) > 2:
                return "cell_centers_" + location_type[-1]
            else:
                return "cell_centers"
        else:
            return location_type

    # DEPRECATED
    normals = deprecate_property("face_normals",
                                 "normals",
                                 removal_version="1.0.0")
    tangents = deprecate_property("edge_tangents",
                                  "tangents",
                                  removal_version="1.0.0")
    projectEdgeVector = deprecate_method("project_edge_vector",
                                         "projectEdgeVector",
                                         removal_version="1.0.0")
    projectFaceVector = deprecate_method("project_face_vector",
                                         "projectFaceVector",
                                         removal_version="1.0.0")
Exemplo n.º 8
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")
Exemplo n.º 9
0
class TensorMesh(
    BaseTensorMesh, BaseRectangularMesh, DiffOperators, InnerProducts, TensorMeshIO
):
    """
    TensorMesh is a mesh class that deals with tensor product meshes.

    Any Mesh that has a constant width along the entire axis
    such that it can defined by a single width vector, called 'h'.

    .. plot::
        :include-source:

        import discretize

        hx = np.array([1, 1, 1])
        hy = np.array([1, 2])
        hz = np.array([1, 1, 1, 1])

        mesh = discretize.TensorMesh([hx, hy, hz])
        mesh.plot_grid()


    Example of a padded tensor mesh using
    :func:`discretize.utils.unpack_widths`:

    .. plot::
        :include-source:

        import discretize
        mesh = discretize.TensorMesh([
            [(10, 10, -1.3), (10, 40), (10, 10, 1.3)],
            [(10, 10, -1.3), (10, 20)]
        ])
        mesh.plot_grid()

    For a quick tensor mesh on a (10x12x15) unit cube

    .. code:: python

        import discretize
        mesh = discretize.TensorMesh([10, 12, 15])

    """

    _meshType = "TENSOR"
    _aliases = {
        **DiffOperators._aliases,
        **BaseRectangularMesh._aliases,
        **BaseTensorMesh._aliases,
    }

    def __init__(self, h=None, origin=None, **kwargs):
        BaseTensorMesh.__init__(self, h=h, origin=origin, **kwargs)

    def __repr__(self):
        """Plain text representation."""
        fmt = "\n  {}: {:,} cells\n\n".format(type(self).__name__, self.nC)
        fmt += 22 * " " + "MESH EXTENT" + 13 * " " + "CELL WIDTH      FACTOR\n"
        fmt += "  dir    nC        min           max         min       max "
        fmt += "     max\n  ---   ---  " + 27 * "-" + "  " + 18 * "-" + "  ------\n"

        # Get attributes and put into table.
        attrs = self._repr_attributes()
        for i in range(self.dim):
            name = attrs["names"][i]
            iattr = attrs[name]
            fmt += "   {}".format(name)
            fmt += " {:6}".format(iattr["nC"])
            for p in ["min", "max"]:
                fmt += " {:13,.2f}".format(iattr[p])
            for p in ["h_min", "h_max"]:
                fmt += " {:9,.2f}".format(iattr[p])
            fmt += "{:8,.2f}".format(iattr["max_fact"])
            fmt += "\n"  # End row

        fmt += "\n"
        return fmt

    def _repr_html_(self):
        """HTML representation."""
        style = " style='padding: 5px 20px 5px 20px;'"

        fmt = "<table>\n"
        fmt += "  <tr>\n"
        fmt += "    <td style='font-weight: bold; font-size: 1.2em; text-align"
        fmt += ": center;' colspan='3'>{}</td>\n".format(type(self).__name__)
        fmt += "    <td style='font-size: 1.2em; text-align: center;'"
        fmt += "colspan='4'>{:,} cells</td>\n".format(self.nC)
        fmt += "  </tr>\n"

        fmt += "  <tr>\n"
        fmt += "    <th></th>\n"
        fmt += "    <th></th>\n"
        fmt += "    <th colspan='2'" + style + ">MESH EXTENT</th>\n"
        fmt += "    <th colspan='2'" + style + ">CELL WIDTH</th>\n"
        fmt += "    <th" + style + ">FACTOR</th>\n"
        fmt += "  </tr>\n"

        fmt += "  <tr>\n"
        fmt += "    <th" + style + ">dir</th>\n"
        fmt += "    <th" + style + ">nC</th>\n"
        fmt += "    <th" + style + ">min</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "    <th" + style + ">min</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "    <th" + style + ">max</th>\n"
        fmt += "  </tr>\n"

        # Get attributes and put into table.
        attrs = self._repr_attributes()
        for i in range(self.dim):
            name = attrs["names"][i]
            iattr = attrs[name]
            fmt += "  <tr>\n"  # Start row
            fmt += "    <td" + style + ">{}</td>\n".format(name)
            fmt += "    <td" + style + ">{}</td>\n".format(iattr["nC"])
            for p in ["min", "max", "h_min", "h_max", "max_fact"]:
                fmt += "    <td" + style + ">{:,.2f}</td>\n".format(iattr[p])
            fmt += "  </tr>\n"  # End row

        fmt += "</table>\n"
        return fmt

    # --------------- Geometries ---------------------
    @property
    def cell_volumes(self):
        """Construct cell volumes of the 3D model as 1d array."""
        if getattr(self, "_cell_volumes", None) is None:
            vh = self.h
            # Compute cell volumes
            if self.dim == 1:
                self._cell_volumes = mkvc(vh[0])
            elif self.dim == 2:
                # Cell sizes in each direction
                self._cell_volumes = mkvc(np.outer(vh[0], vh[1]))
            elif self.dim == 3:
                # Cell sizes in each direction
                self._cell_volumes = mkvc(np.outer(mkvc(np.outer(vh[0], vh[1])), vh[2]))
        return self._cell_volumes

    @property
    def face_x_areas(self):
        """
        Area of the x-faces
        """
        if getattr(self, "_face_x_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1:
                areaFx = np.ones(n[0] + 1)
            elif self.dim == 2:
                areaFx = np.outer(np.ones(n[0] + 1), vh[1])
            elif self.dim == 3:
                areaFx = np.outer(np.ones(n[0] + 1), mkvc(np.outer(vh[1], vh[2])))
            self._face_x_areas = mkvc(areaFx)
        return self._face_x_areas

    @property
    def face_y_areas(self):
        """
        Area of the y-faces
        """
        if getattr(self, "_face_y_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1:
                raise Exception("1D meshes do not have y-Faces")
            elif self.dim == 2:
                areaFy = np.outer(vh[0], np.ones(n[1] + 1))
            elif self.dim == 3:
                areaFy = np.outer(vh[0], mkvc(np.outer(np.ones(n[1] + 1), vh[2])))
            self._face_y_areas = mkvc(areaFy)
        return self._face_y_areas

    @property
    def face_z_areas(self):
        """
        Area of the z-faces
        """
        if getattr(self, "_face_z_areas", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute areas of cell faces
            if self.dim == 1 or self.dim == 2:
                raise Exception("{}D meshes do not have z-Faces".format(self.dim))
            elif self.dim == 3:
                areaFz = np.outer(vh[0], mkvc(np.outer(vh[1], np.ones(n[2] + 1))))
            self._face_z_areas = mkvc(areaFz)
        return self._face_z_areas

    @property
    def face_areas(self):
        """Construct face areas of the 3D model as 1d array."""
        if self.dim == 1:
            return self.face_x_areas
        elif self.dim == 2:
            return np.r_[self.face_x_areas, self.face_y_areas]
        elif self.dim == 3:
            return np.r_[self.face_x_areas, self.face_y_areas, self.face_z_areas]

    @property
    def edge_x_lengths(self):
        """x-edge lengths"""
        if getattr(self, "_edge_x_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1:
                edgeEx = vh[0]
            elif self.dim == 2:
                edgeEx = np.outer(vh[0], np.ones(n[1] + 1))
            elif self.dim == 3:
                edgeEx = np.outer(
                    vh[0], mkvc(np.outer(np.ones(n[1] + 1), np.ones(n[2] + 1)))
                )
            self._edge_x_lengths = mkvc(edgeEx)
        return self._edge_x_lengths

    @property
    def edge_y_lengths(self):
        """y-edge lengths"""
        if getattr(self, "_edge_y_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1:
                raise Exception("1D meshes do not have y-edges")
            elif self.dim == 2:
                edgeEy = np.outer(np.ones(n[0] + 1), vh[1])
            elif self.dim == 3:
                edgeEy = np.outer(
                    np.ones(n[0] + 1), mkvc(np.outer(vh[1], np.ones(n[2] + 1)))
                )
            self._edge_y_lengths = mkvc(edgeEy)
        return self._edge_y_lengths

    @property
    def edge_z_lengths(self):
        """z-edge lengths"""
        if getattr(self, "_edge_z_lengths", None) is None:
            # Ensure that we are working with column vectors
            vh = self.h
            # The number of cell centers in each direction
            n = self.vnC
            # Compute edge lengths
            if self.dim == 1 or self.dim == 2:
                raise Exception("{}D meshes do not have y-edges".format(self.dim))
            elif self.dim == 3:
                edgeEz = np.outer(
                    np.ones(n[0] + 1), mkvc(np.outer(np.ones(n[1] + 1), vh[2]))
                )
            self._edge_z_lengths = mkvc(edgeEz)
        return self._edge_z_lengths

    @property
    def edge_lengths(self):
        """Construct edge legnths of the 3D model as 1d array."""
        if self.dim == 1:
            return self.edge_x_lengths
        elif self.dim == 2:
            return np.r_[self.edge_x_lengths, self.edge_y_lengths]
        elif self.dim == 3:
            return np.r_[self.edge_x_lengths, self.edge_y_lengths, self.edge_z_lengths]
        return self._edge

    @property
    def face_boundary_indices(self):
        """
        Find indices of boundary faces in each direction
        """
        if self.dim == 1:
            indxd = self.gridFx == min(self.gridFx)
            indxu = self.gridFx == max(self.gridFx)
            return indxd, indxu
        elif self.dim == 2:
            indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0])
            indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0])
            indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1])
            indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1])
            return indxd, indxu, indyd, indyu
        elif self.dim == 3:
            indxd = self.gridFx[:, 0] == min(self.gridFx[:, 0])
            indxu = self.gridFx[:, 0] == max(self.gridFx[:, 0])
            indyd = self.gridFy[:, 1] == min(self.gridFy[:, 1])
            indyu = self.gridFy[:, 1] == max(self.gridFy[:, 1])
            indzd = self.gridFz[:, 2] == min(self.gridFz[:, 2])
            indzu = self.gridFz[:, 2] == max(self.gridFz[:, 2])
            return indxd, indxu, indyd, indyu, indzd, indzu

    @property
    def cell_boundary_indices(self):
        """
        Find indices of boundary faces in each direction
        """
        if self.dim == 1:
            indxd = self.gridCC == min(self.gridCC)
            indxu = self.gridCC == max(self.gridCC)
            return indxd, indxu
        elif self.dim == 2:
            indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0])
            indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0])
            indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1])
            indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1])
            return indxd, indxu, indyd, indyu
        elif self.dim == 3:
            indxd = self.gridCC[:, 0] == min(self.gridCC[:, 0])
            indxu = self.gridCC[:, 0] == max(self.gridCC[:, 0])
            indyd = self.gridCC[:, 1] == min(self.gridCC[:, 1])
            indyu = self.gridCC[:, 1] == max(self.gridCC[:, 1])
            indzd = self.gridCC[:, 2] == min(self.gridCC[:, 2])
            indzu = self.gridCC[:, 2] == max(self.gridCC[:, 2])
            return indxd, indxu, indyd, indyu, indzd, indzu

    def _repr_attributes(self):
        """Attributes for the representation of the mesh."""

        attrs = {}
        attrs["names"] = ["x", "y", "z"][: self.dim]

        # Loop over dimensions.
        for i in range(self.dim):
            name = attrs["names"][i]  # Name of this dimension
            attrs[name] = {}

            # Get min/max node.
            n_vector = getattr(self, "nodes_" + name)
            attrs[name]["min"] = np.nanmin(n_vector)
            attrs[name]["max"] = np.nanmax(n_vector)

            # Get min/max cell width.
            h_vector = self.h[i]
            attrs[name]["h_min"] = np.nanmin(h_vector)
            attrs[name]["h_max"] = np.nanmax(h_vector)

            # Get max stretching factor.
            if len(h_vector) < 2:
                attrs[name]["max_fact"] = 1.0
            else:
                attrs[name]["max_fact"] = np.nanmax(
                    np.r_[h_vector[:-1] / h_vector[1:], h_vector[1:] / h_vector[:-1]]
                )

            # Add number of cells.
            attrs[name]["nC"] = self.shape_cells[i]

        return attrs

    # DEPRECATIONS
    vol = deprecate_property("cell_volumes", "vol", removal_version="1.0.0")
    areaFx = deprecate_property("face_x_areas", "areaFx", removal_version="1.0.0")
    areaFy = deprecate_property("face_y_areas", "areaFy", removal_version="1.0.0")
    areaFz = deprecate_property("face_z_areas", "areaFz", removal_version="1.0.0")
    area = deprecate_property("face_areas", "area", removal_version="1.0.0")
    edgeEx = deprecate_property("edge_x_lengths", "edgeEx", removal_version="1.0.0")
    edgeEy = deprecate_property("edge_y_lengths", "edgeEy", removal_version="1.0.0")
    edgeEz = deprecate_property("edge_z_lengths", "edgeEz", removal_version="1.0.0")
    edge = deprecate_property("edge_lengths", "edge", removal_version="1.0.0")
    faceBoundaryInd = deprecate_property(
        "face_boundary_indices", "faceBoundaryInd", removal_version="1.0.0"
    )
    cellBoundaryInd = deprecate_property(
        "cell_boundary_indices", "cellBoundaryInd", removal_version="1.0.0"
    )