Пример #1
0
def nd_sides_shearzone_injection_cell(
    params: FlowParameters, gb: pp.GridBucket, reset_frac_tags: bool = True,
) -> None:
    """ Tag the Nd cells surrounding a shear zone injection point

    Parameters
    ----------
    params : FlowParameters
        parameters that contain "source_scalar_borehole_shearzone"
        (with "shearzone", and "borehole") and "length_scale".
    gb : pp.GridBucket
        grid bucket
    reset_frac_tags : bool [Default: True]
        if set to False, keep injection tag in the shear zone.
    """
    # Shorthand
    shearzone = params.source_scalar_borehole_shearzone.get("shearzone")

    # First, tag the fracture cell, and get the tag
    shearzone_injection_cell(params, gb)
    fracture = gb.get_grids(lambda g: gb.node_props(g, "name") == shearzone)[0]
    tags = fracture.tags["well_cells"]
    # Second, map the cell to the Nd grid
    nd_grid: pp.Grid = gb.grids_of_dimension(gb.dim_max())[0]
    data_edge = gb.edge_props((fracture, nd_grid))
    mg: pp.MortarGrid = data_edge["mortar_grid"]

    slave_to_master_face = mg.mortar_to_master_int() * mg.slave_to_mortar_int()
    face_to_cell = nd_grid.cell_faces.T
    slave_to_master_cell = face_to_cell * slave_to_master_face
    nd_tags = np.abs(slave_to_master_cell) * tags

    # Set tags on the nd-grid
    nd_grid.tags["well_cells"] = nd_tags
    ndd = gb.node_props(nd_grid)
    pp.set_state(ndd, {"well": tags})

    if reset_frac_tags:
        # reset tags on the fracture
        zeros = np.zeros(fracture.num_cells)
        fracture.tags["well_cells"] = zeros
        d = gb.node_props(fracture)
        pp.set_state(d, {"well": zeros})
Пример #2
0
def _split_fracture_extension(
    bucket: pp.GridBucket,
    g_h: pp.Grid,
    g_l: pp.Grid,
    faces_h: np.ndarray,
    nodes_h: np.ndarray,
    cells_l: np.ndarray,
    non_planar: bool = False,
):
    """
    Split the higher-dimensional grid along specified faces. Updates made to
    face_cells of the grid pair and the nodes and faces of the higher-
    dimensional grid.
    Parameters
    ----------
    bucket      - A grid bucket
    g_h          - Higher-dimensional grid to be split along specified faces.
    g_l          - Immersed lower-dimensional grid.
    faces_h     - The higher-dimensional faces to be split.
    cells_l     - The corresponding lower-dimensional cells.
    nodes_h     - The corresponding (hig_her-dimensional) nodes.

    """
    # IMPLEMENTATION NOTE: Part of the following code is likely more general than
    # necessary considering assumptions made before we reach this point - e.g.
    # assumptions in propagate_fractures() and other subfunctions. Specifically,
    # it is unlikely the code will be called with g_h.dim != bucket.dim_max().

    # We are splitting faces in g_h. This affects all the immersed fractures,
    # as face_cells has to be extended for the new faces_h.
    neigh = np.array(bucket.node_neighbors(g_h))

    # Find the neighbours that are lower dimensional
    is_low_dim_grid = np.where([w.dim < g_h.dim for w in neigh])
    low_dim_neigh = neigh[is_low_dim_grid]
    edges = [(g_h, w) for w in low_dim_neigh]
    g_l_ind = np.nonzero(low_dim_neigh == g_l)[0]
    if len(edges) == 0:
        # No lower dim grid. Nothing to do.
        warnings.warn("Unexpected neighbourless g_h in fracture propagation")
        return

    face_cell_list: List[sps.spmatrix] = [
        bucket.edge_props(e, "face_cells") for e in edges
    ]

    # We split all the faces that are connected to faces_h
    # The new faces will share the same nodes and properties (normals,
    # etc.)
    face_cell_list = pp.fracs.split_grid.split_specific_faces(
        g_h, face_cell_list, faces_h, cells_l, g_l_ind, non_planar)

    # Replace the face-cell relation on the GridBucket edge
    for e, f in zip(edges, face_cell_list):
        bucket.edge_props(e)["face_cells"] = f

    # We now find which lower-dim nodes correspond to which higher-
    # dim nodes. We split these nodes according to the topology of
    # the connected higher-dim cells. At a X-intersection we split
    # the node into four, while at the fracture boundary it is not split.
    pp.fracs.split_grid.split_nodes(g_h, [g_l], [nodes_h])

    # Remove zeros from cell_faces
    for g, _ in bucket:
        g.cell_faces.eliminate_zeros()
