예제 #1
0
 def set_grid(self, gb: pp.GridBucket):
     """ Set a new grid
     """
     self.gb = gb
     self.Nd = gb.dim_max()
     self.n_frac = gb.get_grids(lambda _g: _g.dim == self.Nd - 1).size
     self.gb.add_node_props(keys="name")  # Add 'name' as node prop to all grids.
예제 #2
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})
예제 #3
0
def _tag_ivar_well_cells(_, gb: pp.GridBucket) -> None:
    """
    Tag well cells with unitary values, positive for injection cells and negative
    for production cells.
    """
    box = gb.bounding_box(as_dict=True)
    nd = gb.dim_max()
    for g, d in gb:
        tags = np.zeros(g.num_cells)
        if g.dim < nd:
            point = np.array([[(box["xmin"] + box["xmax"]) / 2], [box["ymax"]], [0],])
            distances = pp.distances.point_pointset(point, g.cell_centers)
            indexes = np.argsort(distances)
            if d["node_number"] == 1:
                tags[indexes[-1]] = 1  # injection
            elif d["node_number"] == 3:
                tags[indexes[-1]] = -1  # production
                # write_well_cell_to_csv(g, indexes[-1], self)
        g.tags["well_cells"] = tags
        pp.set_state(d, {"well": tags.copy()})
예제 #4
0
def nd_injection_cell_center(params: FlowParameters, gb: pp.GridBucket) -> None:
    """ Tag the center cell of the nd-grid with 1 (injection)

    Parameters
    ----------
    params : FlowParameters
    gb : pp.GridBucket

    """

    # Get the center of the domain.
    box = gb.bounding_box()
    pts = (box[1] + box[0]) / 2  # center of domain
    pts = np.atleast_2d(pts).T

    # Get the Nd-grid
    nd_grid = gb.grids_of_dimension(gb.dim_max())[0]

    # Tag highest dim grid with 1 in the cell closest to the grid center
    _tag_injection_cell(gb, nd_grid, pts, params.length_scale)
예제 #5
0
def shearzone_injection_cell(params: FlowParameters, gb: pp.GridBucket) -> None:
    """ Tag the borehole - shearzone intersection cell with 1 (injection)

    Parameters
    ----------
    params : FlowParameters
    gb : pp.GridBucket
    """
    # Shorthand
    shearzone = params.source_scalar_borehole_shearzone.get("shearzone")

    # Get intersection point
    pts = shearzone_borehole_intersection(params)

    # Get the grid to inject to
    injection_grid = gb.get_grids(lambda g: gb.node_props(g, "name") == shearzone)[0]
    assert (
        injection_grid.dim == gb.dim_max() - 1
    ), "Injection grid should be a Nd-1 fracture"

    # Tag injection grid with 1 in the injection cell
    _tag_injection_cell(gb, injection_grid, pts, params.length_scale)
예제 #6
0
    def __init__(self, params, gb: pp.GridBucket):
        super().__init__(params)

        self.gb = gb
        self.Nd = gb.dim_max()
