def create_1d_grids( pts, cells, phys_names, cell_info, line_tag=constants.GmshConstants().PHYSICAL_NAME_FRACTURE_LINE): # Recover lines # There will be up to three types of physical lines: intersections (between # fractures), fracture tips, and auxiliary lines (to be disregarded) g_1d = [] # If there are no fracture intersections, we return empty lists if not 'line' in cells: return g_1d, np.empty(0) gmsh_const = constants.GmshConstants() line_tags = cell_info['line'][:, 0] line_cells = cells['line'] gmsh_tip_num = [] tip_pts = np.empty(0) for i, pn in enumerate(phys_names['line']): # Index of the final underscore in the physical name. Chars before this # will identify the line type, the one after will give index offset_index = pn[2].rfind('_') loc_line_cell_num = np.where(line_tags == pn[1])[0] loc_line_pts = line_cells[loc_line_cell_num, :] assert loc_line_pts.size > 1 line_type = pn[2][:offset_index] if line_type == gmsh_const.PHYSICAL_NAME_FRACTURE_TIP[:-1]: gmsh_tip_num.append(i) # We need not know which fracture the line is on the tip of (do # we?) tip_pts = np.append(tip_pts, np.unique(loc_line_pts)) elif line_type == line_tag[:-1]: loc_pts_1d = np.unique(loc_line_pts) # .flatten() loc_coord = pts[loc_pts_1d, :].transpose() g = create_embedded_line_grid(loc_coord, loc_pts_1d) g_1d.append(g) else: # Auxiliary line pass return g_1d, tip_pts
def __write_boundary_2d(self): constants = gridding_constants.GmshConstants() bound_line_ind = np.argwhere( self.lines[2] == constants.DOMAIN_BOUNDARY_TAG).ravel() bound_line = self.lines[:2, bound_line_ind] bound_line = sort_points.sort_point_pairs(bound_line, check_circular=True) s = '// Start of specification of domain' s += '// Define lines that make up the domain boundary \n' loop_str = '{' for i in range(bound_line.shape[1]): s += 'bound_line_' + str(i) + ' = newl; Line(bound_line_'\ + str(i) + ') ={' s += 'p' + str(int(bound_line[0, i])) + ', p' + \ str(int(bound_line[1, i])) + '}; \n' loop_str += 'bound_line_' + str(i) + ', ' s += '\n' loop_str = loop_str[:-2] # Remove last comma loop_str += '}; \n' s += '// Line loop that makes the domain boundary \n' s += 'Domain_loop = newll; \n' s += 'Line Loop(Domain_loop) = ' + loop_str s += 'domain_surf = news; \n' s += 'Plane Surface(domain_surf) = {Domain_loop}; \n' s += 'Physical Surface(\"' + constants.PHYSICAL_NAME_DOMAIN + \ '\") = {domain_surf}; \n' s += '// End of domain specification \n \n' return s
def __write_lines(self, embed_in=None): l = self.lines num_lines = l.shape[1] ls = '\n' s = '// Define lines ' + ls constants = gridding_constants.GmshConstants() if l.shape[0] > 2: lt = l[2] has_tags = True else: has_tags = False for i in range(num_lines): si = str(i) s += 'frac_line_' + si + '= newl; Line(frac_line_' + si \ + ') = {p' + str(l[0, i]) + ', p' + str(l[1, i]) \ + '};' + ls if has_tags: s += 'Physical Line(\"' if l[2, i] == constants.FRACTURE_TIP_TAG: s += constants.PHYSICAL_NAME_FRACTURE_TIP elif l[2, i] == constants.FRACTURE_INTERSECTION_LINE_TAG: s += constants.PHYSICAL_NAME_FRACTURE_LINE else: # This is a line that need not be physical (recognized by # the parser of output from gmsh). s += constants.PHYSICAL_NAME_AUXILIARY_LINE s += si + '\") = {frac_line_' + si + '};' + ls s += ls s += '// End of line specification ' + ls + ls return s
def __write_polygons(self): constants = gridding_constants.GmshConstants() ls = '\n' s = '// Start fracture specification' + ls for pi in range(len(self.polygons[0])): p = self.polygons[0][pi].astype('int') reverse = self.polygons[1][pi] # First define line loop s += 'frac_loop_' + str(pi) + ' = newll; ' s += 'Line Loop(frac_loop_' + str(pi) + ') = { ' for i, li in enumerate(p): if reverse[i]: s += '-' s += 'frac_line_' + str(li) if i < p.size - 1: s += ', ' s += '};' + ls # Then the surface s += 'fracture_' + str(pi) + ' = news; ' s += 'Plane Surface(fracture_' + str(pi) + ') = {frac_loop_' \ + str(pi) + '};' + ls s += 'Physical Surface(\"' + constants.PHYSICAL_NAME_FRACTURES \ + str(pi) + '\") = {fracture_' + str(pi) + '};' + ls s += 'Surface{fracture_' + str(pi) + '} In Volume{1};' + ls + ls s += '// End of fracture specification' + ls + ls return s
def __write_boundary_3d(self): # Write the bounding box in 3D # Pull out bounding coordinates xmin = str(self.domain['xmin']) + ', ' xmax = str(self.domain['xmax']) + ', ' ymin = str(self.domain['ymin']) + ', ' ymax = str(self.domain['ymax']) + ', ' zmin = str(self.domain['zmin']) # + ', ' zmax = str(self.domain['zmax']) # + ', ' # Add mesh size on boundary points if these are provided if self.lchar_bound is not None: zmin += ', ' zmax += ', ' h = str(self.lchar_bound) + '};' else: h = '};' ls = '\n' constants = gridding_constants.GmshConstants() s = '// Define bounding box \n' # Points in bottom of box s += 'p_bound_000 = newp; Point(p_bound_000) = {' s += xmin + ymin + zmin + h + ls s += 'p_bound_100 = newp; Point(p_bound_100) = {' s += xmax + ymin + zmin + h + ls s += 'p_bound_110 = newp; Point(p_bound_110) = {' s += xmax + ymax + zmin + h + ls s += 'p_bound_010 = newp; Point(p_bound_010) = {' s += xmin + ymax + zmin + h + ls s += ls # Lines connecting points s += 'bound_line_1 = newl; Line(bound_line_1) = { p_bound_000,' \ + 'p_bound_100};' + ls s += 'bound_line_2 = newl; Line(bound_line_2) = { p_bound_100,' \ + 'p_bound_110};' + ls s += 'bound_line_3 = newl; Line(bound_line_3) = { p_bound_110,' \ + 'p_bound_010};' + ls s += 'bound_line_4 = newl; Line(bound_line_4) = { p_bound_010,' \ + 'p_bound_000};' + ls s += 'bottom_loop = newll;' + ls s += 'Line Loop(bottom_loop) = {bound_line_1, bound_line_2, ' \ + 'bound_line_3, bound_line_4};' + ls s += 'bottom_surf = news;' + ls s += 'Plane Surface(bottom_surf) = {bottom_loop};' + ls dz = self.domain['zmax'] - self.domain['zmin'] s += 'Extrude {0, 0, ' + str(dz) + '} {Surface{bottom_surf}; }' + ls s += 'Physical Volume(\"' + \ constants.PHYSICAL_NAME_DOMAIN + '\") = {1};' + ls s += '// End of domain specification ' + ls + ls return s
def __write_physical_points(self): ls = '\n' s = '// Start physical point specification' + ls constants = gridding_constants.GmshConstants() for i, p in enumerate(self.intersection_points): s += 'Physical Point(\"' + constants.PHYSICAL_NAME_FRACTURE_POINT \ + str(i) + '\") = {p' + str(p) + '};' + ls s += '// End of physical point specification ' + ls + ls return s
def __merge_domain_fracs_2d(dom, frac_p, frac_l): """ Merge fractures, domain boundaries and lines for compartments. The unified description is ready for feeding into meshing tools such as gmsh Parameters: dom: dictionary defining domain. fields xmin, xmax, ymin, ymax frac_p: np.ndarray. Points used in fracture definition. 2 x num_points. frac_l: np.ndarray. Connection between fracture points. 2 x num_fracs returns: p: np.ndarary. Merged list of points for fractures, compartments and domain boundaries. l: np.ndarray. Merged list of line connections (first two rows), tag identifying which type of line this is (third row), and a running index for all lines (fourth row) """ # Use constants set outside. If we ever const = constants.GmshConstants() # First create lines that define the domain x_min = dom['xmin'] x_max = dom['xmax'] y_min = dom['ymin'] y_max = dom['ymax'] dom_p = np.array([[x_min, x_max, x_max, x_min], [y_min, y_min, y_max, y_max]]) dom_lines = np.array([[0, 1], [1, 2], [2, 3], [3, 0]]).T num_dom_lines = dom_lines.shape[1] # Should be 4 # The lines will have all fracture-related tags set to zero. # The plan is to ignore these tags for the boundary and compartments, # so it should not matter dom_tags = const.DOMAIN_BOUNDARY_TAG * np.ones((1, num_dom_lines)) dom_l = np.vstack((dom_lines, dom_tags)) # Also add a tag to the fractures, signifying that these are fractures frac_l = np.vstack((frac_l, const.FRACTURE_TAG * np.ones(frac_l.shape[1]))) # Merge the point arrays, compartment points first p = np.hstack((dom_p, frac_p)) # Adjust index of fracture points to account for the compart points frac_l[:2] += dom_p.shape[1] l = np.hstack((dom_l, frac_l)).astype('int') # Add a second tag as an identifier of each line. l = np.vstack((l, np.arange(l.shape[1]))) return p, l
def __write_fractures_compartments_2d(self): # Both fractures and compartments are constants = gridding_constants.GmshConstants() frac_ind = np.argwhere( np.logical_or(self.lines[2] == constants.COMPARTMENT_BOUNDARY_TAG, self.lines[2] == constants.FRACTURE_TAG)).ravel() frac_lines = self.lines[:, frac_ind] s = '// Start specification of fractures \n' for i in range(frac_ind.size): s += 'frac_line_' + str(i) + ' = newl; Line(frac_line_' + str(i) \ + ') ={' s += 'p' + str(int(frac_lines[0, i])) + ', p' \ + str(int(frac_lines[1, i])) + '}; \n' s += 'Physical Line(\"' + constants.PHYSICAL_NAME_FRACTURES \ + str(i) + '\") = { frac_line_' + str(i) + ' };\n' s += 'Line{ frac_line_' + str(i) + '} In Surface{domain_surf}; \n' s += '\n' s += '// End of fracture specification \n\n' return s
def __find_intersection_points(lines): const = constants.GmshConstants() frac_id = np.ravel(lines[:2, lines[2] == const.FRACTURE_TAG]) _, ia, count = np.unique(frac_id, True, False, True) return frac_id[ia[count > 1]]
def triangle_grid(fracs, domain, **kwargs): """ Generate a gmsh grid in a 2D domain with fractures. The function uses modified versions of pygmsh and mesh_io, both downloaded from github. To be added: Functionality for tuning gmsh, including grid size, refinements, etc. Examples >>> p = np.array([[-1, 1, 0, 0], [0, 0, -1, 1]]) >>> lines = np.array([[0, 2], [1, 3]]) >>> char_h = 0.5 * np.ones(p.shape[1]) >>> tags = np.array([1, 3]) >>> fracs = {'points': p, 'edges': lines} >>> box = {'xmin': -2, 'xmax': 2, 'ymin': -2, 'ymax': 2} >>> path_to_gmsh = '~/gmsh/bin/gmsh' >>> g = create_grid(fracs, box, gmsh_path=path_to_gmsh) >>> plot_grid.plot_grid(g) Parameters ---------- fracs: (dictionary) Two fields: points (2 x num_points) np.ndarray, lines (2 x num_lines) connections between points, defines fractures. box: (dictionary) keys xmin, xmax, ymin, ymax, [together bounding box for the domain] **kwargs: To be explored. Must contain the key 'gmsh_path' Returns ------- list (length 3): For each dimension (2 -> 0), a list of all grids in that dimension. """ # Verbosity level verbose = kwargs.get('verbose', 1) # File name for communication with gmsh file_name = kwargs.get('file_name', 'gmsh_frac_file') in_file = file_name + '.geo' out_file = file_name + '.msh' # Pick out fracture points, and their connections frac_pts = fracs['points'] frac_con = fracs['edges'] # Unified description of points and lines for domain, and fractures pts_all, lines = __merge_domain_fracs_2d(domain, frac_pts, frac_con) # We split all fracture intersections so that the new lines do not # intersect, except possible at the end points dx = np.array([[domain['xmax'] - domain['xmin']], [domain['ymax'] - domain['ymin']]]) pts_split, lines_split = cg.remove_edge_crossings(pts_all, lines, box=dx) # We find the end points that is shared by more than one intersection intersections = __find_intersection_points(lines_split) # Constants used in the gmsh.geo-file const = constants.GmshConstants() # Gridding size lchar_dom = kwargs.get('lchar_dom', None) lchar_frac = kwargs.get('lchar_frac', lchar_dom) # gmsh options gmsh_path = kwargs.get('gmsh_path') gmsh_verbose = kwargs.get('gmsh_verbose', verbose) gmsh_opts = {'-v': gmsh_verbose} # Create a writer of gmsh .geo-files gw = gmsh_interface.GmshWriter(pts_split, lines_split, domain=domain, mesh_size=lchar_dom, mesh_size_bound=lchar_frac, intersection_points=intersections) gw.write_geo(in_file) # Run gmsh gmsh_status = gmsh_interface.run_gmsh(gmsh_path, in_file, out_file, dims=2, **gmsh_opts) if verbose > 0: start_time = time.time() if gmsh_status == 0: print('Gmsh processed file successfully') else: print('Gmsh failed with status ' + str(gmsh_status)) pts, cells, phys_names, cell_info = mesh_io.read(out_file) # Create grids from gmsh mesh. g_2d = mesh_2_grid.create_2d_grids(pts, cells, is_embedded=False) g_1d, _ = mesh_2_grid.create_1d_grids( pts, cells, phys_names, cell_info, line_tag=const.PHYSICAL_NAME_FRACTURES) g_0d = mesh_2_grid.create_0d_grids(pts, cells) grids = [g_2d, g_1d, g_0d] if verbose > 0: print('\n') print('Grid creation completed. Elapsed time ' + str(time.time() - start_time)) print('\n') for g_set in grids: if len(g_set) > 0: s = 'Created ' + str(len(g_set)) + ' ' + str(g_set[0].dim) + \ '-d grids with ' num = 0 for g in g_set: num += g.num_cells s += str(num) + ' cells' print(s) print('\n') return grids
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. """ 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 f in fracs: 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- or y-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_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() pts = network.decomposition['points'] edges = network.decomposition['edges'] poly = network._poly_2_segment() edge_tags, intersection_points = network._classify_edges(poly) edges = np.vstack((edges, edge_tags)) const = constants.GmshConstants() 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. 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] 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. for p in intersection_points: node = np.argmin(cg.dist_point_pointset(p, g_3d.nodes)) 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