def triangle_grid_from_gmsh(file_name, **kwargs): start_time = time.time() if file_name.endswith(".msh"): file_name = file_name[:-4] out_file = file_name + ".msh" # Verbosity level verbose = kwargs.get("verbose", 1) pts, cells, _, cell_info, phys_names = gmsh_io.read(out_file) # Invert phys_names dictionary to map from physical tags to corresponding # physical names. # As of meshio 1.10, the value of the physical name is defined as a numpy # array, with the first item being the tag, the second the dimension. phys_names = {v[0]: k for k, v in phys_names.items()} # Constants used in the gmsh.geo-file const = constants.GmshConstants() # Create grids from gmsh mesh. logger.info("Create grids of various dimensions") 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] logger.info( "Grid creation completed. Elapsed time " + str(time.time() - start_time) ) 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" logger.info(s) return grids
def _write_fractures_1d(self): # Both fractures and compartments are constants = gridding_constants.GmshConstants() # We consider fractures, boundary tag, an auxiliary tag (fake fractures/mesh # constraints) ind = np.argwhere( np.logical_or.reduce(( self.lines[2] == constants.COMPARTMENT_BOUNDARY_TAG, self.lines[2] == constants.FRACTURE_TAG, self.lines[2] == constants.AUXILIARY_TAG, ))).ravel() lines = self.lines[:, ind] tag = self.lines[2, ind] lines_id = lines[3, :] if lines_id.size == 0: return str() range_id = np.arange(np.amin(lines_id), np.amax(lines_id) + 1) s = "// Start specification of fractures/compartment boundary/auxiliary elements\n" seg_id = 0 for i in range_id: local_seg_id = str() for mask in np.flatnonzero(lines_id == i): # give different name for fractures/boundary and auxiliary if tag[mask] != constants.AUXILIARY_TAG: name = "frac_line_" physical_name = constants.PHYSICAL_NAME_FRACTURES else: name = "seg_line_" physical_name = constants.PHYSICAL_NAME_AUXILIARY s += (name + str(seg_id) + " = newl; " + "Line(" + name + str(seg_id) + ") = {" + "p" + str(int(lines[0, mask])) + ", p" + str(int(lines[1, mask])) + "};\n") local_seg_id += name + str(seg_id) + ", " seg_id += 1 local_seg_id = local_seg_id[:-2] s += ('Physical Line("' + physical_name + str(i) + '") = { ' + local_seg_id + " };\n") s += "\n" s += "// End of /compartment boundary/auxiliary elements specification\n\n" 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 _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[:, 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" bound_id = bound_line[3, :] range_id = np.arange(np.amin(bound_id), np.amax(bound_id) + 1) seg_id = 0 loop_str = "{" for i in range_id: local_bound_id = str() for mask in np.flatnonzero(bound_id == i): s += "bound_line_" + str(seg_id) + " = newl;\n" s += "Line(bound_line_" + str(seg_id) + ") ={" s += ("p" + str(int(bound_line[0, mask])) + ", p" + str(int(bound_line[1, mask])) + "};\n") loop_str += "bound_line_" + str(seg_id) + ", " local_bound_id += "bound_line_" + str(seg_id) + ", " seg_id += 1 local_bound_id = local_bound_id[:-2] s += ('Physical Line("' + constants.PHYSICAL_NAME_DOMAIN_BOUNDARY + str(i) + '") = { ' + local_bound_id + " };\n") 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). Applies to boundary and # subdomain boundary lines. 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_boundary_3d(self): ls = "\n" s = "// Start domain specification" + ls # Write surfaces: s += self._write_polygons(boundary=True) # Make a box out of them s += "domain_loop = newsl;" + ls s += "Surface Loop(domain_loop) = {" for pi in range(len(self.polygons[0])): if self.polygon_tags["boundary"][pi]: s += " boundary_surface_" + str(pi) + "," s = s[:-1] s += "};" + ls s += "Volume(1) = {domain_loop};" + ls s += ('Physical Volume("' + gridding_constants.GmshConstants().PHYSICAL_NAME_DOMAIN + '") = {1};' + ls) s += "// End of domain specification\n\n" return s
def __write_boundary_3d(self): ls = '\n' s = '// Start domain specification' + ls # Write surfaces: s += self.__write_polygons(boundary=True) # Make a box out of them s += 'domain_loop = newsl;' + ls s += 'Surface Loop(domain_loop) = {' for pi in range(len(self.polygons[0])): if self.polygon_tags['boundary'][pi]: s += 'auxiliary_' + str(pi) + ',' s = s[:-1] s += '};' + ls s += 'Volume(1) = {domain_loop};' + ls s += 'Physical Volume(\"' + \ gridding_constants.GmshConstants().PHYSICAL_NAME_DOMAIN + \ '\") = {1};' + ls s += '// End of domain specification\n\n' return s
def _find_intersection_points(lines): const = constants.GmshConstants() frac_id = np.ravel(lines[:2, lines[2] == const.FRACTURE_TAG]) _, frac_ia, frac_count = np.unique(frac_id, True, False, True) # In the case we have auxiliary points remove do not create a 0d point in # case one intersects a single fracture. In the case of multiple fractures intersection # with an auxiliary point do consider the 0d. aux_id = lines[2] == const.AUXILIARY_TAG if np.any(aux_id): aux_id = np.ravel(lines[:2, aux_id]) _, aux_ia, aux_count = np.unique(aux_id, True, False, True) # probably it can be done more efficiently but currently we rarely use the # auxiliary points in 2d for a in aux_id[aux_ia[aux_count > 1]]: # if a match is found decrease the frac_count only by one, this prevent # the multiple fracture case to be handle wrongly frac_count[frac_id[frac_ia] == a] -= 1 return frac_id[frac_ia[frac_count > 1]]
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_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) if self.domain_boundary_points is not None: for i, p in enumerate(self.domain_boundary_points): s += ('Physical Point("' + constants.PHYSICAL_NAME_BOUNDARY_POINT + str(i) + '") = {p' + str(p) + "};" + ls) if self.fracture_and_boundary_points is not None: for i, p in enumerate(self.fracture_and_boundary_points): s += ('Physical Point("' + constants.PHYSICAL_NAME_FRACTURE_BOUNDARY_POINT + str(i) + '") = {p' + str(p) + "};" + ls) s += "// End of physical point specification" + ls + ls return s
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] frac_id = frac_lines[3, :] if frac_id.size == 0: return str() range_id = np.arange(np.amin(frac_id), np.amax(frac_id) + 1) s = '// Start specification of fractures\n' seg_id = 0 for i in range_id: local_seg_id = str() for mask in np.flatnonzero(frac_id == i): s += 'frac_line_' + str(seg_id) + ' = newl; ' + \ 'Line(frac_line_' + str(seg_id) + ') = {' + \ 'p' + str(int(frac_lines[0, mask])) + \ ', p' + str(int(frac_lines[1, mask])) + \ '};\n' + \ 'Line{ frac_line_' + str(seg_id) + \ '} In Surface{domain_surf};\n' local_seg_id += 'frac_line_' + str(seg_id) + ', ' seg_id += 1 local_seg_id = local_seg_id[:-2] s += 'Physical Line(\"' + constants.PHYSICAL_NAME_FRACTURES \ + str(i) + '\") = { ' + local_seg_id + ' };\n' s += '\n' s += '// End of fracture specification\n\n' return s
def create_2d_grids( pts: np.ndarray, cells: Dict[str, np.ndarray], phys_names: Dict[str, str], cell_info: Dict, is_embedded: bool = False, surface_tag: str = None, constraints: np.ndarray = None, ) -> List[pp.Grid]: """Create 2d grids for lines of a specified type from a gmsh tessalation. Only surfaces that were defined as 'physical' in the gmsh sense may have a grid created, but then only if the physical name matches specified line_tag. It is assumed that the mesh is read by meshio. See porepy.fracs.simplex for how to do this. Parameters: pts (np.ndarray, npt x 3): Global point set from gmsh cells (dict): Should have a key 'triangle' which maps to a np.ndarray with indices of the points that form 2d grids. phys_names (dict): mapping from the gmsh tags assigned to physical entities to the physical name of that tag. cell_info (dictionary): Should have a key 'triangle' that contains the physical names (in the gmsh sense) of the points. is_embedded (boolean, optional): If True, the triangle grids are embedded in 3d space. If False (default), the grids are truly 2d. surface_tag (str, optional): The target physical name, all surfaces that have this tag will be assigned a grid. The string is assumed to be on the from BASE_NAME_OF_TAG_{INDEX}, where _INDEX is a number. The comparison is made between the physical names and the line, up to the last underscore. If not provided, the physical names of fracture surfaces will be used as target. constraints (np.array, optional): Array with lists of lines that should not become grids. The array items should match the INDEX in line_tag, see above. Returns: list of grids: List of 2d grids for all physical surfaces that matched with the specified target tag. """ gmsh_constants = constants.GmshConstants() if surface_tag is None: surface_tag = gmsh_constants.PHYSICAL_NAME_FRACTURES if constraints is None: constraints = np.array([], dtype=np.int) if is_embedded: # List of 2D grids, one for each surface g_list: List[pp.Grid] = [] # Special treatment of the case with no fractures if "triangle" not in cells: return g_list # Recover cells on fracture surfaces, and create grids tri_cells = cells["triangle"] # Tags of all triangle grids tri_tags = cell_info["triangle"] # Loop over all gmsh tags associated with triangle grids for pn_ind in np.unique(tri_tags): # Split the physical name into a category and a number - which will become # the fracture number pn = phys_names[pn_ind] offset = pn.rfind("_") frac_num = int(pn[offset + 1:]) plane_type = pn[:offset] # Check if the surface is of the target type, or if the surface is tagged # as a constraint if plane_type != surface_tag[:-1] or int( pn[offset + 1:]) in constraints: continue # Cells of this surface loc_cells = np.where(tri_tags == pn_ind)[0] loc_tri_cells = tri_cells[loc_cells, :].astype(np.int) # Find unique points, and a mapping from local to global points pind_loc, p_map = np.unique(loc_tri_cells, return_inverse=True) loc_tri_ind = p_map.reshape((-1, 3)) g = pp.TriangleGrid(pts[pind_loc, :].transpose(), loc_tri_ind.transpose()) # Add mapping to global point numbers g.global_point_ind = pind_loc # Associate a fracture id (corresponding to the ordering of the # frature planes in the original fracture list provided by the # user) g.frac_num = frac_num # Append to list of 2d grids g_list.append(g) # Done with all surfaces, return return g_list else: # Single grid triangles = cells["triangle"].transpose() # Construct grid g_2d: pp.Grid = pp.TriangleGrid(pts.transpose(), triangles) # we need to add the face tags from gmsh to the current mesh, however, # since there is not a cell-face relation from gmsh but only a cell-node # relation we need to recover the corresponding face-line map. # First find the nodes of each face faces = np.reshape(g_2d.face_nodes.indices, (2, -1), order="F") faces = np.sort(faces, axis=0) # Then we do a bunch of sorting to make sure faces and lines has the same # node ordering: idxf = np.lexsort(faces) line = np.sort(cells["line"].T, axis=0) idxl = np.lexsort(line) IC = np.empty(line.shape[1], dtype=int) IC[idxl] = np.arange(line.shape[1]) # Next change the faces and line to string format ("node_idx0,node_idx1"). # The reason to do so is because we want to compare faces and line columnwise, # i.e., is_line[i] should be true iff faces[:, i] == line[:, j] for ONE j. If # you can make numpy do this, you can remove the string formating. tmp = np.core.defchararray.add(faces[0, idxf].astype(str), ",") facestr = np.core.defchararray.add(tmp, faces[1, idxf].astype(str)) tmp = np.core.defchararray.add(line[0, idxl].astype(str), ",") linestr = np.core.defchararray.add(tmp, line[1, idxl].astype(str)) is_line = np.isin(facestr, linestr, assume_unique=True) # Now find the face index that correspond to each line. line2face is of length # line.shape[1] and we have: face[:, line2face] == line. line2face = idxf[is_line][IC] # Sanity check if not np.allclose(faces[:, line2face], line): raise RuntimeError( "Could not find mapping from gmsh lines to pp.Grid faces") # Now we can assign the correct tags to the grid. # First we add them as False and after we change for the correct # faces. The new tag name become the lower version of what gmsh gives # in the cell_info["line"]. The map phys_names recover the literal name. for tag in np.unique(cell_info["line"]): tag_name = phys_names[tag].lower() + "_faces" g_2d.tags[tag_name] = np.zeros(g_2d.num_faces, dtype=np.bool) # Add correct tag faces = line2face[cell_info["line"] == tag] g_2d.tags[tag_name][faces] = True # Create mapping to global numbering (will be a unit mapping, but is # crucial for consistency with lower dimensions) g_2d.global_point_ind = np.arange(pts.shape[0]) # Convert to list to be consistent with lower dimensions # This may also become useful in the future if we ever implement domain # decomposition approaches based on gmsh. return [g_2d]
def __write_polygons(self, boundary=False): """ Writes either all fractures or all boundary planes. """ constants = gridding_constants.GmshConstants() bound_tags = self.polygon_tags.get("boundary", [False] * len(self.polygons[0])) subd_tags = self.polygon_tags.get("subdomain", [False] * len(self.polygons[0])) ls = "\n" # Name boundary or fracture f_or_b = "auxiliary" if boundary else "fracture" if not boundary: s = "// Start fracture specification" + ls else: s = "" for pi in range(len(self.polygons[0])): if bound_tags[pi] != boundary: continue # Check if the polygon is a subdomain boundary, i.e., auxiliary # polygon. auxiliary = subd_tags[pi] if auxiliary: # Keep track of "fake fractures", i.e., subdomain # boundaries. f_or_b = "auxiliary" p = self.polygons[0][pi].astype("int") reverse = self.polygons[1][pi] # First define line loop s += "frac_loop_" + str(pi) + " = newll; " + ls 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 n = f_or_b + "_" # Then the surface s += n + str(pi) + " = news; " s += ( "Plane Surface(" + n + str(pi) + ") = {frac_loop_" + str(pi) + "};" + ls ) if bound_tags[pi] or auxiliary: # Domain boundary or "fake fracture" = subdomain boundary s += ( 'Physical Surface("' + constants.PHYSICAL_NAME_AUXILIARY + str(pi) + '") = {auxiliary_' + str(pi) + "};" + ls ) else: # Normal fracture s += ( 'Physical Surface("' + constants.PHYSICAL_NAME_FRACTURES + str(pi) + '") = {fracture_' + str(pi) + "};" + ls ) if self.domain is not None: s += "Surface{" + n + str(pi) + "} In Volume{1};" + ls + ls for li in self.e2f[pi]: s += "Line{frac_line_" + str(li) + "} In Surface{" + n s += str(pi) + "};" + ls s += ls if not boundary: s += "// End of fracture specification" + ls + ls return s
def create_1d_grids( pts, cells, phys_names, cell_info, line_tag=constants.GmshConstants().PHYSICAL_NAME_FRACTURE_LINE, tol=1e-4, constraints=None, **kwargs): if constraints is None: constraints = np.empty(0, dtype=np.int) # Recover lines # There will be up to three types of physical lines: intersections (between # fractures), fracture tips, and auxiliary lines (to be disregarded) # All intersection lines and points on boundaries are non-physical in 3d. # I.e., they are assigned boundary conditions, but are not gridded. 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"]["gmsh:physical"] line_cells = cells["line"] gmsh_tip_num = [] tip_pts = np.empty(0) for i, pn_ind in enumerate(np.unique(line_tags)): # Index of the final underscore in the physical name. Chars before this # will identify the line type, the one after will give index pn = phys_names[pn_ind] offset_index = pn.rfind("_") loc_line_cell_num = np.where(line_tags == pn_ind)[0] loc_line_pts = line_cells[loc_line_cell_num, :] assert loc_line_pts.size > 1 line_type = pn[:offset_index] # Try to get the fracture number from the physical name of the object # This will only work if everything after the '_' can be interpreted # as a number. Specifically, it should work for all meshes generated by # the standard PorePy procedure, but it may fail for externally generated # geo-files. If it fails, we simply set the frac_num to None in this case. try: frac_num = int(pn[offset_index + 1:]) except ValueError: frac_num = None # If this is a meshing constraint, but not a fracture, we don't need to do anything if frac_num in constraints: continue 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, tol=tol) g.frac_num = int(frac_num) g_1d.append(g) else: # Auxiliary line pass return g_1d, tip_pts
def _write_polygons(self, boundary=False): """ Writes either all fractures or all boundary planes. """ constants = gridding_constants.GmshConstants() bound_tags = self.polygon_tags.get("boundary", [False] * len(self.polygons[0])) constraint_tags = self.polygon_tags["constraint"] ls = "\n" # Name boundary or fracture if not boundary: s = "// Start fracture specification" + ls else: s = "// Start boundary surface specification" + ls for pi in range(len(self.polygons[0])): if bound_tags[pi] != boundary: continue p = self.polygons[0][pi].astype("int") reverse = self.polygons[1][pi] # First define line loop s += "frac_loop_" + str(pi) + " = newll; " + ls 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 if boundary: surf_stem = "boundary_surface_" else: if constraint_tags[pi]: surf_stem = "auxiliary_surface_" else: surf_stem = "fracture_" # Then the surface s += surf_stem + str(pi) + " = news; " s += ("Plane Surface(" + surf_stem + str(pi) + ") = {frac_loop_" + str(pi) + "};" + ls) if bound_tags[pi]: # Domain boundary s += ('Physical Surface("' + constants.PHYSICAL_NAME_DOMAIN_BOUNDARY_SURFACE + str(pi) + '") = {' + surf_stem + str(pi) + "};" + ls) elif constraint_tags[pi]: s += ('Physical Surface("' + constants.PHYSICAL_NAME_AUXILIARY + str(pi) + '") = {' + surf_stem + str(pi) + "};" + ls) else: # Normal fracture s += ('Physical Surface("' + constants.PHYSICAL_NAME_FRACTURES + str(pi) + '") = {' + surf_stem + str(pi) + "};" + ls) if not bound_tags[pi] and self.domain is not None: s += "Surface{" + surf_stem + str( pi) + "} In Volume{1};" + ls + ls for li in self.e2f[pi]: s += "Line{frac_line_" + str(li) + "} In Surface{" + surf_stem s += str(pi) + "};" + ls s += ls if not boundary: s += "// End of fracture specification" + ls + ls return s
def __write_polygons(self, boundary=False): """ Writes either all fractures or all boundary planes. """ constants = gridding_constants.GmshConstants() bound_tags = self.polygon_tags.get('boundary', [False] * len(self.polygons[0])) subd_tags = self.polygon_tags.get('subdomain', [False] * len(self.polygons[0])) ls = '\n' # Name boundary or fracture f_or_b = 'auxiliary' if boundary else 'fracture' if not boundary: s = '// Start fracture specification' + ls else: s = '' for pi in range(len(self.polygons[0])): if bound_tags[pi] != boundary: continue # Check if the polygon is a subdomain boundary, i.e., auxiliary # polygon. auxiliary = subd_tags[pi] if auxiliary: # Keep track of "fake fractures", i.e., subdomain # boundaries. f_or_b = 'auxiliary' p = self.polygons[0][pi].astype('int') reverse = self.polygons[1][pi] # First define line loop s += 'frac_loop_' + str(pi) + ' = newll; ' + ls 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 n = f_or_b + '_' # Then the surface s += n + str(pi) + ' = news; ' s += 'Plane Surface(' + n + str(pi) + ') = {frac_loop_' \ + str(pi) + '};' + ls if bound_tags[pi] or auxiliary: # Domain boundary or "fake fracture" = subdomain boundary s += 'Physical Surface(\"' + constants.PHYSICAL_NAME_AUXILIARY \ + str(pi) + '\") = {auxiliary_' + str(pi) + '};' + ls else: # Normal fracture s += 'Physical Surface(\"' + constants.PHYSICAL_NAME_FRACTURES \ + str(pi) + '\") = {fracture_' + str(pi) + '};' + ls if self.domain is not None: s += 'Surface{' + n + str(pi) + '} In Volume{1};' + ls + ls for li in self.e2f[pi]: s += 'Line{frac_line_' + str(li) + '} In Surface{' + n s += str(pi) + '};' + ls s += ls if not boundary: s += '// End of fracture specification' + ls + ls return s
def create_1d_grids( pts: np.ndarray, cells: Dict[str, np.ndarray], phys_names: Dict, cell_info: Dict, line_tag: str = None, tol: float = 1e-4, constraints: np.ndarray = None, return_fracture_tips: bool = True, ) -> Tuple[List[pp.Grid], np.ndarray]: """ Create 1d grids for lines of a specified type from a gmsh tessalation. Only lines that were defined as 'physical' in the gmsh sense may have a grid created, but then only if the physical name matches specified line_tag. It is assumed that the mesh is read by meshio. See porepy.fracs.simplex for how to do this. Parameters: pts (np.ndarray, npt x 3): Global point set from gmsh cells (dict): Should have a key 'line', which maps to a np.ndarray with indices of the lines that form 1d grids. phys_names (dict): mapping from the gmsh tags assigned to physical entities to the physical name of that tag. cell_info (dictionary): Should have a key 'line', that contains the physical names (in the gmsh sense) of the points. line_tag (str, optional): The target physical name, all lines that have this tag will be assigned a grid. The string is assumed to be on the from BASE_NAME_OF_TAG_{INDEX}, where _INDEX is a number. The comparison is made between the physical names and the line, up to the last underscore. If not provided, the physical names of fracture lines will be used as target. tol (double, optional): Tolerance used when comparing points in the creation of line grids. Defaults to 1e-4. constraints (np.array, optional): Array with lists of lines that should not become grids. The array items should match the INDEX in line_tag, see above. return_fracture_tips (boolean, optional): If True (default), fracture tips will be found and returned. Returns: list of grids: List of 1d grids for all physical lines that matched with the specified target tag. np.array, each item is an array of indices of points on a fracture tip. Only returned in return_fracture_tips is True. """ gmsh_constants = constants.GmshConstants() if line_tag is None: line_tag = gmsh_constants.PHYSICAL_NAME_FRACTURE_LINE if constraints is None: constraints = np.empty(0, dtype=np.int) # Recover lines # There will be up to three types of physical lines: intersections (between # fractures), fracture tips, and auxiliary lines (to be disregarded) # Data structure for the point grids g_1d = [] # If there are no fracture intersections, we return empty lists if "line" not in cells: return g_1d, np.empty(0) line_tags = cell_info["line"] line_cells = cells["line"] gmsh_tip_num = [] tip_pts = np.empty(0) for i, pn_ind in enumerate(np.unique(line_tags)): # Index of the final underscore in the physical name. Chars before this # will identify the line type, the one after will give index pn = phys_names[pn_ind] offset_index = pn.rfind("_") loc_line_cell_num = np.where(line_tags == pn_ind)[0] loc_line_pts = line_cells[loc_line_cell_num, :] assert loc_line_pts.size > 1 line_type = pn[:offset_index] # Try to get the fracture number from the physical name of the object # This will only work if everything after the '_' can be interpreted # as a number. Specifically, it should work for all meshes generated by # the standard PorePy procedure, but it may fail for externally generated # geo-files. If it fails, we simply set the frac_num to None in this case. try: frac_num = int(pn[offset_index + 1:]) except ValueError: frac_num = None # If this is a meshing constraint, but not a fracture, we don't need to do anything if frac_num in constraints: continue if line_type == gmsh_constants.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, tol=tol) g.frac_num = int(frac_num) g_1d.append(g) else: # Auxiliary line pass if return_fracture_tips: return g_1d, tip_pts else: return g_1d
def _find_and_split_intersections(self, constraints): # Unified description of points and lines for domain, and fractures points = self.pts edges = self.edges if not np.all(np.diff(edges[:2], axis=0) != 0): raise ValueError("Found a point edge in splitting of edges") const = constants.GmshConstants() tags = np.zeros((2, edges.shape[1]), dtype=np.int) tags[0][np.logical_not(self.tags["boundary"])] = const.FRACTURE_TAG tags[0][self.tags["boundary"]] = const.DOMAIN_BOUNDARY_TAG tags[0][constraints] = const.AUXILIARY_TAG tags[1] = np.arange(edges.shape[1]) edges = np.vstack((edges, tags)) # Ensure unique description of points pts_all, _, old_2_new = unique_columns_tol(points, tol=self.tol) edges[:2] = old_2_new[edges[:2]] to_remove = np.where(edges[0, :] == edges[1, :])[0] lines = np.delete(edges, to_remove, axis=1) self.decomposition["domain_boundary_points"] = old_2_new[ self.decomposition["domain_boundary_points"]] # In some cases the fractures and boundaries impose the same constraint # twice, although it is not clear why. Avoid this by uniquifying the lines. # This may disturb the line tags in lines[2], but we should not be # dependent on those. li = np.sort(lines[:2], axis=0) _, new_2_old, old_2_new = unique_columns_tol(li, tol=self.tol) lines = lines[:, new_2_old] if not np.all(np.diff(lines[:2], axis=0) != 0): raise ValueError( "Found a point edge in splitting of edges after merging points" ) # We split all fracture intersections so that the new lines do not # intersect, except possible at the end points logger.info("Remove edge crossings") tm = time.time() pts_split, lines_split = pp.intersections.split_intersecting_segments_2d( pts_all, lines, tol=self.tol) logger.info("Done. Elapsed time " + str(time.time() - tm)) # Ensure unique description of points pts_split, _, old_2_new = unique_columns_tol(pts_split, tol=self.tol) lines_split[:2] = old_2_new[lines_split[:2]] to_remove = np.where(lines[0, :] == lines[1, :])[0] lines = np.delete(lines, to_remove, axis=1) self.decomposition["domain_boundary_points"] = old_2_new[ self.decomposition["domain_boundary_points"]] # Remove lines with the same start and end-point. # This can be caused by L-intersections, or possibly also if the two # endpoints are considered equal under tolerance tol. remove_line_ind = np.where(np.diff(lines_split[:2], axis=0)[0] == 0)[0] lines_split = np.delete(lines_split, remove_line_ind, axis=1) # TODO: This operation may leave points that are not referenced by any # lines. We should probably delete these. # We find the end points that are shared by more than one intersection intersections = self._find_intersection_points(lines_split) self.decomposition.update({ "points": pts_split, "edges": lines_split, "intersections": intersections, "domain": self.domain, })
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 = 0.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 * pp.geometry_property_checks.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 = pp.FractureNetwork3d(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(pp.distances.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
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_from_gmsh(file_name, constraints=None, **kwargs): """Generate a list of grids dimensions {2, 1, 0}, starting from a gmsh mesh. Parameters: file_name (str): Path to file of gmsh.msh specification. constraints (np.array, optional): Index of fracture lines that are constraints in the meshing, but should not have a lower-dimensional mesh. Defaults to empty. Returns: list of list of grids: grids in 2d, 1d and 0d. If no grids exist in a specified dimension, the inner list will be empty. """ if constraints is None: constraints = np.empty(0, dtype=np.int) start_time = time.time() if file_name.endswith(".msh"): file_name = file_name[:-4] out_file = file_name + ".msh" pts, cells, cell_info, phys_names = _read_gmsh_file(out_file) # Constants used in the gmsh.geo-file const = constants.GmshConstants() # Create grids from gmsh mesh. logger.info("Create grids of various dimensions") g_2d = mesh_2_grid.create_2d_grids(pts, cells, is_embedded=False, phys_names=phys_names, cell_info=cell_info) g_1d, _ = mesh_2_grid.create_1d_grids( pts, cells, phys_names, cell_info, line_tag=const.PHYSICAL_NAME_FRACTURES, constraints=constraints, **kwargs, ) g_0d = mesh_2_grid.create_0d_grids(pts, cells, phys_names, cell_info) grids = [g_2d, g_1d, g_0d] logger.info("Grid creation completed. Elapsed time " + str(time.time() - start_time)) 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" logger.info(s) return grids
def _merge_domain_fracs_2d(dom, frac_p, frac_l, constraints): """ 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) """ if frac_p is None: frac_p = np.zeros((2, 0)) frac_l = np.zeros((2, 0)) # Use constants set outside. If we ever const = constants.GmshConstants() if dom is None: dom_lines = np.empty((2, 0)) elif isinstance(dom, dict): # 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 else: dom_p = dom tmp = np.arange(dom_p.shape[1]) dom_lines = np.vstack((tmp, (tmp + 1) % dom_p.shape[1])) num_dom_lines = dom_lines.shape[1] # Should be 4 for the dictionary case # 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]))) is_constraint = np.in1d(np.arange(frac_l.shape[1]), constraints) frac_l[-1][is_constraint] = const.AUXILIARY_TAG # Merge the point arrays, compartment points first p = np.hstack((frac_p, dom_p)) # Adjust index of fracture points to account for the compartment points dom_l[:2] += frac_p.shape[1] l = np.hstack((frac_l, dom_l)).astype(np.int) # Add a second tag as an identifier of each line. l = np.vstack((l, np.arange(l.shape[1]))) return p, l, dom_p
def create_2d_grids( pts: np.ndarray, cells: Dict[str, np.ndarray], phys_names: Dict[str, str], cell_info: Dict, is_embedded: bool = False, surface_tag: str = None, constraints: np.ndarray = None, ) -> List[pp.Grid]: """ Create 2d grids for lines of a specified type from a gmsh tessalation. Only surfaces that were defined as 'physical' in the gmsh sense may have a grid created, but then only if the physical name matches specified line_tag. It is assumed that the mesh is read by meshio. See porepy.fracs.simplex for how to do this. Parameters: pts (np.ndarray, npt x 3): Global point set from gmsh cells (dict): Should have a key 'triangle' which maps to a np.ndarray with indices of the points that form 2d grids. phys_names (dict): mapping from the gmsh tags assigned to physical entities to the physical name of that tag. cell_info (dictionary): Should have a key 'triangle' that contains the physical names (in the gmsh sense) of the points. is_embedded (boolean, optional): If True, the triangle grids are embedded in 3d space. If False (default), the grids are truly 2d. surface_tag (str, optional): The target physical name, all surfaces that have this tag will be assigned a grid. The string is assumed to be on the from BASE_NAME_OF_TAG_{INDEX}, where _INDEX is a number. The comparison is made between the physical names and the line, up to the last underscore. If not provided, the physical names of fracture surfaces will be used as target. constraints (np.array, optional): Array with lists of lines that should not become grids. The array items should match the INDEX in line_tag, see above. Returns: list of grids: List of 2d grids for all physical surfaces that matched with the specified target tag. """ # List of 2D grids, one for each surface g_2d = [] gmsh_constants = constants.GmshConstants() if surface_tag is None: surface_tag = gmsh_constants.PHYSICAL_NAME_FRACTURES if constraints is None: constraints = np.array([], dtype=np.int) if is_embedded: # Special treatment of the case with no fractures if "triangle" not in cells: return g_2d # Recover cells on fracture surfaces, and create grids tri_cells = cells["triangle"] # Tags of all triangle grids tri_tags = cell_info["triangle"] # Loop over all gmsh tags associated with triangle grids for pn_ind in np.unique(tri_tags): # Split the physical name into a category and a number - which will become # the fracture number pn = phys_names[pn_ind] offset = pn.rfind("_") frac_num = int(pn[offset + 1:]) plane_type = pn[:offset] # Check if the surface is of the target type, or if the surface is tagged # as a constraint if plane_type != surface_tag[:-1] or int( pn[offset + 1:]) in constraints: continue # Cells of this surface loc_cells = np.where(tri_tags == pn_ind)[0] loc_tri_cells = tri_cells[loc_cells, :].astype(np.int) # Find unique points, and a mapping from local to global points pind_loc, p_map = np.unique(loc_tri_cells, return_inverse=True) loc_tri_ind = p_map.reshape((-1, 3)) g = pp.TriangleGrid(pts[pind_loc, :].transpose(), loc_tri_ind.transpose()) # Add mapping to global point numbers g.global_point_ind = pind_loc # Associate a fracture id (corresponding to the ordering of the # frature planes in the original fracture list provided by the # user) g.frac_num = frac_num # Append to list of 2d grids g_2d.append(g) else: triangles = cells["triangle"].transpose() # Construct grid g_2d = pp.TriangleGrid(pts.transpose(), triangles) # we need to add the face tags from gmsh to the current mesh, # first we add them as False and after we change for the correct # faces. The new tag name become the lower version of what gmsh gives # in the cell_info["line"]. The map phys_names recover the literal name. # create all the extra tags for the grids, by default they're false for tag in np.unique(cell_info["line"]): tag_name = phys_names[tag].lower() + "_faces" g_2d.tags[tag_name] = np.zeros(g_2d.num_faces, dtype=np.bool) # since there is not a cell-face relation from gmsh but only a cell-node # relation we need to recover the corresponding face. for tag_id, tag in enumerate(cell_info["line"]): tag_name = phys_names[tag].lower() + "_faces" # check where is the first node indipendent on the position # in the triangle, being first, second or third node first = (triangles == cells["line"][tag_id, 0]).any(axis=0) # check where is the second node, same approach as before second = (triangles == cells["line"][tag_id, 1]).any(axis=0) # select which are the cells that have this edge tria = np.logical_and(first, second).astype(np.int) # with the cell_faces map we get the faces associated to # the selected triangles face = np.abs(g_2d.cell_faces).dot(tria) # we have two case, the face is internal or is at the boundary # we consider them separately if np.any(face > 1): # select the face if it is internal g_2d.tags[tag_name][face > 1] = True else: # the face is on a boundary face = np.logical_and(face, g_2d.tags["domain_boundary_faces"]) if np.sum(face) == 2: # the triangle has two faces at the boundary, check if it # is the first otherwise it is the other. face_id = np.where(face)[0] first_face = np.zeros(face.size, dtype=np.bool) first_face[face_id[0]] = True nodes = g_2d.face_nodes.dot(first_face) # check if the nodes of the first face are the same if np.all(nodes[cells["line"][tag_id, :]]): face[face_id[1]] = False else: face[face_id[0]] = False g_2d.tags[tag_name][face] = True # Create mapping to global numbering (will be a unit mapping, but is # crucial for consistency with lower dimensions) g_2d.global_point_ind = np.arange(pts.shape[0]) # Convert to list to be consistent with lower dimensions # This may also become useful in the future if we ever implement domain # decomposition approaches based on gmsh. g_2d = [g_2d] return g_2d
def create_0d_grids( pts: np.ndarray, cells: Dict[str, np.ndarray], phys_names: Dict[int, str], cell_info: Dict[str, np.ndarray], target_tag_stem: str = None, ) -> List[pp.Grid]: """ Create 0d grids for points of a specified type from a gmsh tessalation. Only points that were defined as 'physical' in the gmsh sense may have a grid created, but then only if the physical name matches specified target_tag_stem. It is assumed that the mesh is read by meshio. See porepy.fracs.simplex for how to do this. Parameters: pts (np.ndarray, npt x 3): Global point set from gmsh cells (dict): Should have a key vertex, which maps to a np.ndarray if indices of the points that form point grids. phys_names (dict): mapping from the gmsh tags assigned to physical entities to the physical name of that tag. cell_info (dictionary): Should have a key 'vertex', that contains the physical names (in the gmsh sense) of the points. target_tag_stem (str, optional): The target physical name, all points that have this tag will be assigned a grid. The string is assumed to be on the from BASE_NAME_OF_TAG_{INDEX}, where _INDEX is a number. The comparison is made between the physical names and the target_tag_stem, up to the last underscore. If not provided, the physical names of fracture points will be used as target. Returns: list of grids: List of 0d grids for all physical points that matched with the specified target tag. """ if target_tag_stem is None: target_tag_stem = constants.GmshConstants( ).PHYSICAL_NAME_FRACTURE_POINT g_0d = [] if "vertex" in cells: # Index (in the array pts) of the points that are specified as physical in the # .geo-file point_cells = cells["vertex"].ravel() # Keys to the physical names table of the points that have been decleared as # physical physical_name_indices = cell_info["vertex"] # Loop over all physical points for pi, phys_names_ind in enumerate(physical_name_indices): pn = phys_names[phys_names_ind] offset_index = pn.rfind("_") phys_name_vertex = pn[:offset_index] # Check if this is the target. The -1 is needed to avoid the extra _ in # the defined constantnt if phys_name_vertex == target_tag_stem[:-1]: # This should be a new grid g = pp.PointGrid(pts[point_cells[pi]]) g.global_point_ind = np.atleast_1d(np.asarray(point_cells[pi])) # Store the index of this physical name tag. g.physical_name_index = int(pn[offset_index + 1:]) g_0d.append(g) else: continue return g_0d