def update_physical_high_grid(mg, g_new, g_old, tol): split_matrix = {} if mg.dim == 0: # retrieve the old faces and the corresponding coordinates _, old_faces, _ = sps.find(mg.high_to_mortar_int) old_nodes = g_old.face_centers[:, old_faces] # retrieve the boundary faces and the corresponding coordinates new_faces = g_new.get_boundary_faces() new_nodes = g_new.face_centers[:, new_faces] # we assume only one old node mask = cg.dist_point_pointset(old_nodes, new_nodes) < tol new_faces = new_faces[mask] shape = (g_old.num_faces, g_new.num_faces) matrix_DIJ = (np.ones(old_faces.shape), (old_faces, new_faces)) split_matrix = sps.csc_matrix(matrix_DIJ, shape=shape) elif mg.dim == 1: # The case is conceptually similar to 0d, but quite a bit more # technical. Implementation is moved to separate function split_matrix = _match_grids_along_line_from_geometry( mg, g_new, g_old, tol) else: # should be mg.dim == 2 # It should be possible to use essentially the same approach as in 1d, # but this is not yet covered. raise NotImplementedError("Have not yet implemented this.") mg.update_high(split_matrix)
def remesh_1d(g_old, num_nodes, tol=1e-6): """ Create a new 1d mesh covering the same domain as an old one. The new grid is equispaced, and there is no guarantee that the nodes in the old and new grids are coincinding. Use with care, in particular for grids with internal boundaries. Parameters: g_old (grid): 1d grid to be replaced. num_nodes (int): Number of nodes in the new grid. tol (double, optional): Tolerance used to compare node coornidates (for mapping of boundary conditions). Defaults to 1e-6. Returns: grid: New grid. """ # Create equi-spaced nodes covering the same domain as the old grid theta = np.linspace(0, 1, num_nodes) start, end = g_old.get_boundary_nodes() # Not sure why the new axis was necessary. nodes = g_old.nodes[:, start, np.newaxis] * theta + g_old.nodes[:, end, np.newaxis] * (1. - theta) # Create the new grid, and assign nodes. g = TensorGrid(nodes[0, :]) g.nodes = nodes g.compute_geometry() # map the tags from the old grid to the new one # retrieve the old faces and the corresponding coordinates old_frac_faces = np.where(g_old.tags["fracture_faces"].ravel())[0] # compute the mapping from the old boundary to the new boundary # we need to go through the coordinates new_frac_face = [] for fi in old_frac_faces: nfi = np.where( cg.dist_point_pointset(g_old.face_centers[:, fi], nodes) < tol)[0] if len(nfi) > 0: new_frac_face.append(nfi[0]) # This can probably be made more elegant g.tags["fracture_faces"][new_frac_face] = True # Fracture tips should be on the boundary only. if np.any(g_old.tags["tip_faces"]): g.tags["tip_faces"] = g.tags["domain_boundary_faces"] return g
def remesh_1d(g_old, num_nodes, tol=1e-6): """ Create a new 1d mesh covering the same domain as an old one. The new grid is equispaced, and there is no guarantee that the nodes in the old and new grids are coincinding. Use with care, in particular for grids with internal boundaries. Parameters: g_old (grid): 1d grid to be replaced. num_nodes (int): Number of nodes in the new grid. tol (double, optional): Tolerance used to compare node coornidates (for mapping of boundary conditions). Defaults to 1e-6. Returns: grid: New grid. """ # Create equi-spaced nodes covering the same domain as the old grid theta = np.linspace(0, 1, num_nodes) start, end = g_old.get_all_boundary_nodes() # Not sure why the new axis was necessary. nodes = g_old.nodes[:, start, np.newaxis] * theta + g_old.nodes[:, end, np.newaxis] * (1. - theta) # Create the new grid, and assign nodes. g = TensorGrid(nodes[0, :]) g.nodes = nodes g.compute_geometry() # map the tags from the old grid to the new one # normally the tags are given at faces/point that are fixed the 1d mesh # we use this assumption to proceed. for f_old in np.arange(g_old.num_faces): # detect in the new grid which face is geometrically the same (upon a tolerance) # as in the old grid dist = cg.dist_point_pointset(g_old.face_centers[:, f_old], g.face_centers) f_new = np.where(dist < tol)[0] # if you find a match transfer all the tags from the face in the old grid to # the face in the new grid if f_new.size: if f_new.size != 1: raise ValueError( "It cannot be more than one face, something went wrong") for tag in pp.utils.tags.standard_face_tags(): g.tags[tag][f_new] = g_old.tags[tag][f_old] g.update_boundary_node_tag() return g
def _find_nodes_on_line(g, nx, s_pt, e_pt): """ We have the start and end point of the fracture. From this we find the start and end node and use the structure of the cartesian grid to find the intermediate nodes. """ s_node = np.argmin(cg.dist_point_pointset(s_pt, g.nodes)) e_node = np.argmin(cg.dist_point_pointset(e_pt, g.nodes)) # We make sure the nodes are ordered from low to high. if s_node > e_node: tmp = s_node s_node = e_node e_node = tmp # We now find the other grid nodes. We here use the node ordering of # meshgrid (which is used by the TensorGrid class). # We find the number of nodes along each dimension. From this we find the # jump in node number between two consecutive nodes. if np.all(np.isclose(s_pt[1:], e_pt[1:])): # x-line: nodes = np.arange(s_node, e_node + 1) elif np.all(np.isclose(s_pt[[0, 2]], e_pt[[0, 2]])): # y-line nodes = np.arange(s_node, e_node + 1, nx[0] + 1, dtype=int) elif nx.size == 3 and np.all(np.isclose(s_pt[0:2], e_pt[0:2])): # is z-line nodes = np.arange(s_node, e_node + 1, (nx[0] + 1) * (nx[1] + 1), dtype=int) else: raise RuntimeError( "Something went wrong. Found a diagonal intersection") return nodes
def update_cell_faces(g, delete_faces, new_faces, in_combined, fn_orig, node_coord_orig, tol=1e-4): """ Replace faces in a cell-face map. If faces have been refined (or otherwise modified), it is necessary to update the cell-face relation as well. This function does so, while taking care that the (implicit) mapping between cells and nodes is ordered so that geometry computation still works. The changes of g.cell_faces are done in-place. It is assumed that the new faces that replace an old are ordered along the common line. E.g. if a face with node coordinates (0, 0) and (3, 0) is replaced by three new faces of unit length, they should be ordered as 1. (0, 0) - (1, 0) 2. (1, 0) - (2, 0) 3. (2, 0) - (3, 0) Switching the order into 3, 2, 1 is okay, but, say, 1, 3, 2 will create problems. It is also tacitly assumed that each cell has at most one face deleted. Changing this may not be difficult, but has not been prioritized so far. The function has been tested in 2d only, reliability in 3d is unknown, but doubtful. Parameters: g (grid): To be updated. delete_faces (np.ndarray): Faces to be deleted, as found in g.cell_faces new_faces (np.ndarray): Index of new faces, as found in g.face_nodes in_combined (np.ndarray): Map between old and new faces. delete_faces[i] is replaced by new_faces[in_combined[i]:in_combined[i+1]]. fn_orig (np.ndarray): Face-node relation of the orginial grid, before update of faces. node_coord_orig (np.ndarray): Node coordinates of orginal grid, before update of nodes. tol (double, defaults to 1e-4): Small tolerance, used to compare coordinates of points. """ # nodes_per_face = g.dim cell_faces = g.cell_faces # Mapping from new deleted_2_new_faces = np.empty(in_combined.size - 1, dtype=object) # The nodes in the original 1d grid was sorted either in the same way, or # in the oposite order of the new grid. In the latter case, we need to # reverse the order of in_combined to reconstruct the old face-node # relations if in_combined[0] < in_combined[-1]: for i in range(deleted_2_new_faces.size): if in_combined[i] == in_combined[i + 1]: deleted_2_new_faces[i] = new_faces[in_combined[i]] else: deleted_2_new_faces[i] = new_faces[np.arange( in_combined[i], in_combined[i + 1])] # assert deleted_2_new_faces[i].size > 0, \ # str(i)+" "+str(in_combined[i])+" "+str(in_combined[i+1])+\ # " "+str(np.arange(in_combined[i], in_combined[i+1])) else: for i in range(deleted_2_new_faces.size): if in_combined[i] == in_combined[i + 1]: print(new_faces) deleted_2_new_faces[i] = new_faces[in_combined[i]] else: deleted_2_new_faces[i] = new_faces[np.arange( in_combined[i + 1], in_combined[i])] # assert deleted_2_new_faces[i].size > 0, \ # str(i)+" "+str(in_combined[i+1])+" "+str(in_combined[i])+\ # " "+str(np.arange(in_combined[i+1], in_combined[i])) # Now that we have mapping from old to new faces, also update face tags update_face_tags(g, delete_faces, deleted_2_new_faces) # The cell-face relations cf = cell_faces.indices indptr = cell_faces.indptr # Find elements in the cell-face relation that are also along the # intersection, and should be replaced hit = np.where(np.in1d(cf, delete_faces))[0] # Mapping from cell_face of 2d grid to cells in 1d grid. Can be combined # with deleted_2_new_faces to match new and old faces # Safeguarding (or stupidity?): Only faces along 1d grid have non-negative # index, but we should never hit any of the other elements cf_2_f = -np.ones(delete_faces.max() + 1, dtype=np.int) cf_2_f[delete_faces] = np.arange(delete_faces.size) # Map from faces, as stored in cell_faces,to the corresponding cells face_2_cell = rldecode(np.arange(indptr.size), np.diff(indptr)) # The cell-face map will go from 3 faces per cell to an arbitrary number. # Split mapping into list of arrays to prepare for this new_cf = [cf[indptr[i]:indptr[i + 1]] for i in range(g.num_cells)] # Similar treatment of direction of normal vectors new_sgn = [g.cell_faces.data[indptr[i]:indptr[i+1]] \ for i in range(g.num_cells)] # Create mapping to adjust face indices for deletions tmp = np.arange(cf.max() + 1) adjust_deleted = np.zeros_like(tmp) adjust_deleted[delete_faces] = 1 face_adjustment = tmp - np.cumsum(adjust_deleted) # Face-node relations as array fn = g.face_nodes.indices.reshape((nodes_per_face, g.num_faces), order='F') # Collect indices of cells that have one of their faces on the fracture. hit_cell = [] for i in hit: # The loop variable refers to indices in the face-cell map. Get cell # number. cell = face_2_cell[i] hit_cell.append(cell) # For this cell, find where in the cell-face map the fracture face is # placed. tr = np.where(new_cf[cell] == cf[i])[0] # There should be only one face on the fracture assert tr.size == 1 tr = tr[0] # Implementation note: If we ever get negative indices here, something # has gone wrong related to cf_2_f, see above. # Digestion of loop: i (in hit) refers to elements in cell-face # cf[i] is specific face # cf_2_f[cf[i]] maps to deleted face along fracture # outermost is one-to-many map from deleted to new faces. new_faces_loc = deleted_2_new_faces[cf_2_f[cf[i]]] # Index of the replaced face ci = cf[i] # We need to sort the new face-cell relation so that the edges defined # by cell-face-> face_nodes form a closed, non-intersecting loop. If # this is not the case, geometry computation will go wrong. # By assumption, the new faces are defined so that their nodes are # contiguous along the line of the old face. # Coordinates of the nodes of the replaced face. # Note use of original coordinates here. ci_coord = node_coord_orig[:, fn_orig[:, ci]] # Coordinates of the nodes of the first new face fi_coord = g.nodes[:, fn[:, new_faces_loc[0]]] # Distance between the new nodes and the first node of the old face. dist = cg.dist_point_pointset(ci_coord[:, 0], fi_coord) # Length of the old face. length_face = cg.dist_point_pointset(ci_coord[:, 0], ci_coord[:, 1])[0] # If the minimum distance is larger than a (scaled) tolerance, the new # faces were defined from the second to the first node. Switch order. # This will create trouble if one of the new faces are very small. if dist.min() > length_face * tol: new_faces_loc = new_faces_loc[::-1] # Replace the cell-face relation for this cell. # At the same time (stupid!) also adjust indices of the surviving # faces. new_cf[cell] = np.hstack( (face_adjustment[new_cf[cell][:tr].ravel()], new_faces_loc, face_adjustment[new_cf[cell][tr + 1:].ravel()])) # Also replicate directions of normal vectors new_sgn[cell] = np.hstack( (new_sgn[cell][:tr].ravel(), np.tile(new_sgn[cell][tr], new_faces_loc.size), new_sgn[cell][tr + 1:].ravel())) # Adjust face index of cells that have no contact with the updated faces for i in np.setdiff1d(np.arange(len(new_cf)), hit_cell): new_cf[i] = face_adjustment[new_cf[i]] # New pointer structure for cell-face relations num_cell_face = np.array([new_cf[i].size for i in range(len(new_cf))]) indptr_new = np.hstack((0, np.cumsum(num_cell_face))) ind = np.concatenate(new_cf) data = np.concatenate(new_sgn) # All faces in the cell-face relation should be referred to by 1 or 2 cells assert np.bincount(ind).max() <= 2 assert np.all(np.bincount(ind) > 0) g.cell_faces = sps.csc_matrix((data, ind, indptr_new))
def cart_grid_3d(fracs, nx, physdims=None): """ Create grids for a domain with possibly intersecting fractures in 3d. Based on rectangles describing the individual fractures, the method constructs grids in 3d (the whole domain), 2d (one for each individual fracture), 1d (along fracture intersections), and 0d (meeting between intersections). Parameters ---------- fracs (list of np.ndarray, each 3x4): Vertexes in the rectangle for each fracture. The vertices must be sorted and aligned to the axis. The fractures will snap to the closest grid faces. nx (np.ndarray): Number of cells in each direction. Should be 3D. physdims (np.ndarray): Physical dimensions in each direction. Defaults to same as nx, that is, cells of unit size. Returns ------- list (length 4): For each dimension (3 -> 0), a list of all grids in that dimension. Examples -------- frac1 = np.array([[1,1,4,4], [1,4,4,1], [2,2,2,2]]) frac2 = np.array([[2,2,2,2], [1,1,4,4], [1,4,4,1]]) fracs = [frac1, frac2] gb = cart_grid_3d(fracs, [5,5,5]) """ nx = np.asarray(nx) if physdims is None: physdims = nx elif np.asarray(physdims).size != nx.size: raise ValueError("Physical dimension must equal grid dimension") else: physdims = np.asarray(physdims) # We create a 3D cartesian grid. The global node mapping is trivial. g_3d = structured.CartGrid(nx, physdims=physdims) g_3d.global_point_ind = np.arange(g_3d.num_nodes) g_3d.compute_geometry() g_2d = [] g_1d = [] g_0d = [] # We set the tolerance for finding points in a plane. This can be any # small number, that is smaller than .25 of the cell sizes. tol = .1 * physdims / nx # Create 2D grids for fi, f in enumerate(fracs): assert np.all(f.shape == (3, 4)), "fractures must have shape [3,4]" is_xy_frac = np.allclose(f[2, 0], f[2]) is_xz_frac = np.allclose(f[1, 0], f[1]) is_yz_frac = np.allclose(f[0, 0], f[0]) assert (is_xy_frac + is_xz_frac + is_yz_frac == 1), "Fracture must align to x-, y- or z-axis" # snap to grid f_s = (np.round(f * nx[:, np.newaxis] / physdims[:, np.newaxis]) * physdims[:, np.newaxis] / nx[:, np.newaxis]) if is_xy_frac: flat_dim = [2] active_dim = [0, 1] elif is_xz_frac: flat_dim = [1] active_dim = [0, 2] else: flat_dim = [0] active_dim = [1, 2] # construct normal vectors. If the rectangle is ordered # clockwise we need to flip the normals so they point # outwards. sign = 2 * cg.is_ccw_polygon(f_s[active_dim]) - 1 tangent = f_s.take(np.arange(f_s.shape[1]) + 1, axis=1, mode="wrap") - f_s normal = tangent normal[active_dim] = tangent[active_dim[1::-1]] normal[active_dim[1]] = -normal[active_dim[1]] normal = sign * normal # We find all the faces inside the convex hull defined by the # rectangle. To find the faces on the fracture plane, we remove any # faces that are further than tol from the snapped fracture plane. in_hull = half_space.half_space_int(normal, f_s, g_3d.face_centers) f_tag = np.logical_and( in_hull, np.logical_and( f_s[flat_dim, 0] - tol[flat_dim] <= g_3d.face_centers[flat_dim], g_3d.face_centers[flat_dim] < f_s[flat_dim, 0] + tol[flat_dim], ), ) f_tag = f_tag.ravel() nodes = sps.find(g_3d.face_nodes[:, f_tag])[0] nodes = np.unique(nodes) loc_coord = g_3d.nodes[:, nodes] g = _create_embedded_2d_grid(loc_coord, nodes) g.frac_num = fi g_2d.append(g) # Create 1D grids: # Here we make use of the network class to find the intersection of # fracture planes. We could maybe avoid this by doing something similar # as for the 2D-case, and count the number of faces belonging to each edge, # but we use the FractureNetwork class for now. frac_list = [] for f in fracs: frac_list.append(fractures.Fracture(f)) # Combine the fractures into a network network = fractures.FractureNetwork(frac_list) # Impose domain boundary. For the moment, the network should be immersed in # the domain, or else gmsh will complain. box = { "xmin": 0, "ymin": 0, "zmin": 0, "xmax": physdims[0], "ymax": physdims[1], "zmax": physdims[2], } network.impose_external_boundary(box) # Find intersections and split them. network.find_intersections() network.split_intersections() # Extract geometrical network information. pts = network.decomposition["points"] edges = network.decomposition["edges"] poly = network._poly_2_segment() # And tags identifying points and edges corresponding to normal # fractures, domain boundaries and subdomain boundaries. Only the # entities corresponding to normal fractures should actually be gridded. edge_tags, intersection_points = network._classify_edges(poly) const = constants.GmshConstants() auxiliary_points, edge_tags = network.on_domain_boundary(edges, edge_tags) bound_and_aux = np.array([const.DOMAIN_BOUNDARY_TAG, const.AUXILIARY_TAG]) edges = np.vstack((edges, edge_tags)) # Loop through the edges to make 1D grids. Ommit the auxiliary edges. for e in np.ravel( np.where(edges[2] == const.FRACTURE_INTERSECTION_LINE_TAG)): # We find the start and end point of each fracture intersection (1D # grid) and then the corresponding global node index. if np.isin(edge_tags[e], bound_and_aux): continue s_pt = pts[:, edges[0, e]] e_pt = pts[:, edges[1, e]] nodes = _find_nodes_on_line(g_3d, nx, s_pt, e_pt) loc_coord = g_3d.nodes[:, nodes] assert (loc_coord.shape[1] > 1), "1d grid in intersection should span\ more than one node" g = mesh_2_grid.create_embedded_line_grid(loc_coord, nodes) g_1d.append(g) # Create 0D grids # Here we also use the intersection information from the FractureNetwork # class. No grids for auxiliary points. for p in intersection_points: if auxiliary_points[p]: continue node = np.argmin(cg.dist_point_pointset(pts[:, p], g_3d.nodes)) assert np.allclose(g_3d.nodes[:, node], pts[:, p]) g = point_grid.PointGrid(g_3d.nodes[:, node]) g.global_point_ind = np.asarray(node) g_0d.append(g) grids = [[g_3d], g_2d, g_1d, g_0d] return grids