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})
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)
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])