def test_rt0_2d_triangle(self): dim = 2 nodes = np.array([[0, 1, 0], [0, 0, 1], [0, 0, 0]]) indices = [0, 1, 1, 2, 2, 0] indptr = [0, 2, 4, 6] face_nodes = sps.csc_matrix(([True] * 6, indices, indptr)) cell_faces = sps.csc_matrix([[1], [1], [1]]) name = "test" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() kxx = np.ones(g.num_cells) perm = pp.SecondOrderTensor(kxx=kxx, kyy=kxx, kzz=1) bf = g.get_boundary_faces() bc = pp.BoundaryCondition(g, bf, bf.size * ["dir"]) M = self._matrix(g, perm, bc) # Matrix computed with an already validated code M_known = np.matrix([ [0.33333333, 0.0, -0.16666667, -1.0], [0.0, 0.16666667, 0.0, -1.0], [-0.16666667, 0.0, 0.33333333, -1.0], [-1.0, -1.0, -1.0, 0.0], ]) self.assertTrue(np.allclose(M, M.T)) self.assertTrue(np.allclose(M, M_known))
def test_mvem_tetra(self): dim = 3 nodes = np.array([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) indices = [0, 2, 1, 1, 2, 3, 2, 0, 3, 3, 0, 1] indptr = [0, 3, 6, 9, 12] face_nodes = sps.csc_matrix(([True] * 12, indices, indptr)) cell_faces = sps.csc_matrix([[1], [1], [1], [1]]) name = "test" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() kxx = np.ones(g.num_cells) perm = pp.SecondOrderTensor(kxx=kxx, kyy=kxx, kzz=kxx) bf = g.get_boundary_faces() bc = pp.BoundaryCondition(g, bf, bf.size * ["dir"]) vect = np.vstack( (2 * g.cell_volumes, 3 * g.cell_volumes, np.zeros(g.num_cells)) ).ravel(order="F") b = self._matrix(g, perm, bc, vect) b_known = np.array([0.41666667, 0.41666667, -0.25, -0.58333333, 0.0]) self.assertTrue(np.allclose(b, b_known))
def test_rt0_tetra(self): dim = 3 nodes = np.array([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) indices = [0, 2, 1, 1, 2, 3, 2, 0, 3, 3, 0, 1] indptr = [0, 3, 6, 9, 12] face_nodes = sps.csc_matrix(([True] * 12, indices, indptr)) cell_faces = sps.csc_matrix([[1], [1], [1], [1]]) name = "test" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() kxx = np.ones(g.num_cells) perm = pp.SecondOrderTensor(kxx=kxx, kyy=kxx, kzz=kxx) bf = g.get_boundary_faces() bc = pp.BoundaryCondition(g, bf, bf.size * ["dir"]) M = self._matrix(g, perm, bc) M_known = np.matrix([ [0.53333333, 0.03333333, -0.13333333, -0.13333333, -1.0], [0.03333333, 0.2, 0.03333333, 0.03333333, -1.0], [-0.13333333, 0.03333333, 0.53333333, -0.13333333, -1.0], [-0.13333333, 0.03333333, -0.13333333, 0.53333333, -1.0], [-1.0, -1.0, -1.0, -1.0, 0.0], ]) self.assertTrue(np.allclose(M, M.T)) self.assertTrue(np.allclose(M, M_known))
def grid_2d(self, pert_node=False, flip_normal=False): # pert_node pertubes one node in the grid. Leads to non-matching cells. # flip_normal flips one normal vector in 2d grid adjacent to the fracture. # Tests that there is no assumptions on direction of fluxes in the # mortar coupling. nodes = np.array([ [0, 0, 0], [1, 0, 0], [1, 0.5, 0], [0.5, 0.5, 0], [0, 0.5, 0], [0, 0.5, 0], [0.5, 0.5, 0], [1, 0.5, 0], [1, 1, 0], [0, 1, 0], ]).T if pert_node: nodes[0, 3] = 0.75 fn = np.array([ [0, 1], [1, 2], [2, 3], [3, 4], [4, 0], [0, 3], [3, 1], [5, 6], [6, 7], [7, 8], [8, 9], [9, 5], [9, 6], [6, 8], ]).T cf = np.array([[3, 4, 5], [0, 6, 5], [1, 2, 6], [7, 12, 11], [12, 13, 10], [8, 9, 13]]).T cols = np.tile(np.arange(fn.shape[1]), (fn.shape[0], 1)).ravel("F") face_nodes = sps.csc_matrix( (np.ones_like(cols), (fn.ravel("F"), cols))) cols = np.tile(np.arange(cf.shape[1]), (cf.shape[0], 1)).ravel("F") data = np.array( [1, 1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, -1]) cell_faces = sps.csc_matrix((data, (cf.ravel("F"), cols))) g = pp.Grid(2, nodes, face_nodes, cell_faces, "TriangleGrid") g.compute_geometry() g.tags["fracture_faces"][[2, 3, 7, 8]] = 1 if flip_normal: g.face_normals[:, [2]] *= -1 g.cell_faces[2, 2] *= -1 g.global_point_ind = np.arange(nodes.shape[1]) return g
def test_two_cells(self): # 2d grid is a single triangle grid with nodes ordered in the counter clockwise # direction fn_2d = sps.csc_matrix( np.array( [[1, 0, 0, 1, 0], [1, 1, 0, 0, 1], [0, 1, 1, 0, 0], [0, 0, 1, 1, 1]] ) ) cf_2d = sps.csc_matrix(np.array([[1, 0], [0, 1], [0, 1], [1, 0], [-1, 1]])) nodes_2d = np.array([[0, 1, 1, 0], [0, 0, 1, 1], [0, 0, 0, 0]]) g_2d = pp.Grid(2, nodes_2d, fn_2d, cf_2d, "tmp") z = np.array([0, 1]) nz = z.size g_3d, cell_map, face_map = pp.grid_extrusion._extrude_2d(g_2d, z) self.assertTrue(g_3d.num_nodes == 8) self.assertTrue(g_3d.num_cells == 2) self.assertTrue(g_3d.num_faces == 9) nodes = np.tile(nodes_2d, nz) nodes[2, :4] = z[0] nodes[2, 4:] = z[1] self.assertTrue(np.allclose(g_3d.nodes, nodes)) fn_3d = np.array( [ [1, 1, 0, 0, 1, 1, 0, 0], [0, 1, 1, 0, 0, 1, 1, 0], [0, 0, 1, 1, 0, 0, 1, 1], # Done with first vertical layer [1, 0, 0, 1, 1, 0, 0, 1], [0, 1, 0, 1, 0, 1, 0, 1], [1, 1, 0, 1, 0, 0, 0, 0], # Second vertical layer [0, 1, 1, 1, 0, 0, 0, 0], [0, 0, 0, 0, 1, 1, 0, 1], [0, 0, 0, 0, 0, 1, 1, 1], ] ).T self.assertTrue(np.allclose(g_3d.face_nodes.toarray(), fn_3d)) cf_3d = np.array( [[1, 0], [0, 1], [0, 1], [1, 0], [-1, 1], [-1, 0], [0, -1], [1, 0], [0, 1]] ) self.assertTrue(np.allclose(g_3d.cell_faces.toarray(), cf_3d)) self.assertTrue(check_cell_map(cell_map, g_2d, nz - 1)) self.assertTrue(check_face_map(face_map, g_2d, nz - 1))
def test_merge_grids_split(self): g1 = pp.TensorGrid(np.linspace(0, 2, 2)) g2 = pp.TensorGrid(np.linspace(2, 4, 2)) g_nodes = np.hstack((g1.nodes, g2.nodes)) g_face_nodes = sps.block_diag((g1.face_nodes, g2.face_nodes), "csc") g_cell_faces = sps.block_diag((g1.cell_faces, g2.cell_faces), "csc") g = pp.Grid(1, g_nodes, g_face_nodes, g_cell_faces, "pp.TensorGrid") h1 = pp.TensorGrid(np.linspace(0, 2, 3)) h2 = pp.TensorGrid(np.linspace(2, 4, 3)) h_nodes = np.hstack((h1.nodes, h2.nodes)) h_face_nodes = sps.block_diag((h1.face_nodes, h2.face_nodes), "csc") h_cell_faces = sps.block_diag((h1.cell_faces, h2.cell_faces), "csc") h = pp.Grid(1, h_nodes, h_face_nodes, h_cell_faces, "pp.TensorGrid") g.compute_geometry() h.compute_geometry() weights, new, old = pp.match_grids.match_1d(g, h, tol=1e-4) # Weights give mappings from h to g. All cells are split in two self.assertTrue(np.allclose(weights, np.array([1.0, 1.0, 1.0, 1.0]))) self.assertTrue(np.allclose(new, np.array([0, 0, 1, 1]))) self.assertTrue(np.allclose(old, np.array([0, 1, 2, 3])))
def import_grid(folder, fname, dim, **kwargs): index_shift = kwargs.get("index_shift", 1) # load the nodes fnodes_root = kwargs.get("nodes", "points") fnodes = folder + "/" + fnodes_root + fname + ".txt" nodes = np.loadtxt(fnodes, dtype=np.float) nodes = np.atleast_2d(nodes).T nodes = np.vstack([nodes[1:], np.zeros(nodes.shape[1])]) # load the face to node map fface_nodes_root = kwargs.get("face_nodes", "face2node") fface_nodes = folder + "/" + fface_nodes_root + fname + ".txt" face_nodes = np.loadtxt(fface_nodes, dtype=np.int).T col = face_nodes[0] - index_shift row = face_nodes[1] - index_shift data = face_nodes[2].astype(np.bool) face_nodes = sps.csc_matrix((data, (row, col))) # load the cell to face map fcell_faces_root = kwargs.get("cell_faces", "cell2face") fcell_faces = folder + "/" + fcell_faces_root + fname + ".txt" cell_faces = np.loadtxt(fcell_faces, dtype=np.int).T col = cell_faces[0] - index_shift row = cell_faces[1] - index_shift data = cell_faces[2] cell_faces = sps.csc_matrix((data, (row, col))) # it's not really triangular grid but it's useful somewhere name = "TriangleGrid" if dim == 2 else "TensorGrid" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.name.append(fname) g.compute_geometry() # load the point to local to global map fglobal_root = kwargs.get("global", "glob2loc") fglobal = folder + "/" + fglobal_root + fname + ".txt" if glob.glob(fglobal): g.global_point_ind = np.loadtxt(fglobal, dtype=np.int).T[-1] else: g.global_point_ind = np.arange(g.num_nodes) + 1 warnings.warn( "Attention default global to local mapping used for grid\n" + str(g)) return g
def unite_grids(gs): # it's simple index transformation gs = np.atleast_1d(gs) # collect the nodes nodes = np.hstack((g.nodes for g in gs)) # collect the face_nodes map face_nodes = sps.block_diag([g.face_nodes for g in gs], "csc") # collect the cell_faces map cell_faces = sps.block_diag([g.cell_faces for g in gs], "csc") # dimension and name dim = gs[0].dim name = "United grid" # create the grid and give it back g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() return g
def test_rt0_2d_triangle(self): dim = 2 nodes = np.array([[0, 1, 0], [0, 0, 1], [0, 0, 0]]) indices = [0, 1, 1, 2, 2, 0] indptr = [0, 2, 4, 6] face_nodes = sps.csc_matrix(([True] * 6, indices, indptr)) cell_faces = sps.csc_matrix([[1], [1], [1]]) name = "test" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() kxx = np.ones(g.num_cells) perm = pp.SecondOrderTensor(3, kxx=kxx, kyy=kxx, kzz=1) bf = g.get_boundary_faces() bc = pp.BoundaryCondition(g, bf, bf.size * ["dir"]) solver = pp.RT0(physics="flow") param = pp.Parameters(g) param.set_tensor(solver, perm) param.set_bc(solver, bc) M = solver.matrix(g, {"param": param}).todense() # Matrix computed with an already validated code M_known = np.matrix( [ [0.33333333, 0., -0.16666667, -1.], [0., 0.16666667, 0., -1.], [-0.16666667, 0., 0.33333333, -1.], [-1., -1., -1., 0.], ] ) self.assertTrue(np.allclose(M, M.T)) self.assertTrue(np.allclose(M, M_known))
def __extract_cells_from_faces_2d(g, f): # Local cell-face and face-node maps. cell_nodes, unique_nodes = __extract_submatrix(g.face_nodes, f) cell_faces_indices = cell_nodes.indices data = -1 * cell_nodes.data _, ix = np.unique(cell_faces_indices, return_index=True) data[ix] *= -1 cell_faces = sps.csc_matrix((data, cell_faces_indices, cell_nodes.indptr)) num_faces = np.shape(cell_faces)[0] num_nodes = np.shape(cell_nodes)[0] num_nodes_per_face = np.ones(num_nodes) face_node_ind = pp.utils.matrix_compression.rldecode( np.arange(num_faces), num_nodes_per_face) face_nodes = sps.coo_matrix( (np.ones(num_nodes, dtype=bool), (np.arange(num_faces), face_node_ind))).tocsc() # Append information on subgrid extraction to the new grid's history name = list(g.name) name.append("Extract subgrid") h = pp.Grid(g.dim - 1, g.nodes[:, unique_nodes], face_nodes, cell_faces, name) h.compute_geometry() assert np.all(np.isclose(g.face_areas[f], h.cell_volumes)) h.cell_volumes = g.face_areas[f] assert np.all(np.isclose(g.face_centers[:, f], h.cell_centers)) h.cell_centers = g.face_centers[:, f] h.parent_face_ind = f return h, f, unique_nodes
def test_rt0_tetra(self): dim = 3 nodes = np.array([[0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) indices = [0, 2, 1, 1, 2, 3, 2, 0, 3, 3, 0, 1] indptr = [0, 3, 6, 9, 12] face_nodes = sps.csc_matrix(([True] * 12, indices, indptr)) cell_faces = sps.csc_matrix([[1], [1], [1], [1]]) name = "test" g = pp.Grid(dim, nodes, face_nodes, cell_faces, name) g.compute_geometry() kxx = np.ones(g.num_cells) perm = pp.SecondOrderTensor(3, kxx=kxx, kyy=kxx, kzz=kxx) bf = g.get_boundary_faces() bc = pp.BoundaryCondition(g, bf, bf.size * ["dir"]) solver = pp.RT0(physics="flow") param = pp.Parameters(g) param.set_tensor(solver, perm) param.set_bc(solver, bc) M = solver.matrix(g, {"param": param}).todense() M_known = np.matrix( [ [0.53333333, 0.03333333, -0.13333333, -0.13333333, -1.], [0.03333333, 0.2, 0.03333333, 0.03333333, -1.], [-0.13333333, 0.03333333, 0.53333333, -0.13333333, -1.], [-0.13333333, 0.03333333, -0.13333333, 0.53333333, -1.], [-1., -1., -1., -1., 0.], ] ) self.assertTrue(np.allclose(M, M.T)) self.assertTrue(np.allclose(M, M_known))
def extract_subgrid(g, c, sort=True, faces=False, is_planar=True): """ Extract a subgrid based on cell/face indices. For simplicity the cell/face indices will be sorted before the subgrid is extracted. If the parent grid has geometry attributes (cell centers etc.) these are copied to the child. No checks are done on whether the cells/faces form a connected area. The method should work in theory for non-connected cells, the user will then have to decide what to do with the resulting grid. This option has however not been tested. Parameters: g (core.grids.Grid): Grid object, parent c (np.array, dtype=int): Indices of cells to be extracted sort=True (bool): If true c is sorted faces=False (bool): If true c are intrepetred as faces, and the exptracted grid will be a lower dimensional grid defined by the these faces is_planar: (optional) defaults to True. Only used when extracting faces from a 3d grid. If True the faces f must be planar. Set to False to use this function for extracting a non-planar 2D grid, but use at own risk. Returns: Grid: Extracted subgrid. Will share (note, *not* copy) geometric fileds with the parent grid. Also has an additional field parent_cell_ind giving correspondance between parent and child cells. np.ndarray, dtype=int: Index of the extracted faces, ordered so that element i is the global index of face i in the subgrid. np.ndarray, dtype=int: Index of the extracted nodes, ordered so that element i is the global index of node i in the subgrid. Raises: IndexError if index is as boolean and do not match the array size. """ if np.asarray(c).dtype == "bool": # convert to indices. # First check for dimension if faces and c.size != g.num_faces: raise IndexError("boolean index did not match number of faces") elif not faces and c.size != g.num_cells: raise IndexError("boolean index did not match number of cells") c = np.where(c)[0] if sort: c = np.sort(np.atleast_1d(c)) if faces: return __extract_cells_from_faces(g, c, is_planar) # Local cell-face and face-node maps. cf_sub, unique_faces = __extract_submatrix(g.cell_faces, c) fn_sub, unique_nodes = __extract_submatrix(g.face_nodes, unique_faces) # Append information on subgrid extraction to the new grid's history name = list(g.name) name.append("Extract subgrid") # Construct new grid. h = pp.Grid(g.dim, g.nodes[:, unique_nodes], fn_sub, cf_sub, name) # Copy geometric information if any if hasattr(g, "cell_centers"): h.cell_centers = g.cell_centers[:, c] if hasattr(g, "cell_volumes"): h.cell_volumes = g.cell_volumes[c] if hasattr(g, "face_centers"): h.face_centers = g.face_centers[:, unique_faces] if hasattr(g, "face_normals"): h.face_normals = g.face_normals[:, unique_faces] if hasattr(g, "face_areas"): h.face_areas = g.face_areas[unique_faces] h.parent_cell_ind = c return h, unique_faces, unique_nodes
def simplexify(gb, data=[]): "make the higher dimensional grid make of simplices by splitting the cells and return the map" new_gb = gb.copy() grids = gb.grids_of_dimension(gb.dim_max()) for g in grids: g_cell_nodes = g.cell_nodes() face_nodes_indices = np.empty(0, dtype=np.int) #g.num_faces) cell_faces_indices = np.empty(0, dtype=np.int) cell_faces_data = np.empty(0, dtype=np.int) # projection operator proj_I = np.empty(0, dtype=np.int) proj_J = np.empty(0, dtype=np.int) num_faces = g.num_faces num_cells = 0 for c in np.arange(g.num_cells): # cell nodes c_nodes_pos = slice(g_cell_nodes.indptr[c], g_cell_nodes.indptr[c + 1]) c_nodes = g_cell_nodes.indices[c_nodes_pos] # cell faces c_faces_pos = slice(g.cell_faces.indptr[c], g.cell_faces.indptr[c + 1]) c_faces = g.cell_faces.indices[c_faces_pos] c_faces_data = g.cell_faces.data[c_faces_pos] out = np.ones((4, c_faces.size), dtype=np.int) out[0] = np.arange(c_faces.size) + num_faces out[1] = c_nodes out[2] = c + g.num_nodes for f, d in zip(c_faces, c_faces_data): f_nodes_pos = slice(g.face_nodes.indptr[f], g.face_nodes.indptr[f + 1]) f_nodes = g.face_nodes.indices[f_nodes_pos] # check which new faces we need to consider mask = np.in1d(out[1], f_nodes, assume_unique=True) c_indices = np.hstack((out[0, mask], f)) c_data = np.hstack((out[3, mask], d)) out[3, mask] *= -1 # save the data cell_faces_indices = np.hstack((cell_faces_indices, c_indices)) cell_faces_data = np.hstack((cell_faces_data, c_data)) proj_I = np.hstack((proj_I, num_cells + np.arange(c_faces.size))) proj_J = np.hstack( (proj_J, c * np.ones(c_faces.size, dtype=np.int))) num_cells += c_faces.size num_faces += c_faces.size face_nodes_indices = np.hstack( (face_nodes_indices, out[1:-1].ravel(order='F'))) face_nodes_indices = np.hstack( (g.face_nodes.indices, face_nodes_indices)) face_nodes_indptr = np.arange(num_faces + 1, dtype=np.int) * 2 face_nodes_data = np.ones(face_nodes_indices.size, dtype=np.bool) face_nodes = sps.csc_matrix( (face_nodes_data, face_nodes_indices, face_nodes_indptr)) cell_faces_indptr = np.arange(num_cells + 1, dtype=np.int) * 3 cell_faces = sps.csc_matrix( (cell_faces_data, cell_faces_indices, cell_faces_indptr)) nodes = np.hstack((g.nodes, g.cell_centers)) new_g = pp.Grid(g.dim, nodes, face_nodes, cell_faces, g.name) new_g.compute_geometry() new_gb.update_nodes({g: new_g}) # update the data proj = sps.csr_matrix((np.ones(proj_J.size), (proj_I, proj_J))) for d in data: if gb._nodes[g][pp.STATE].get(d, None) is not None: old_values = gb._nodes[g][pp.STATE][d] if old_values.ndim > 1: new_gb._nodes[new_g][pp.STATE][d] = np.zeros( (old_values.shape[0], proj.shape[0])) for dim in np.arange(old_values.ndim): new_gb._nodes[new_g][pp.STATE][d][ dim, :] = proj * old_values[dim, :] else: new_gb._nodes[new_g][pp.STATE][d] = proj * old_values else: old_values = gb._nodes[g][d] new_gb._nodes[new_g][d] = proj * old_values return new_gb
def test_two_cells_one_quad(self): # 2d grid is a single triangle grid with nodes ordered in the counter clockwise # direction fn_2d = sps.csc_matrix( np.array( [ [1, 0, 0, 0, 1, 0], [1, 1, 0, 0, 0, 1], [0, 1, 1, 0, 0, 0], [0, 0, 1, 1, 0, 0], [0, 0, 0, 1, 1, 1], ] ) ) cf_2d = sps.csc_matrix( np.array([[1, 0], [0, -1], [0, 1], [0, 1], [-1, 0], [1, -1]]) ) nodes_2d = np.array([[0, 1, 2, 1, 0], [0, 0, 1, 2, 1], [0, 0, 0, 0, 0]]) g_2d = pp.Grid(2, nodes_2d, fn_2d, cf_2d, "tmp") z = np.array([0, 1]) nz = z.size g_3d, cell_map, face_map = pp.grid_extrusion._extrude_2d(g_2d, z) self.assertTrue(g_3d.num_nodes == 10) self.assertTrue(g_3d.num_cells == 2) self.assertTrue(g_3d.num_faces == 10) nodes = np.tile(nodes_2d, nz) nodes[2, :5] = z[0] nodes[2, 5:] = z[1] self.assertTrue(np.allclose(g_3d.nodes, nodes)) fn_ind = np.array( [ 0, 1, 6, 5, 1, 2, 7, 6, 2, 3, 8, 7, 3, 4, 9, 8, 4, 0, 5, 9, 1, 4, 9, 6, 0, 1, 4, 1, 2, 3, 4, 5, 6, 9, 6, 7, 8, 9, ] ) fn_ind_ptr = np.array([0, 4, 8, 12, 16, 20, 24, 27, 31, 34, fn_ind.size]) fn_3d = sps.csc_matrix( (np.ones(fn_ind.size), fn_ind, fn_ind_ptr), shape=(10, 10) ) self.assertTrue(np.allclose(g_3d.face_nodes.toarray(), fn_3d.toarray())) cf_3d = np.array( [ [1, 0], [0, -1], [0, 1], [0, 1], [-1, 0], [1, -1], [-1, 0], [0, -1], [1, 0], [0, 1], ] ) self.assertTrue(np.allclose(g_3d.cell_faces.toarray(), cf_3d)) self.assertTrue(check_cell_map(cell_map, g_2d, nz - 1)) self.assertTrue(check_face_map(face_map, g_2d, nz - 1))
def grid_2d(self, flip_normals=False, pert_node=False, rotate_fracture=False): """ flip_normals: Some, but not all, faces in 2d grid on fracture surface are rotated. Divergence operator is modified accordingly. The solution should be invariant under this change. perturb_node: One node in 2d grid on the fracture surface is shifted. This breaks symmetry of the grid across the fracture. Should not be combined with rotate_fracture. rotate_fracture: Fracture is rotated from aligned with the y-axis to a slightly tilted position. Should not be combined with perturb_node """ nodes = np.array([ [0, 0, 0], # 0 [1, 0, 0], # 1 [1, 0.5, 0], # 2 [1, 1, 0], # 3 [0, 1, 0], # 4 [0, 0.5, 0], # 5 [0.75, 0.375, 0], # 6 [0.75, 0.75, 0], # 7 [0.25, 0.75, 0], # 8 [0.25, 0.25, 0], # 9 [0.5, 0.25, 0], # 10 [0.5, 0.5, 0], # 11 [0.5, 0.75, 0], # 12 [0.5, 0.5, 0], # 13 ]).T if pert_node: if rotate_fracture: raise ValueError("Incompatible options to grid construction") nodes[1, 13] = 0.6 if rotate_fracture: nodes[0, 10] = 0.4 nodes[0, 12] = 0.6 fn = np.array( # Faces always go from node of low to high index [ [0, 1], # 0 [1, 2], # 1 [2, 3], # 2 [3, 4], # 3 [4, 5], # 4 [0, 5], # 5 [0, 9], # 6 [0, 10], # 7 [9, 10], # 8 [5, 9], # 9 [9, 13], # 10 [8, 13], # 11 [5, 8], # 12 [8, 9], # 13 [4, 8], # 14 [8, 12], # 15 [4, 12], # 16 [1, 10], # 17 [1, 6], # 18 [6, 10], # 19 [6, 11], # 20 [6, 7], # 21 [7, 11], # 22 [2, 6], # 23 [2, 7], # 24 [7, 12], # 25 [3, 7], # 26 [3, 12], # 27 [10, 13], # 28 [12, 13], # 29 [10, 11], # 30 [11, 12], # 31 ]).T cols = np.tile(np.arange(fn.shape[1]), (fn.shape[0], 1)).ravel("F") face_nodes = sps.csc_matrix( (np.ones_like(cols), (fn.ravel("F"), cols))) cf = np.array([ [0, 17, 7], # 0 [6, 7, 8], # 1 [5, 6, 9], # 2 [8, 28, 10], # 3 [10, 11, 13], # 4 [9, 13, 12], # 5 [11, 29, 15], # 6 [4, 12, 14], # 7 [14, 15, 16], # 8 [3, 16, 27], # 9 [25, 31, 22], # 10 [25, 26, 27], # 11 [2, 26, 24], # 12 [21, 23, 24], # 13 [20, 21, 22], # 14 [19, 20, 30], # 15 [17, 18, 19], # 16 [1, 23, 18], # 17 ]).T cols = np.tile(np.arange(cf.shape[1]), (cf.shape[0], 1)).ravel("F") data = np.array([ [1, 1, -1], # 0 [-1, 1, -1], # 1 [-1, 1, -1], # 2 [1, 1, -1], # 3 [1, -1, 1], # 4 [1, -1, -1], # 5 [1, -1, -1], # 6 [1, 1, -1], # 7 [1, 1, -1], # 8 [1, 1, -1], # 9 [1, -1, -1], # 10 [-1, -1, 1], # 11 [1, 1, -1], # 12 [-1, -1, 1], # 13 [-1, 1, 1], # 14 [-1, 1, -1], # 15 [-1, 1, 1], # 16 [1, 1, -1], # 17 ]).ravel() cell_faces = sps.csc_matrix((data, (cf.ravel("F"), cols))) g = pp.Grid(2, nodes, face_nodes, cell_faces, "TriangleGrid") g.compute_geometry() fracture_faces = [28, 29, 30, 31] g.tags["fracture_faces"][fracture_faces] = 1 g.global_point_ind = np.arange(nodes.shape[1]) if flip_normals: g.face_normals[:, fracture_faces] *= -1 g.cell_faces[fracture_faces, [3, 6, 15, 10]] *= -1 g.face_normals[:, 28] *= -1 g.cell_faces[28, 3] *= -1 return g
def test_single_cell_negative_z(self): # 2d grid is a single triangle grid with nodes ordered in the counter clockwise # direction fn_2d = sps.csc_matrix(np.array([[1, 0, 1], [1, 1, 0], [0, 1, 1]])) cf_2d = sps.csc_matrix(np.array([[-1], [1], [-1]])) nodes_2d = np.array([[0, 2, 1], [0, 0, 1], [0, 0, 0]]) g_2d = pp.Grid(2, nodes_2d, fn_2d, cf_2d, "tmp") z = np.array([0, -1, -3]) nz = z.size g_3d, cell_map, face_map = pp.grid_extrusion._extrude_2d(g_2d, z) self.assertTrue(g_3d.num_nodes == 9) self.assertTrue(g_3d.num_cells == 2) self.assertTrue(g_3d.num_faces == 9) nodes = np.tile(nodes_2d, nz) nodes[2, :3] = z[0] nodes[2, 3:6] = z[1] nodes[2, 6:] = z[2] self.assertTrue(np.allclose(g_3d.nodes, nodes)) fn_3d = np.array( [ [1, 1, 0, 1, 1, 0, 0, 0, 0], [0, 1, 1, 0, 1, 1, 0, 0, 0], [1, 0, 1, 1, 0, 1, 0, 0, 0], # Done with first vertical layer [0, 0, 0, 1, 1, 0, 1, 1, 0], [0, 0, 0, 0, 1, 1, 0, 1, 1], [0, 0, 0, 1, 0, 1, 1, 0, 1], # Second vertical layer [1, 1, 1, 0, 0, 0, 0, 0, 0], [0, 0, 0, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 1, 1, 1], ] ).T self.assertTrue(np.allclose(g_3d.face_nodes.toarray(), fn_3d)) cf_3d = np.array( [ [-1, 0], [1, 0], [-1, 0], [0, -1], [0, 1], [0, -1], [-1, 0], [1, -1], [0, 1], ] ) self.assertTrue(np.allclose(g_3d.cell_faces.toarray(), cf_3d)) # Geometric quantities # Cell center cc = np.array([[1, 1], [1 / 3, 1 / 3], [-0.5, -2]]) self.assertTrue(np.allclose(g_3d.cell_centers, cc)) # Face centers fc = np.array( [ [1, 0, -0.5], [1.5, 0.5, -0.5], [0.5, 0.5, -0.5], [1, 0, -2], [1.5, 0.5, -2], [0.5, 0.5, -2], [1, 1 / 3, 0], [1, 1 / 3, -1], [1, 1 / 3, -3], ] ).T self.assertTrue(np.allclose(g_3d.face_centers, fc)) # Face areas fa = np.array( [2, np.sqrt(2), np.sqrt(2), 4, 2 * np.sqrt(2), 2 * np.sqrt(2), 1, 1, 1] ) self.assertTrue(np.allclose(g_3d.face_areas, fa)) # Normal vectors normals = np.array( [ [0, 2, 0], [1, 1, 0], [1, -1, 0], [0, 4, 0], [2, 2, 0], [2, -2, 0], [0, 0, -1], [0, 0, -1], [0, 0, -1], ] ).T self.assertTrue(np.allclose(g_3d.face_normals, normals)) self.assertTrue(check_cell_map(cell_map, g_2d, nz - 1)) self.assertTrue(check_face_map(face_map, g_2d, nz - 1))
def create_grid(self): """ Domain is unit square. One fracture, centered on (0.5, 0.5), tilted according to self.angle. """ angle = self.angle corners = np.array([[0, 1, 1, 0], [0, 0, 1, 1], [0, 0, 0, 0]]) # The fracture points always have x coordinates 0.2 and 0.8 # The y-coordinates are set so that the fracture forms the prescribed angle with # the x-axis frac_pt = np.array( [[0.2, 0.8], [0.5 - 0.3 * np.tan(angle), 0.5 + 0.3 * np.tan(angle)], [0, 0]]) nodes = np.hstack((corners, frac_pt)) rows = np.array([ [0, 1], [1, 5], [5, 0], [1, 2], [2, 5], [2, 4], [2, 3], [3, 0], [3, 4], [4, 0], [4, 5], [4, 5], ]).ravel() cols = np.vstack((np.arange(12), np.arange(12))).ravel("F") data = np.ones_like(rows) fn_2d = sps.coo_matrix((data, (rows, cols)), shape=(6, 12)).tocsc() rows = np.array([[0, 1, 2], [1, 3, 4], [4, 5, 10], [5, 6, 8], [7, 8, 9], [2, 11, 9]]).ravel() cols = np.tile(np.arange(6), (3, 1)).ravel("F") data = np.array( [-1, -1, -1, 1, -1, -1, 1, -1, -1, -1, 1, -1, 1, 1, -1, 1, 1, 1]) cf_2d = sps.coo_matrix((data, (rows, cols)), shape=(12, 6)).tocsc() g_2d = pp.Grid(2, nodes, fn_2d, cf_2d, "mock_2d_grid") g_2d.compute_geometry() fn_1d = sps.csc_matrix(np.array([[1, 0], [0, 1]])) cf_1d = sps.csc_matrix(np.array([[-1], [1]])) g_1d = pp.Grid(1, frac_pt, fn_1d, cf_1d, "mock_1d_grid") g_1d.compute_geometry() gb = pp.GridBucket() gb.add_nodes([g_2d, g_1d]) # Construct mortar grid side_grids = { mortar_grid.LEFT_SIDE: g_1d.copy(), mortar_grid.RIGHT_SIDE: g_1d.copy(), } data = np.array([1, 1]) row = np.array([0, 0]) col = np.array([10, 11]) face_cells_mortar = sps.coo_matrix( (data, (row, col)), shape=(g_1d.num_cells, g_2d.num_faces)).tocsc() mg = pp.MortarGrid(1, side_grids, face_cells_mortar) edge = (g_2d, g_1d) gb.add_edge(edge, face_cells_mortar) d = gb.edge_props(edge) d["mortar_grid"] = mg self.gb = gb self._Nd = 2 self.g1 = g_1d self.g2 = g_2d self.edge = edge self.mg = mg
def __extract_cells_from_faces_3d(g, f, is_planar=True): """ Extract a 2D grid from the faces of a 3D grid. One of the uses of this function is to obtain a 2D MortarGrid from the boundary of a 3D grid. The faces f is by default assumed to be planar, however, this is mainly because compute_geometry does not handle non-planar grids. compute_geometry is used to do a sanity check of the extracted grid. if is_planar is set to False, this function should handle non-planar grids, however, this has not been tested thouroghly, and it does not performe the geometric sanity checks. Parameters: ---------- g: (Grid) A 3d grid f: (ndarray) faces of the 3d grid to by used as cells in the 2d grid is_planar: (optional) defaults to True: If False the faces f must be planar. Set to False to use this function for a non-planar 2D grid, but use at own risk Returns: ------- (Grid) A 2d grid extracted from the 3d grid g and the faces f. Throws: ------ ValueError: If the given faces is not planar and is_planar==True """ # Local cell-face and face-node maps. cell_nodes, unique_nodes = __extract_submatrix(g.face_nodes, f) if is_planar and not pp.geometry_property_checks.points_are_planar( g.nodes[:, unique_nodes]): raise ValueError("The faces extracted from a 3D grid must be planar") num_cell_nodes = cell_nodes.nnz cell_node_ptr = cell_nodes.indptr num_nodes_per_cell = cell_node_ptr[1:] - cell_node_ptr[:-1] next_node = np.arange(num_cell_nodes) + 1 next_node[cell_node_ptr[1:] - 1] = cell_node_ptr[:-1] face_start = cell_nodes.indices face_end = cell_nodes.indices[next_node] face_nodes_indices = np.vstack((face_start, face_end)) face_nodes_sorted = np.sort(face_nodes_indices, axis=0) _, IA, IC = np.unique(face_nodes_sorted, return_index=True, return_inverse=True, axis=1) face_nodes_indices = face_nodes_indices[:, IA].ravel("F") num_face_nodes = face_nodes_indices.size face_nodes_indptr = np.arange(0, num_face_nodes + 1, 2) face_nodes = sps.csc_matrix( (np.ones(num_face_nodes), face_nodes_indices, face_nodes_indptr)) cell_idx = pp.utils.matrix_compression.rldecode(np.arange(num_face_nodes), num_nodes_per_cell) data = np.ones(IC.shape) _, ix = np.unique(IC, return_index=True) data[ix] *= -1 cell_faces = sps.coo_matrix((data, (IC, cell_idx))).tocsc() # Append information on subgrid extraction to the new grid's history name = list(g.name) name.append("Extract subgrid") h = pp.Grid(g.dim - 1, g.nodes[:, unique_nodes], face_nodes, cell_faces, name) if is_planar: # We could now just copy the corresponding geometric values from g to h, but we # run h.compute_geometry() to check if everything went ok. h.compute_geometry() if not np.all(np.isclose(g.face_areas[f], h.cell_volumes)): raise AssertionError( """Somethign went wrong in extracting subgrid. Face area of higher dim is not equal face centers of lower dim grid""") if not np.all(np.isclose(g.face_centers[:, f], h.cell_centers)): raise AssertionError( """Somethign went wrong in extracting subgrid. Face centers of higher dim is not equal cell centers of lower dim grid""") h.cell_volumes = g.face_areas[f] h.cell_centers = g.face_centers[:, f] h.parent_face_ind = f return h, f, unique_nodes
def extract_subgrid(g, c, sort=True, faces=False): """ Extract a subgrid based on cell/face indices. For simplicity the cell/face indices will be sorted before the subgrid is extracted. If the parent grid has geometry attributes (cell centers etc.) these are copied to the child. No checks are done on whether the cells/faces form a connected area. The method should work in theory for non-connected cells, the user will then have to decide what to do with the resulting grid. This option has however not been tested. Parameters: g (core.grids.Grid): Grid object, parent c (np.array, dtype=int): Indices of cells to be extracted sort=True (bool): If true c is sorted faces=False (bool): If true c are intrepetred as faces, and the exptracted grid will be a lower dimensional grid defined by the these faces Returns: Grid: Extracted subgrid. Will share (note, *not* copy) geometric fileds with the parent grid. Also has an additional field parent_cell_ind giving correspondance between parent and child cells. np.ndarray, dtype=int: Index of the extracted faces, ordered so that element i is the global index of face i in the subgrid. np.ndarray, dtype=int: Index of the extracted nodes, ordered so that element i is the global index of node i in the subgrid. """ if sort: c = np.sort(np.atleast_1d(c)) if faces: return __extract_cells_from_faces(g, c) # Local cell-face and face-node maps. cf_sub, unique_faces = __extract_submatrix(g.cell_faces, c) fn_sub, unique_nodes = __extract_submatrix(g.face_nodes, unique_faces) # Append information on subgrid extraction to the new grid's history name = list(g.name) name.append("Extract subgrid") # Construct new grid. h = pp.Grid(g.dim, g.nodes[:, unique_nodes], fn_sub, cf_sub, name) # Copy geometric information if any if hasattr(g, "cell_centers"): h.cell_centers = g.cell_centers[:, c] if hasattr(g, "cell_volumes"): h.cell_volumes = g.cell_volumes[c] if hasattr(g, "face_centers"): h.face_centers = g.face_centers[:, unique_faces] if hasattr(g, "face_normals"): h.face_normals = g.face_normals[:, unique_faces] if hasattr(g, "face_areas"): h.face_areas = g.face_areas[unique_faces] h.parent_cell_ind = c return h, unique_faces, unique_nodes
def _extrude_1d( g: pp.TensorGrid, z: np.ndarray ) -> Tuple[pp.Grid, np.ndarray, np.ndarray]: """ Extrude a 1d grid into 2d by prismatic extension in the z-direction. The original grid is assumed to be in the xy-plane, that is, any existing non-zero z-direction is ignored. Both the original and the new grid will have their geometry computed. Parameters: g (pp.Grid): Original grid to be extruded. Should have dimension 1. z (np.ndarray): z-coordinates of the nodes in the extruded grid. Should be either non-negative or non-positive, and be sorted in increasing or decreasing order, respectively. Returns: pp.Grid: A grid of dimension 2. np.array of np.arrays: Cell mappings, so that element ci gives all indices of cells in the extruded grid that comes from cell ci in the original grid. np.array of np.arrays: Face mappings, so that element fi gives all indices of faces in the extruded grid that comes from face fi in the original grid. """ # Number of cells in the grid num_cell_layers = z.size - 1 # Node coordinates can be extruded in using a tensor product x = g.nodes[0] y = g.nodes[1] x_2d, z_2d = np.meshgrid(x, z) y_2d, _ = np.meshgrid(y, z) nodes = np.vstack((x_2d.ravel(), y_2d.ravel(), z_2d.ravel())) # Bookkeeping nn_old = g.num_nodes nc_old = g.num_cells nf_old = g.num_faces nn_new = g.num_nodes * (num_cell_layers + 1) nc_new = g.num_cells * num_cell_layers nf_new = g.num_faces * num_cell_layers + g.num_cells * (num_cell_layers + 1) fn_old = g.face_nodes.indices # Vertical faces are made by extruding old face-node relation fn_vert = np.empty((2, 0), dtype=np.int) for k in range(num_cell_layers): fn_this = k * nn_old + np.vstack((fn_old, nn_old + fn_old)) fn_vert = np.hstack((fn_vert, fn_this)) # Horizontal faces are defined from the old cell-node relation # Implementation note: This operation is much simpler than in the 2d-3d operation, # since much less is assumed on the ordering of face-nodes for 2d grids than 3d cn_old = g.cell_nodes().indices.reshape((2, g.num_cells), order="F") # Bottom layer fn_hor = cn_old # All other layers for k in range(num_cell_layers): fn_hor = np.hstack((fn_hor, cn_old + (k + 1) * nn_old)) # Finalize the face-node map fn_rows = np.hstack((fn_vert, fn_hor)) fn_cols = np.tile(np.arange(fn_rows.shape[1]), (2, 1)) fn_data = np.ones(fn_cols.size, dtype=np.bool) fn = sps.coo_matrix( (fn_data, (fn_rows.ravel("f"), fn_cols.ravel("f"))), shape=(nn_new, nf_new) ).tocsc() # Next, cell-faces # We know there are exactly four faces for each cell cf_rows = np.empty((4, 0), dtype=np.int) cf_old = g.cell_faces.indices.reshape((2, -1), order="f") # Create vertical and horizontal faces together for k in range(num_cell_layers): # Vertical faces are identified by the old cell-face relation cf_vert_this = nn_old * k + cf_old # Put horizontal faces on top and bottom cf_hor_this = np.vstack((np.arange(nc_old), np.arange(nc_old) + nc_old)) # Add an offset of the number of vertical faces in the grid + previous horizontal # faces cf_hor_this += nf_old * num_cell_layers + k * nc_old cf_rows = np.hstack((cf_rows, np.vstack((cf_vert_this, cf_hor_this)))) # Finalize Cell-face relation cf_rows = cf_rows.ravel("f") cf_cols = np.tile(np.arange(nc_new), (4, 1)).ravel("f") # Define positive and negative sides. The choices here are somewhat arbitrary. tmp = np.ones(nc_new, dtype=np.int) cf_data = np.vstack((-tmp, tmp, -tmp, tmp)).ravel("f") cf = sps.coo_matrix((cf_data, (cf_rows, cf_cols)), shape=(nf_new, nc_new)).tocsc() tags = _define_tags(g, num_cell_layers) # We are ready to define the new grid g_info = g.name.copy() g_info.append("Extrude 1d->2d") g_new = pp.Grid(2, nodes, fn, cf, g_info, tags=tags) g_new.compute_geometry() if hasattr(g, "frac_num"): g_new.frac_num = g.frac_num # Mappings between old and new cells and faces cell_map, face_map = _create_mappings(g, g_new, num_cell_layers) return g_new, cell_map, face_map
def _extrude_2d(g: pp.Grid, z: np.ndarray) -> Tuple[pp.Grid, np.ndarray, np.ndarray]: """ Extrude a 2d grid into 3d by prismatic extension. The original grid is assumed to be in the xy-plane, that is, any existing non-zero z-direction is ignored. Both the original and the new grid will have their geometry computed. Parameters: g (pp.Grid): Original grid to be extruded. Should have dimension 2. z (np.ndarray): z-coordinates of the nodes in the extruded grid. Should be either non-negative or non-positive, and be sorted in increasing or decreasing order, respectively. Returns: pp.Grid: A grid of dimension 3. np.array of np.arrays: Cell mappings, so that element ci gives all indices of cells in the extruded grid that comes from cell ci in the original grid. np.array of np.arrays: Face mappings, so that element fi gives all indices of faces in the extruded grid that comes from face fi in the original grid. """ g.compute_geometry() negative_extrusion = np.all(z <= 0) ## Bookkeeping of the number of grid items # Number of nodes in the z-direction num_node_layers = z.size # Number of cell layers, one less than the nodes num_cell_layers = num_node_layers - 1 # Short hand for the number of cells in the 2d grid nc_2d = g.num_cells nf_2d = g.num_faces nn_2d = g.num_nodes # The number of nodes in the 3d grid is given by the number of 2d nodes, and the # number of node layers nn_3d = nn_2d * num_node_layers # The 3d cell count is similar to that for the nodes nc_3d = nc_2d * num_cell_layers # The number of faces is more intricate: In each layer of cells, there will be as # many faces as there is in the 2d grid. In addition, in the direction of extrusion # there will be one set of faces per node layer, with each layer containing as many # faces as there are cells in the 2d grid nf_3d = nf_2d * num_cell_layers + nc_2d * num_node_layers ## Nodes - only coorinades are needed # The nodes in the 2d grid are copied for all layers, with the z-coordinates changed # for each layer. This means that for a vertical pilar, the face-node and cell-node # relations can be inferred from that in the original 2d grid, with index increments # of size nn_2d x_layer = g.nodes[0] y_layer = g.nodes[1] nodes = np.empty((3, 0)) # Stack the layers of nodes for zloc in z: nodes = np.hstack((nodes, np.vstack((x_layer, y_layer, zloc * np.ones(nn_2d))))) ## Face-node relations # The 3d grid has two types of faces: Those formed by faces in the 2d grid, termed # 'vertical' below, and those on the top and bottom of the 3d cells, termed # horizontal # Face-node relation for the 2d grid. We know there are exactly two nodes in each # 2d face. fn_2d = g.face_nodes.indices.reshape((2, g.num_faces), order="F") # Nodes of the faces for the bottom layer of 3d cells. These are formed by # connecting nodes in the bottom layer with those immediately above fn_layer = np.vstack((fn_2d[0], fn_2d[1], fn_2d[1] + nn_2d, fn_2d[0] + nn_2d)) # For the vertical cells, the flux direction indicated in cell_face map will be # inherited from the 2d grid (see below). The normal vector, which should be # consistent with this value, is effectively computed from the ordering of the # face-node relation (and the same is true for several other geometric quantities). # This requires that the face-nodes are sorted in a CCW order when seen from the # side of a positive cell_face value. To sort this out, we need to flip some of the # columns in fn_layer # Faces, cells and values of the 2d cell-face map [fi, ci, sgn] = sps.find(g.cell_faces) # Only consider each face once _, idx = np.unique(fi, return_index=True) # The node ordering in fn_layer will be CCW seen from cell ci if the cell center of # ci is CW relative to the line from the first to the second node of the 2d cell. # # Example: with p0 = [0, 0, 0], p1 = [1, 0, 0], the 3d face will have further nodes # p2 = [1, 0, 1], p3 = [0, 0, 1]. # This will be counterclockwise to a 2d cell center of, say, [0.5, -0.5, 0], # (which is CW relative to p0 and p1) # p0 = g.nodes[:, fn_2d[0, fi[idx]]] p1 = g.nodes[:, fn_2d[1, fi[idx]]] pc = g.cell_centers[:, ci[idx]] ccw_2d = pp.geometry_property_checks.is_ccw_polyline(p0, p1, pc) # We should flip those columns in fn_layer where the sign is positive, and the 2d # is not ccw (meaning the 3d will be). Similarly, also flip negative signs and 2d # ccw. flip = np.logical_or( np.logical_and(sgn[idx] > 0, np.logical_not(ccw_2d)), np.logical_and(sgn[idx] < 0, ccw_2d), ) # Finally, if the extrusion is in the negative direction, the ordering of all # face-node relations is the oposite of that indicated above. if negative_extrusion: flip = np.logical_not(flip) fn_layer[:, flip] = fn_layer[np.array([1, 0, 3, 2])][:, flip] # The face-node relation for the vertical cells are found by stacking those in the # bottom layer, with an appropriate offset. This also implies that the vertical # faces of a cell in layer k are the same as the faces of the corresponding 2d cell, # with the appropriate adjustments for the number of faces and cells in each layer fn_rows_vertical = np.empty((4, 0)) # Loop over all layers of cells for k in range(num_cell_layers): fn_rows_vertical = np.hstack((fn_rows_vertical, fn_layer + nn_2d * k)) # Reshape the node indices into a single array fn_rows_vertical = fn_rows_vertical.ravel("F") # All vertical faces have exactly four nodes nodes_per_face_vertical = 4 # Aim for a csc-representation of the faces. Column pointers fn_cols_vertical = np.arange( 0, nodes_per_face_vertical * nf_2d * num_cell_layers, nodes_per_face_vertical ) # Next, deal with the horizontal faces. The face-node relation is based on the # cell-node relation of the 2d grid. # The structure of this constrution is a bit more involved than for the vertical # faces, since the 2d cells have an unknown, and generally varying, number of nodes cn_2d = g.cell_nodes() # Short hand for node indices of each cell. cn_ind_2d = cn_2d.indices.copy() # Similar to the vertical faces, the face-node relation in 3d should match the # sign in the cell-face relation, so that the generated normal vector points out of # the cell with cf-value 1. # This requires a sorting of the nodes for each cell for ci in range(nc_2d): # Node indices of this 2d cell start = cn_2d.indptr[ci] stop = cn_2d.indptr[ci + 1] ni = cn_ind_2d[start:stop] coord = g.nodes[:2, ni] # Sort the points. # IMPLEMENTATION NOTE: this probably assumes convexity of the 2d cell. sort_ind = pp.utils.sort_points.sort_point_plane( np.vstack((coord, np.zeros(coord.shape[1]))), g.cell_centers[:, ci].reshape((-1, 1)), ) # Indices that sort the nodes. The sort function contains a rotation, which # implies that it is unknown whether the ordering is cw or ccw # If the sorted points are ccw, we store them, unless the extrusion is negative # in which case the ordering should be cw, and the points are turned. if pp.geometry_property_checks.is_ccw_polygon(coord[:, sort_ind]): if negative_extrusion: cn_ind_2d[start:stop] = cn_ind_2d[start:stop][sort_ind[::-1]] else: cn_ind_2d[start:stop] = cn_ind_2d[start:stop][sort_ind] # Else, the ordering should be negative. elif pp.geometry_property_checks.is_ccw_polygon(coord[:, sort_ind[::-1]]): if negative_extrusion: cn_ind_2d[start:stop] = cn_ind_2d[start:stop][sort_ind] else: cn_ind_2d[start:stop] = cn_ind_2d[start:stop][sort_ind[::-1]] else: raise ValueError("this should not happen. Is the cell non-convex??") # Compressed column storage for horizontal faces: Store node indices fn_rows_horizontal = np.array([], dtype=np.int) # .. and pointers to the start of new faces fn_cols_horizontal = np.array(0, dtype=np.int) # Loop over all layers of nodes (one more than number of cells) # This means that the horizontal faces of a given cell is given by its index (bottom) # and its index + the number of 2d cells, both offset with the total number of # vertical faces for k in range(num_node_layers): # The horizontal cell-node relation for this layer is the bottom one, plus an # offset of the number of 2d nodes, per layer fn_rows_horizontal = np.hstack((fn_rows_horizontal, cn_ind_2d + nn_2d * k)) # The index pointers are those of the 2d cell-node relation. # Adjustment for the vertical faces is done below # Drop the final element of the 2d indptr, which effectively signifies the end # of this array (we will add the corresponding element for the full array below) fn_cols_horizontal = np.hstack( (fn_cols_horizontal, cn_2d.indptr[1:] + cn_ind_2d.size * k) ) # Add the final element which marks the end of the array # fn_cols_horizontal = np.hstack((fn_cols_horizontal, fn_rows_horizontal.size)) # The horizontal faces are appended to the vertical ones. The node indices are the # same, but the face indices must be increased by the number of vertical faces num_vertical_faces = nf_2d * num_cell_layers fn_cols_horizontal += num_vertical_faces * nodes_per_face_vertical # Put together the vertical and horizontal data, create the face-node relation indptr = np.hstack((fn_cols_vertical, fn_cols_horizontal)).astype(np.int) indices = np.hstack((fn_rows_vertical, fn_rows_horizontal)).astype(np.int) data = np.ones(indices.size, dtype=np.int) # Finally, construct the face-node sparse matrix face_nodes = sps.csc_matrix((data, indices, indptr), shape=(nn_3d, nf_3d)) ### Next the cell-faces. # Similar to the face-nodes, the easiest option is first to deal with the vertical # faces, which can be inferred directly from faces in the 2d grid, and then the # horizontal direction. # IMPLEMENTATION NOTE: Since all cells have both horizontal and vertical faces, and # these are found in separate operations, the easiest way to assemble the 3d # cell-face matrix is to construct information for a coo-matrix (not compressed # storage), and then convert later. This has some overhead, but the alternative # is to combine and sort the face indices in the horizontal and vertical components # so that all faces of any cell is stored together. This is most conveniently # left to scipy sparse .tocsc() function ## Vertical faces # For the vertical faces, the information from the 2d grid can be copied cf_rows_2d = g.cell_faces.indices cf_cols_2d = g.cell_faces.indptr cf_data_2d = g.cell_faces.data cf_rows_vertical = np.array([], dtype=np.int) # For the cells, we will store the number of facqes for each cell. This will later # be expanded to a full set of cell indices cf_vertical_cell_count = np.array([], dtype=np.int) cf_data_vertical = np.array([]) for k in range(num_cell_layers): # The face indices are found from the 2d information, with increaments that # reflect how many layers of vertical faces there are below cf_rows_vertical = np.hstack((cf_rows_vertical, cf_rows_2d + k * nf_2d)) # The diff here gives the number of faces per cell cf_vertical_cell_count = np.hstack( (cf_vertical_cell_count, np.diff(cf_cols_2d)) ) # The data is just plus and minus ones, no need to adjust cf_data_vertical = np.hstack((cf_data_vertical, cf_data_2d)) # Expand information of the number of faces per cell into a corresponding full set # of cell indices cf_cols_vertical = pp.utils.matrix_compression.rldecode( np.arange(nc_3d), cf_vertical_cell_count ) ## Horizontal faces # There is one set of faces per layer of nodes. # The cell_face relation will assign -1 to the upper cells, and +1 to lower cells. # This corresponds to normal vectors pointing upwards. # The bottom and top layers are special, in that they have only one neighboring # cell. All other layers have two (they are internal) # Bottom layer cf_rows_horizontal = num_vertical_faces + np.arange(nc_2d) cf_cols_horizontal = np.arange(nc_2d) cf_data_horizontal = -np.ones(nc_2d, dtype=np.int) # Intermediate layers, note for k in range(1, num_cell_layers): # Face indices are given twice, for the lower and upper neighboring cell # The offset of the face index is the number of vertical faces plus the number # of horizontal faces in lower layers rows_here = ( num_vertical_faces + k * nc_2d + np.hstack((np.arange(nc_2d), np.arange(nc_2d))) ) cf_rows_horizontal = np.hstack((cf_rows_horizontal, rows_here)) # Cell indices, first of the lower layer, then of the upper cols_here = np.hstack( ((k - 1) * nc_2d + np.arange(nc_2d), k * nc_2d + np.arange(nc_2d)) ) cf_cols_horizontal = np.hstack((cf_cols_horizontal, cols_here)) # Data: +1 for the lower cells, -1 for the upper data_here = np.hstack((np.ones(nc_2d), -np.ones(nc_2d))) cf_data_horizontal = np.hstack((cf_data_horizontal, data_here)) # Top layer, with index offset for all other faces cf_rows_horizontal = np.hstack( ( cf_rows_horizontal, num_vertical_faces + num_cell_layers * nc_2d + np.arange(nc_2d), ) ) # Similarly, the cell indices of the topbost layer cf_cols_horizontal = np.hstack( (cf_cols_horizontal, (num_cell_layers - 1) * nc_2d + np.arange(nc_2d)) ) # Only +1 in the data (oposite to lowermost layer) cf_data_horizontal = np.hstack((cf_data_horizontal, np.ones(nc_2d))) # Merge horizontal and vertical layers cf_rows = np.hstack((cf_rows_horizontal, cf_rows_vertical)) cf_cols = np.hstack((cf_cols_horizontal, cf_cols_vertical)) cf_data = np.hstack((cf_data_horizontal, cf_data_vertical)) cell_faces = sps.coo_matrix( (cf_data, (cf_rows, cf_cols)), shape=(nf_3d, nc_3d) ).tocsc() tags = _define_tags(g, num_cell_layers) name = g.name.copy() name.append("Extrude 2d->3d") g_info = g.name.copy() g_info.append("Extrude 1d->2d") g_new = pp.Grid(3, nodes, face_nodes, cell_faces, g_info, tags=tags) g_new.compute_geometry() # Mappings between old and new cells and faces cell_map, face_map = _create_mappings(g, g_new, num_cell_layers) return g_new, cell_map, face_map
def grid_2d(self, pert_node: bool = False, flip_normal: bool = False) -> pp.Grid: """ Make a 2d unit square simplex grid with six cells: __________ |\ /| | \ / | | \ / | | \/ | |--------| | /\ | | / \ | | / \ | |/ \| ---------- pert_node pertubes one node in the grid. Leads to non-matching cells. flip_normal flips one normal vector in 2d grid adjacent to the fracture. Tests that there is no assumptions on direction of fluxes in the mortar coupling. """ nodes = np.array( [ [0, 0, 0], [1, 0, 0], [1, 0.5, 0], [0.5, 0.5, 0], [0, 0.5, 0], [0, 0.5, 0], [0.5, 0.5, 0], [1, 0.5, 0], [1, 1, 0], [0, 1, 0], ] ).T if pert_node: nodes[0, 3] = 0.75 fn = np.array( [ [0, 1], [1, 2], [2, 3], [3, 4], [4, 0], [0, 3], [3, 1], [5, 6], [6, 7], [7, 8], [8, 9], [9, 5], [9, 6], [6, 8], ] ).T cf = np.array( [[3, 4, 5], [0, 6, 5], [1, 2, 6], [7, 12, 11], [12, 13, 10], [8, 9, 13]] ).T cols = np.tile(np.arange(fn.shape[1]), (fn.shape[0], 1)).ravel("F") face_nodes = sps.csc_matrix((np.ones_like(cols), (fn.ravel("F"), cols))) cols = np.tile(np.arange(cf.shape[1]), (cf.shape[0], 1)).ravel("F") data = np.array([1, 1, 1, 1, -1, -1, 1, 1, 1, 1, -1, 1, 1, 1, -1, 1, 1, -1]) cell_faces = sps.csc_matrix((data, (cf.ravel("F"), cols))) g = pp.Grid(2, nodes, face_nodes, cell_faces, "TriangleGrid") g.compute_geometry() g.tags["fracture_faces"][[2, 3, 7, 8]] = 1 if False: # TODO: purge di = 0.1 g.cell_centers[0, 0] = 0.25 - di g.cell_centers[0, 2] = 0.75 + di g.cell_centers[0, 3] = 0.25 - di g.cell_centers[0, 5] = 0.75 + di di = 0.021 g.cell_centers[1, 0] = 0.5 - di g.cell_centers[1, 2] = 0.5 - di g.cell_centers[1, 3] = 0.5 + di g.cell_centers[1, 5] = 0.5 + di if flip_normal: g.face_normals[:, [UCC2]] *= -1 g.cell_faces[2, 2] *= -1 g.global_point_ind = np.arange(nodes.shape[1]) return g