Пример #3
0
def set_projections(
        gb: pp.GridBucket,
        edges: Optional[List[Tuple[pp.Grid, pp.Grid]]] = None) -> None:
    """Define a local coordinate system, and projection matrices, for all
    grids of co-dimension 1.

    The function adds one item to the data dictionary of all GridBucket edges
    that neighbors a co-dimension 1 grid, defined as:
        key: tangential_normal_projection, value: pp.TangentialNormalProjection
            provides projection to the surface of the lower-dimensional grid

    Note that grids of co-dimension 2 and higher are ignored in this construction,
    as we do not plan to do contact mechanics on these objects.

    It is assumed that the surface is planar.

    """
    if edges is None:
        edges = [e for e, _ in gb.edges()]

    # Information on the vector normal to the surface is not available directly
    # from the surface grid (it could be constructed from the surface geometry,
    # which spans the tangential plane). We instead get the normal vector from
    # the adjacent higher dimensional grid.
    # We therefore access the grids via the edges of the mixed-dimensional grid.
    for e in edges:
        d_m = gb.edge_props(e)

        mg = d_m["mortar_grid"]
        # Only consider edges where the lower-dimensional neighbor is of co-dimension 1
        if not mg.dim == (gb.dim_max() - 1):
            continue

        # Neigboring grids
        g_l, g_h = gb.nodes_of_edge(e)

        # Find faces of the higher dimensional grid that coincide with the mortar
        # grid. Go via the primary to mortar projection
        # Convert matrix to csr, then the relevant face indices are found from
        # the (column) indices
        faces_on_surface = mg.primary_to_mortar_int().tocsr().indices

        # Find out whether the boundary faces have outwards pointing normal vectors
        # Negative sign implies that the normal vector points inwards.
        sgn, _ = g_h.signs_and_cells_of_boundary_faces(faces_on_surface)

        # Unit normal vector
        unit_normal = g_h.face_normals[:g_h.dim] / g_h.face_areas
        # Ensure all normal vectors on the relevant surface points outwards
        unit_normal[:, faces_on_surface] *= sgn

        # Now we need to pick out *one*  normal vector of the higher dimensional grid

        # which coincides with this mortar grid, so we kill off all entries for the
        # "other" side:
        unit_normal[:, mg._ind_face_on_other_side] = 0

        # Project to the mortar and then to the fracture
        outwards_unit_vector_mortar = mg.primary_to_mortar_int().dot(
            unit_normal.T).T
        normal_lower = mg.mortar_to_secondary_int().dot(
            outwards_unit_vector_mortar.T).T

        # NOTE: The normal vector is based on the first cell in the mortar grid,
        # and will be pointing from that cell towards the other side of the
        # mortar grid. This defines the positive direction in the normal direction.
        # Although a simpler implementation seems to be possible, going via the
        # first element in faces_on_surface, there is no guarantee that this will
        # give us a face on the positive (or negative) side, hence the more general
        # approach is preferred.
        #
        # NOTE: The basis for the tangential direction is determined by the
        # construction internally in TangentialNormalProjection.
        projection = pp.TangentialNormalProjection(normal_lower)

        d_l = gb.node_props(g_l)
        # Store the projection operator in the lower-dimensional data
        d_l["tangential_normal_projection"] = projection
