def parse_elements(elements): try: num_elements = int(elements[0]) except ValueError: raise GmshError(f'first line of elements sections is not a number: {elements[0]}') if len(elements) != num_elements + 1: raise GmshError('number-of-elements field does not match number of lines in elements section') elements = [e.split(' ') for e in elements[1:]] try: elements = [tuple(int(f) for f in e) for e in elements] except ValueError: raise GmshError('malformed elements section') element_types = {1: 'line', 2: 'triangle'} element_nodes = {'line': 2, 'triangle': 3} def parse_line(fields): if fields[1] not in element_types: raise GmshError(f'element type {fields[0]} not supported') element_type = element_types[fields[1]] num_nodes = element_nodes[element_type] num_tags = fields[2] if len(fields) != num_nodes + num_tags + 3: raise GmshError('malformed elements section') return element_type, (fields[0], tuple(fields[3:3 + num_tags]), fields[3 + num_tags:]) elements_by_type = defaultdict(list) for e in elements: t, l = parse_line(e) elements_by_type[t].append(l) return elements_by_type
def parse_line(fields): if fields[1] not in element_types: raise GmshError(f'element type {fields[0]} not supported') element_type = element_types[fields[1]] num_nodes = element_nodes[element_type] num_tags = fields[2] if len(fields) != num_nodes + num_tags + 3: raise GmshError('malformed elements section') return element_type, (fields[0], tuple(fields[3:3 + num_tags]), fields[3 + num_tags:])
def parse_names(physical_names): try: num_elements = int(physical_names[0]) except ValueError: raise GmshError(f'first line of physical names sections is not a number: {physical_names[0]}') if len(physical_names) != num_elements + 1: raise GmshError('number-of-names field does not match number of lines in physical names section') physical_names = [pn.split(' ') for pn in physical_names[1:]] if not all(len(pn) == 3 for pn in physical_names): raise GmshError('malformed physical names section') try: physical_names = [(int(b), int(a), str(c).replace('"', '')) for a, b, c in physical_names] except ValueError: raise GmshError('malformed physical names section') return physical_names
def parse_nodes(nodes): try: num_nodes = int(nodes[0]) except ValueError: raise GmshError(f'first line of nodes sections is not a number: {nodes[0]}') if len(nodes) != num_nodes + 1: raise GmshError('number-of-nodes field does not match number of lines in nodes section') nodes = [n.split(' ') for n in nodes[1:]] if not all(len(n) == 4 for n in nodes): raise GmshError('malformed nodes section') try: nodes = [(int(a), (float(b), float(c), float(d))) for a, b, c, d in nodes] except ValueError: raise GmshError('malformed nodes section') return nodes
def _parse_gmsh_file(f): allowed_sections = ['Nodes', 'Elements', 'PhysicalNames', 'Periodic', 'NodeData', 'ElementData', 'ElementNodeData'] supported_sections = ['Nodes', 'Elements', 'PhysicalNames'] try: l = next(f).strip() if l != '$MeshFormat': raise GmshError(f'expected $MeshFormat, got {l}') l = next(f).strip() header = l.split(' ') if len(header) != 3: raise GmshError(f'header {l} has {len(header)} fields, expected 3') if header[0] != '2.2': raise GmshError(f'wrong file format version: got {header[0]}, expected 2.2') try: file_type = int(header[1]) except ValueError: raise GmshError(f'malformed header: expected integer, got {header[1]}') if file_type != 0: raise GmshError('wrong file type: only ASCII gmsh files are supported') try: data_size = int(header[2]) # NOQA except ValueError: raise GmshError(f'malformed header: expected integer, got {header[2]}') l = next(f).strip() if l != '$EndMeshFormat': raise GmshError(f'expected $EndMeshFormat, got {l}') except StopIteration: raise GmshError('unexcpected end of file') in_section = False sections = defaultdict(list) for l in f: l = l.strip() if l == '': continue if not in_section: if not l.startswith('$'): raise GmshError(f'expected section name, got {l}') section = l[1:] if section not in allowed_sections: raise GmshError(f'unknown section type: {section}') if section not in supported_sections: raise GmshError(f'unsupported section type: {section}') if section in sections: raise GmshError(f'only one {section} section allowed') in_section = True elif l.startswith('$'): if l != '$End' + section: raise GmshError(f'expected $End{section}, got {l}') in_section = False else: sections[section].append(l) if in_section: raise GmshError(f'file ended while in section {section}') # now we parse each section ... def parse_nodes(nodes): try: num_nodes = int(nodes[0]) except ValueError: raise GmshError(f'first line of nodes sections is not a number: {nodes[0]}') if len(nodes) != num_nodes + 1: raise GmshError('number-of-nodes field does not match number of lines in nodes section') nodes = [n.split(' ') for n in nodes[1:]] if not all(len(n) == 4 for n in nodes): raise GmshError('malformed nodes section') try: nodes = [(int(a), (float(b), float(c), float(d))) for a, b, c, d in nodes] except ValueError: raise GmshError('malformed nodes section') return nodes def parse_elements(elements): try: num_elements = int(elements[0]) except ValueError: raise GmshError(f'first line of elements sections is not a number: {elements[0]}') if len(elements) != num_elements + 1: raise GmshError('number-of-elements field does not match number of lines in elements section') elements = [e.split(' ') for e in elements[1:]] try: elements = [tuple(int(f) for f in e) for e in elements] except ValueError: raise GmshError('malformed elements section') element_types = {1: 'line', 2: 'triangle'} element_nodes = {'line': 2, 'triangle': 3} def parse_line(fields): if fields[1] not in element_types: raise GmshError(f'element type {fields[0]} not supported') element_type = element_types[fields[1]] num_nodes = element_nodes[element_type] num_tags = fields[2] if len(fields) != num_nodes + num_tags + 3: raise GmshError('malformed elements section') return element_type, (fields[0], tuple(fields[3:3 + num_tags]), fields[3 + num_tags:]) elements_by_type = defaultdict(list) for e in elements: t, l = parse_line(e) elements_by_type[t].append(l) return elements_by_type def parse_names(physical_names): try: num_elements = int(physical_names[0]) except ValueError: raise GmshError(f'first line of physical names sections is not a number: {physical_names[0]}') if len(physical_names) != num_elements + 1: raise GmshError('number-of-names field does not match number of lines in physical names section') physical_names = [pn.split(' ') for pn in physical_names[1:]] if not all(len(pn) == 3 for pn in physical_names): raise GmshError('malformed physical names section') try: physical_names = [(int(b), int(a), str(c).replace('"', '')) for a, b, c in physical_names] except ValueError: raise GmshError('malformed physical names section') return physical_names parser_map = {'Nodes': parse_nodes, 'Elements': parse_elements, 'PhysicalNames': parse_names} for k, v in sections.items(): sections[k] = parser_map[k](v) return sections
def discretize_gmsh(domain_description=None, geo_file=None, geo_file_path=None, msh_file_path=None, mesh_algorithm='del2d', clscale=1., options='', refinement_steps=0): """Mesh a |DomainDescription| or an already existing Gmsh GEO-file using the Gmsh mesher. Parameters ---------- domain_description A |DomainDescription| of the |PolygonalDomain| or |RectDomain| to discretize. Has to be `None` when `geo_file` is given. geo_file File handle of the Gmsh Geo-file to discretize. Has to be `None` when `domain_description` is given. geo_file_path Path of the created Gmsh GEO-file. When meshing a |PolygonalDomain| or |RectDomain| and `geo_file_path` is `None`, a temporary file will be created. If `geo_file` is specified, this is ignored and the path to `geo_file` will be used. msh_file_path Path of the created Gmsh MSH-file. If `None`, a temporary file will be created. mesh_algorithm The mesh generation algorithm to use (meshadapt, del2d, front2d). clscale Mesh element size scaling factor. options Other options to control the meshing procedure of Gmsh. See http://geuz.org/gmsh/doc/texinfo/gmsh.html#Command_002dline-options for all available options. refinement_steps Number of refinement steps to do after the initial meshing. Returns ------- grid The generated :class:`~pymor.grids.gmsh.GmshGrid`. boundary_info The generated :class:`~pymor.grids.gmsh.GmshBoundaryInfo`. """ assert domain_description is None or geo_file is None logger = getLogger('pymor.domaindiscretizers.gmsh.discretize_gmsh') # run Gmsh; initial meshing logger.info('Checking for Gmsh ...') # when we are running MPI parallel and Gmsh is compiled with MPI support, # we have to make sure Gmsh does not notice the MPI environment or it will fail. env = {k: v for k, v in os.environ.items() if 'MPI' not in k.upper()} try: version = subprocess.check_output(['gmsh', '--version'], stderr=subprocess.STDOUT, env=env).decode() except (subprocess.CalledProcessError, OSError): raise GmshError( 'Could not find Gmsh.' ' Please ensure that the gmsh binary (http://geuz.org/gmsh/) is in your PATH.' ) logger.info('Found version ' + version.strip()) def discretize_PolygonalDomain(): # combine points and holes, since holes are points, too, and have to be stored as such. points = [domain_description.points] points.extend(domain_description.holes) return points, domain_description.boundary_types def discretize_RectDomain(): points = [[ domain_description.domain[0].tolist(), [domain_description.domain[1][0], domain_description.domain[0][1]], domain_description.domain[1].tolist(), [domain_description.domain[0][0], domain_description.domain[1][1]] ]] boundary_types = {domain_description.bottom: [1]} if domain_description.right not in boundary_types: boundary_types[domain_description.right] = [2] else: boundary_types[domain_description.right].append(2) if domain_description.top not in boundary_types: boundary_types[domain_description.top] = [3] else: boundary_types[domain_description.top].append(3) if domain_description.left not in boundary_types: boundary_types[domain_description.left] = [4] else: boundary_types[domain_description.left].append(4) if None in boundary_types: del boundary_types[None] return points, boundary_types # these two are referenced in a finally block, but were left undefined in some paths geo_file, msh_file = None, None try: # When a |PolygonalDomain| or |RectDomain| has to be discretized create a Gmsh GE0-file and write all data. if domain_description is not None: logger.info('Writing Gmsh geometry file ...') # Create a temporary GEO-file if None is specified if geo_file_path is None: geo_file = tempfile.NamedTemporaryFile(mode='wt', delete=False, suffix='.geo') geo_file_path = geo_file.name else: geo_file = open(geo_file_path, 'w') if isinstance(domain_description, PolygonalDomain): points, boundary_types = discretize_PolygonalDomain() elif isinstance(domain_description, RectDomain): points, boundary_types = discretize_RectDomain() else: raise NotImplementedError( f'I do not know how to discretize {domain_description}') # assign ids to all points and write them to the GEO-file. for id, p in enumerate([p for ps in points for p in ps]): assert len(p) == 2 geo_file.write( 'Point(' + str(id + 1) + ') = ' + str(p + [0, 0]).replace('[', '{').replace(']', '}') + ';\n') # store points and their ids point_ids = dict( zip([str(p) for ps in points for p in ps], range(1, len([p for ps in points for p in ps]) + 1))) # shift points 1 entry to the left. points_deque = [collections.deque(ps) for ps in points] for ps_d in points_deque: ps_d.rotate(-1) # create lines by connecting the points with shifted points, such that they form a polygonal chains. lines = [[point_ids[str(p0)], point_ids[str(p1)]] for ps, ps_d in zip(points, points_deque) for p0, p1 in zip(ps, ps_d)] # assign ids to all lines and write them to the GEO-file. for l_id, l in enumerate(lines): geo_file.write('Line(' + str(l_id + 1) + ')' + ' = ' + str(l).replace('[', '{').replace(']', '}') + ';\n') # form line_loops (polygonal chains), create ids and write them to file. line_loops = [[point_ids[str(p)] for p in ps] for ps in points] line_loop_ids = range( len(lines) + 1, len(lines) + len(line_loops) + 1) for ll_id, ll in zip(line_loop_ids, line_loops): geo_file.write('Line Loop(' + str(ll_id) + ')' + ' = ' + str(ll).replace('[', '{').replace(']', '}') + ';\n') # set this here explicitly for string conversion to make sense line_loop_ids = list(line_loop_ids) # create the surface defined by line loops, starting with the exterior and then the holes. geo_file.write( 'Plane Surface(' + str(line_loop_ids[0] + 1) + ')' + ' = ' + str(line_loop_ids).replace('[', '{').replace(']', '}') + ';\n') geo_file.write('Physical Surface("boundary") = {' + str(line_loop_ids[0] + 1) + '};\n') # write boundaries. for boundary_type, bs in boundary_types.items(): geo_file.write( 'Physical Line' + '("' + str(boundary_type) + '")' + ' = ' + str([l_id for l_id in bs]).replace('[', '{').replace(']', '}') + ';\n') geo_file.close() # When a GEO-File is provided just get the corresponding file path. else: geo_file_path = geo_file.name # Create a temporary MSH-file if no path is specified. if msh_file_path is None: msh_file = tempfile.NamedTemporaryFile(mode='wt', delete=False, suffix='.msh') msh_file_path = msh_file.name msh_file.close() tic = time.time() # run Gmsh; initial meshing logger.info('Calling Gmsh ...') cmd = [ 'gmsh', geo_file_path, '-2', '-algo', mesh_algorithm, '-clscale', str(clscale), options, '-o', msh_file_path ] subprocess.check_call(cmd, env=env) # run gmsh; perform mesh refinement cmd = ['gmsh', msh_file_path, '-refine', '-o', msh_file_path] for i in range(refinement_steps): logger.info(f'Performing Gmsh refinement step {i+1}') subprocess.check_call(cmd, env=env) toc = time.time() t_gmsh = toc - tic logger.info(f'Gmsh took {t_gmsh} s') # Create |GmshGrid| and |GmshBoundaryInfo| form the just created MSH-file. grid, bi = load_gmsh(open(msh_file_path)) finally: # delete tempfiles if they were created beforehand. if isinstance(geo_file, tempfile._TemporaryFileWrapper): os.remove(geo_file_path) if isinstance(msh_file, tempfile._TemporaryFileWrapper): os.remove(msh_file_path) return grid, bi