예제 #7
0
def propagate_fractures(gb: pp.GridBucket, faces: Dict[pp.Grid,
                                                       np.ndarray]) -> None:
    """
    gb - grid bucket with matrix and fracture grids.
    faces_h - list of list of faces to be split in the highest-dimensional
        grid. The length of the outer list equals the number of fractures.
        Each entry in the list is a list containing the higher-dimensional
        indices of the faces to be split for the extension of the corresponding
        fracture.
    Changes to grids done in-place.
    The call changes:
        Geometry and connectivity fields of the two grids involved.
        The face_cells mapping between them
        Their respective face tags.
    Also adds the following to node data dictionaries:
        new_cells and new_faces tags, for use in e.g. local discretization
        updates.
        partial_update, a boolean flag indicating that the grids have been
        updated.

    """

    dim_h: int = gb.dim_max()
    g_h: pp.Grid = gb.grids_of_dimension(dim_h)[0]

    n_old_faces_h: int = g_h.num_faces

    # First initialise certain tags to get rid of any existing tags from
    # previous calls
    d_h: Dict = gb.node_props(g_h)
    d_h["new_cells"] = np.empty(0, dtype=int)
    d_h["new_faces"] = np.empty(0, dtype=int)
    d_h["split_faces"] = np.empty(0, dtype=int)

    # Data structure for keeping track of faces in g_h to be split
    split_faces = np.empty(0, dtype=np.int)

    # By default, we will not update the higher-dimensional grid. This will be
    # changed in the below for loop if the grid gets faces split.
    # This variable can be used e.g. to check if a rediscretization is necessary on
    # the higher-dimensional grid
    d_h["partial_update"] = False

    # Initialize mapping between old and new faces for g_h. We will store the updates
    # from splitting related to each lower-dimensional grid, and then merge towards the
    # end; the split data may be handy for debugging
    face_map_h: List[sps.spmatrix] = [
        sps.dia_matrix((np.ones(g_h.num_faces), 0),
                       (g_h.num_faces, g_h.num_faces))
    ]

    # The propagation is divided into two main steps:
    # First, update the geomtry of the fracture grids, and, simultaneously, the higher
    # dimensional grid (the former will be updated once, the latter may undergo several
    # update steps, depending on how many fractures propagate).
    # Second, update the mortar grids. This is done after all fractures have been
    # propagated.

    for g_l in gb.grids_of_dimension(dim_h - 1):

        # The propagation of a fracture consists of the following major steps:
        #   1. Find which faces in g_h should be split for this g_l.
        #   2. Add nodes to g_l where the fracture will propagate.
        #   3. Update face-node and cell-face relation in g_l.
        #   4. Update face geometry of g_l.
        #   5. Update cell geometry of g_l.
        #   6. Split the faces in g_h to make room for the new fracture.
        #   7. Update geometry in g_l and g_h.
        #
        # IMPLEMENTATION NOTE: While point 7 replaces information from 4 and 5, the
        # provisional fields may still be needed in point 6.

        # Initialize data on new faces and cells
        d_l = gb.node_props(g_l)
        d_l["new_cells"] = np.empty(0, dtype=int)
        d_l["new_faces"] = np.empty(0, dtype=int)

        # Step 1:
        # Uniquify the faces to be split. Amongs others, this avoids trouble when
        # a faces is requested split twice, from two neighboring faces
        faces_h = np.unique(np.atleast_1d(np.array(faces[g_l])))
        split_faces = np.append(split_faces, faces_h)

        if faces_h.size == 0:
            # If there is no propagation for this fracture, we continue
            # No need to update discretization of this grid
            d_l["partial_update"] = False

            # Variable mappings are unit mappings
            d_l["face_index_map"] = sps.identity(g_l.num_faces)
            d_l["cell_index_map"] = sps.identity(g_l.num_cells)

            # Identity mapping of faces in this step
            face_map_h.append(sps.identity(g_h.num_faces))

            # Move on to the next fracture
            continue

        # Keep track of original information:
        n_old_faces_l = g_l.num_faces
        n_old_cells_l = g_l.num_cells
        n_old_nodes_l = g_l.num_nodes
        n_old_nodes_h = g_h.num_nodes

        # It is convenient to tag the nodes lying on the domain boundary. This
        # helps updating the face tags later:
        pp.utils.tags.add_node_tags_from_face_tags(gb, "domain_boundary")

        # Step 2:
        # Get the "involved nodes", i.e., the union between the new nodes in
        # the lower dimension and the boundary nodes where the fracture
        # propagates. The former are added to the nodes in g_l - specifically,
        # both node coordinates and global_point_ind of g_l are amended.
        unique_node_ind_l, unique_node_ind_h = _update_nodes_fracture_grid(
            g_h, g_l, faces_h)

        # Step 3:
        # Update the connectivity matrices (cell_faces and face_nodes) and tag
        # the lower-dimensional faces, including re-classification of (former)
        # tips to internal faces, where appropriate.
        n_new_faces, new_face_centers = _update_connectivity_fracture_grid(
            g_l,
            g_h,
            unique_node_ind_l,
            unique_node_ind_h,
            n_old_nodes_l,
            n_old_faces_l,
            n_old_cells_l,
            faces_h,
        )

        # Step 4: Update fracture grid face geometry
        # Note: This simply expands arrays with face geometry, but it does not
        # compute reasonable values for the geometry
        _append_face_geometry_fracture_grid(g_l, n_new_faces, new_face_centers)

        # Step 5: Update fracture grid cell geometry
        # Same for cells. Here the geometry quantities are copied from the
        # face values of g_h, thus values should be reasonable.
        new_cells: np.ndarray = _update_cells_fracture_grid(g_h, g_l, faces_h)

        # Step 6: Split g_h along faces_h
        _split_fracture_extension(gb,
                                  g_h,
                                  g_l,
                                  faces_h,
                                  unique_node_ind_h,
                                  new_cells,
                                  non_planar=True)

        # Store information on which faces and cells have just been added.
        # Note that we only keep track of the faces and cells from the last
        # propagation call!
        new_faces_l = np.arange(g_l.num_faces - n_new_faces, g_l.num_faces)
        new_faces_h = g_h.frac_pairs[1, np.isin(g_h.frac_pairs[0], faces_h)]

        # Sanity check on the grid; most likely something will have gone wrong
        # long before if there is a problem.
        assert np.all(new_faces_h >= n_old_faces_h)
        if not np.min(new_cells) >= n_old_cells_l:
            raise ValueError(
                "New cells are assumed to be appended to cell array")
        if not np.min(new_faces_l) >= n_old_faces_l:
            raise ValueError(
                "New faces are assumed to be appended to face array")

        # Update the geometry
        _update_geometry(g_h, g_l, new_cells, n_old_cells_l, n_old_faces_l)

        # Finally some bookkeeping that can become useful in a larger-scale simulation.

        # Mark both grids for a partial update
        d_h["partial_update"] = True
        d_l["partial_update"] = True

        # Append arrays of new faces (g_l, g_h) and cells (g_l)
        d_h["new_faces"] = np.append(d_h["new_faces"], new_faces_h)
        d_l["new_cells"] = np.append(d_l["new_cells"], new_cells)
        d_l["new_faces"] = np.append(d_l["new_faces"], new_faces_l)

        # Create mappings between the old and and faces and cells in g_l
        arr = np.arange(n_old_faces_l)
        face_map_l = sps.coo_matrix(
            (np.ones(n_old_faces_l, dtype=np.int), (arr, arr)),
            shape=(g_l.num_faces, n_old_faces_l),
        ).tocsr()
        arr = np.arange(n_old_cells_l)
        cell_map_l = sps.coo_matrix(
            (np.ones(n_old_cells_l, dtype=np.int), (arr, arr)),
            shape=(g_l.num_cells, n_old_cells_l),
        ).tocsr()

        # These can be stored directly - there should be no more changes for g_l
        d_l["face_index_map"] = face_map_l
        d_l["cell_index_map"] = cell_map_l

        # For g_h we construct the map of faces for the splitting of this g_l
        # and append it to the list of face_maps

        # The size of the next map should be compatible with the number of faces in
        # the previous map.
        nfh = face_map_h[-1].shape[0]
        arr = np.arange(nfh)
        face_map_h.append(
            sps.coo_matrix(
                (np.ones(nfh, dtype=np.int), (arr, arr)),
                shape=(g_h.num_faces, nfh),
            ).tocsr())

        # Append default tags for the new nodes. Both high and low-dimensional grid
        _append_node_tags(g_l, g_l.num_nodes - n_old_nodes_l)
        _append_node_tags(g_h, g_h.num_nodes - n_old_nodes_h)

    # The standard node tags are updated from the face tags, which are updated on the
    # fly in the above loop.
    node_tags = ["domain_boundary", "tip", "fracture"]
    for tag in node_tags:
        # The node tag is set to true if at least one neighboring face is tagged
        pp.utils.tags.add_node_tags_from_face_tags(gb, tag)
    # Done with all splitting.

    # Compose the mapping of faces for g_l
    fm = face_map_h[0]
    for m in face_map_h[1:]:
        fm = m * fm
    d_h["face_index_map"] = fm
    # Also make a cell-map, this is a 1-1 mapping in this case
    d_h["cell_index_map"] = sps.identity(g_h.num_cells)

    d_h["split_faces"] = np.array(split_faces, dtype=int)

    ##
    # Second main step of propagation: Update mortar grid.

    # When all faces have been split, we can update the mortar grids
    for e, d_e in gb.edges_of_node(g_h):
        _, g_l = e
        d_l = gb.node_props(g_l)
        _update_mortar_grid(g_h, g_l, d_e, d_l["new_cells"], d_h["new_faces"])

        # Mapping of cell indices on the mortar grid is composed by the corresponding
        # map for g_l.
        cell_map = sps.kron(sps.identity(2), d_l["cell_index_map"]).tocsr()
        d_e["cell_index_map"] = cell_map

        # Also update projection operators
        pp.contact_conditions.set_projections(gb, [e])
예제 #8
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
예제 #9
0
def set_projections(gb: pp.GridBucket) -> 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.

    """
    # 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, d_m in gb.edges():

        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_h = gb.nodes_of_edge(e)

        # Find faces of the higher dimensional grid that coincide with the mortar
        # grid. Go via the master to mortar projection
        # Convert matrix to csr, then the relevant face indices are found from
        # the (column) indices
        faces_on_surface = mg.master_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.sign_of_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. This could probably have been
        # done with face tags, but we instead project the normal vectors onto the
        # mortar grid to kill off all irrelevant faces. Restriction to a single
        # normal vector is done in the construction of the projection object
        # (below).
        # NOTE: Use a single normal vector to span the tangential and normal space,
        # thus assuming the surface is planar.
        outwards_unit_vector_mortar = mg.master_to_mortar_int().dot(unit_normal.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(
            outwards_unit_vector_mortar[:, 0].reshape((-1, 1))
        )

        # Store the projection operator in the mortar data
        d_m["tangential_normal_projection"] = projection