Пример #4
0
    def to_ad(
        self,
        gb: pp.GridBucket,
        state: Optional[np.ndarray] = None,
        active_variables: Optional[list] = None,
    ):
        """Evaluate the residual and Jacobian matrix for a given state.

        Parameters:
            gb (pp.GridBucket): GridBucket used to represent the problem. Will be used
                to parse the operators that combine to form this Equation..
            state (np.ndarray, optional): State vector for which the residual and its
                derivatives should be formed. If not provided, the state will be pulled from
                the previous iterate (if this exists), or alternatively from the state
                at the previous time step.

        Returns:
            An Ad-array representation of the residual and Jacbobian.

        """
        # Parsing in two stages: First make an Ad-representation of the variable state
        # (this must be done jointly for all variables of the Equation to get all
        # derivatives represented). Then parse the equation by traversing its
        # tree-representation, and parse and combine individual operators.

        # Initialize variables
        prev_vals = np.zeros(self._dof_manager.num_dofs())

        populate_state = state is None
        if populate_state:
            state = np.zeros(self._dof_manager.num_dofs())

        assert state is not None
        for (g, var) in self._dof_manager.block_dof:
            ind = self._dof_manager.dof_ind(g, var)
            if isinstance(g, tuple):
                prev_vals[ind] = gb.edge_props(g, pp.STATE)[var]
            else:
                prev_vals[ind] = gb.node_props(g, pp.STATE)[var]

            if populate_state:
                if isinstance(g, tuple):
                    try:
                        state[ind] = gb.edge_props(g, pp.STATE)[pp.ITERATE][var]
                    except KeyError:
                        prev_vals[ind] = gb.edge_props(g, pp.STATE)[var]
                else:
                    try:
                        state[ind] = gb.node_props(g, pp.STATE)[pp.ITERATE][var]
                    except KeyError:
                        state[ind] = gb.node_props(g, pp.STATE)[var]

        # Initialize Ad variables with the current iterates
        if active_variables is None:
            ad_vars = initAdArrays([state[ind] for ind in self._variable_dofs])
            self._ad = {var_id: ad for (var_id, ad) in zip(self._variable_ids, ad_vars)}
        else:
            active_variable_ids = [v.id for v in active_variables]

            ad_variable_ids = list(
                set(self._variable_ids).intersection(active_variable_ids)
            )
            assert all([i in self._variable_ids for i in active_variable_ids])
            ad_variable_local_ids = [
                self._variable_ids.index(i) for i in active_variable_ids
            ]
            ad_variable_dofs = [self._variable_dofs[i] for i in ad_variable_local_ids]
            ad_vars = initAdArrays([state[ind] for ind in ad_variable_dofs])
            self._ad = {var_id: ad for (var_id, ad) in zip(ad_variable_ids, ad_vars)}

        # Also make mappings from the previous iteration.
        if active_variables is None:
            prev_iter_vals_list = [state[ind] for ind in self._prev_iter_dofs]
            self._prev_iter_vals = {
                var_id: val
                for (var_id, val) in zip(self._prev_iter_ids, prev_iter_vals_list)
            }
        else:
            # FIXME: This needs explanations
            prev_iter_vals_list = [state[ind] for ind in self._prev_iter_dofs]
            non_ad_variable_ids = list(set(self._variable_ids) - set(ad_variable_ids))
            non_ad_variable_local_ids = [
                self._variable_ids.index(i) for i in non_ad_variable_ids
            ]
            non_ad_variable_dofs = [
                self._variable_dofs[i] for i in non_ad_variable_local_ids
            ]
            non_ad_vals_list = [state[ind] for ind in non_ad_variable_dofs]
            self._prev_iter_vals = {
                var_id: val
                for (var_id, val) in zip(
                    self._prev_iter_ids + non_ad_variable_ids,
                    prev_iter_vals_list + non_ad_vals_list,
                )
            }

        # Also make mappings from the previous time step.
        prev_vals_list = [prev_vals[ind] for ind in self._prev_time_dofs]
        self._prev_vals = {
            var_id: val for (var_id, val) in zip(self._prev_time_ids, prev_vals_list)
        }

        # Parse operators. This is left to a separate function to facilitate the
        # necessary recursion for complex operators.
        eq = self._parse_operator(self._operator, gb)

        return eq
Пример #5
0
    def __init__(
        self,
        gb: pp.GridBucket,
        grids: Optional[List[pp.Grid]] = None,
        edges: Optional[List[Tuple[pp.Grid, pp.Grid]]] = None,
        nd: int = 1,
    ) -> None:
        """Construct mortar projection object.

        The projections will be ordered according to the ordering in grids, or the order
        of the GridBucket iteration over grids. Iit is critical that the same ordering
        is used by other operators.

        Parameters:
            grids (List of pp.Grid, optional): List of grids for which the projections
                should apply. If not provided, all grids in gb will be used. The order
                 of the grids in the list sets the ordering of the subdomain projections.
            gb (pp.GridBucket): Mixed-dimensional grid.
            edges (List of edges, optional): List of edges for which the projections
                should apply. If not provided, all grids in gb will be used. The order
                 of the grids in the list sets the ordering of the subdomain projections.
            nd (int, optional): Dimension of the quantities to be projected.

        """
        grids = _grid_list(grids, gb)
        if edges is None:
            edges = [e for e, _ in gb.edges()]

        self._num_edges: int = len(edges)
        self._nd: int = nd

        ## Initialize projections

        cell_projection, face_projection = _subgrid_projections(
            grids, self._nd)

        # sparse blocks are slow; it should be possible to do a right multiplication
        # of local-to-global mortar indices instead of the block.

        # Data structures for constructing the projection operators
        mortar_to_primary_int, mortar_to_primary_avg = [], []
        primary_to_mortar_int, primary_to_mortar_avg = [], []

        mortar_to_secondary_int, mortar_to_secondary_avg = [], []
        secondary_to_mortar_int, secondary_to_mortar_avg = [], []

        # The goal is to construct global projections between grids and mortar grids.
        # The construction takes two stages, and is different for projections to and
        # from the mortar grid:
        # For projections from the mortar grid, a mapping is first made from local
        # mortar numbering global grid ordering. In the second stage, the mappings from
        # mortar are stacked to make a global mapping.
        # Projections to the mortar grid are made by first defining projections from
        # global grid numbering to local mortar grids, and then stack the latter.

        for e in edges:
            g_primary, g_secondary = e
            mg: pp.MortarGrid = gb.edge_props(e, "mortar_grid")
            if (g_primary.dim != mg.dim + 1) or g_secondary.dim != mg.dim:
                # This will correspond to DD of sorts; we could handle this
                # by using cell_projections for g_primary and/or
                # face_projection for g_secondary, depending on the exact
                # configuration
                raise NotImplementedError("Non-standard interface.")

            # Projections to primary
            mortar_to_primary_int.append(face_projection[g_primary] *
                                         mg.mortar_to_primary_int(nd))
            mortar_to_primary_avg.append(face_projection[g_primary] *
                                         mg.mortar_to_primary_avg(nd))

            # Projections from primary
            primary_to_mortar_int.append(
                mg.primary_to_mortar_int(nd) * face_projection[g_primary].T)
            primary_to_mortar_avg.append(
                mg.primary_to_mortar_avg(nd) * face_projection[g_primary].T)

            mortar_to_secondary_int.append(cell_projection[g_secondary] *
                                           mg.mortar_to_secondary_int(nd))
            mortar_to_secondary_avg.append(cell_projection[g_secondary] *
                                           mg.mortar_to_secondary_avg(nd))

            secondary_to_mortar_int.append(
                mg.secondary_to_mortar_int(nd) *
                cell_projection[g_secondary].T)
            secondary_to_mortar_avg.append(
                mg.secondary_to_mortar_avg(nd) *
                cell_projection[g_secondary].T)

        # Stack mappings from the mortar horizontally.
        # The projections are wrapped by a pp.ad.Matrix to be compatible with the
        # requirements for processing of Ad operators.
        self.mortar_to_primary_int = Matrix(
            sps.bmat([mortar_to_primary_int]).tocsr())
        self.mortar_to_primary_avg = Matrix(
            sps.bmat([mortar_to_primary_avg]).tocsr())
        self.mortar_to_secondary_int = Matrix(
            sps.bmat([mortar_to_secondary_int]).tocsr())
        self.mortar_to_secondary_avg = Matrix(
            sps.bmat([mortar_to_secondary_avg]).tocsr())

        # Vertical stacking of the projections
        self.primary_to_mortar_int = Matrix(
            sps.bmat([[m] for m in primary_to_mortar_int]).tocsr())
        self.primary_to_mortar_avg = Matrix(
            sps.bmat([[m] for m in primary_to_mortar_avg]).tocsr())
        self.secondary_to_mortar_int = Matrix(
            sps.bmat([[m] for m in secondary_to_mortar_int]).tocsr())
        self.secondary_to_mortar_avg = Matrix(
            sps.bmat([[m] for m in secondary_to_mortar_avg]).tocsr())

        # Also generate a merged version of MortarGrid.sign_of_mortar_sides:
        mats = []
        for e in edges:
            mg = gb.edge_props(e, "mortar_grid")
            mats.append(mg.sign_of_mortar_sides(nd))
        self.sign_of_mortar_sides = Matrix(sps.block_diag(mats))