Source code for calfem.mesh

import os
import sys
import tempfile
import shutil
import subprocess
import numpy as np
from calfem.core import createdofs
from calfem.utils import which
import calfem.core as cfc

import logging as cflog

import gmsh

[docs]def error(msg): """Log error message""" cflog.error(msg)
[docs]def info(msg): """Log information message""" cflog.info(msg)
def cmp(a, b): return (a > b) ^ (a < b) # def dofsFromNodes(listOfNodes, dofs): # D = [] # for node in listOfNodes: # D.extend(dofs[node]) # return D def _offsetIndices(lst, offset=0): '''Shifts the indices by offset. Positive offsets move the indices away from 0. Negative offsets move the indices towards 0. If an index is 0 the offset is added to it.''' return [x + cmp(x, -0.5)*offset for x in lst] def _formatList(lst, offset=0): """ Turns a list of numbers into a corresponding string of comma-separated numbers. The parameter offset is a number that is added to the numbers. Can be used for turning a list of 0-based indices into a corresponding string of comma-separated offset indices. Offsets depend on the sign, i.e. negative numbers get the offset subtracted. Do not use offsets on lists with negative float values. """ # Increment the indices by 1. Join list-elements as strings separated by ', '. try: return ', '.join(map(str, _offsetIndices(lst, offset))) except TypeError: # If lst is not iterable (causes TypeError), then it is probably an integer. return lst+offset def _insertInSetDict(dictionary, key, values): '''inserts values at key in dictionary containing sets. Values may be a single value or iterable, in which case each value is inserted''' if not key in dictionary: dictionary[key] = set() try: for v in values: dictionary[key].add(v) # Exception if values is not an iterable - insert values itself instead. except TypeError: dictionary[key].add(values) def _insertBoundaryElement(boundaryElements, elementType, marker, nodes): """ Insert an element to the boundaryElements dict. Parameters: boundaryElements Dictionary of boundary elements elementType 'elm-type' according to GMSH marker Boundary marker nodes List of element nodes, order according to GMSH """ if not marker in boundaryElements: boundaryElements[marker] = [] boundaryElements[marker].append( {'elm-type': elementType, 'node-number-list': nodes}) def createGmshMesh(geometry, el_type=2, el_size_factor=1, dofs_per_node=1, gmsh_exec_path=None, clcurv=False, min_size=None, max_size=None, meshing_algorithm=None, additional_options=''): meshGen = GmshMeshGenerator(geometry, el_type, el_size_factor, dofs_per_node, gmsh_exec_path, clcurv, min_size, max_size, meshing_algorithm, additional_options) return meshGen.create() createMesh = createGmshMesh create_mesh = createGmshMesh mesh = createGmshMesh
[docs]class GmshMeshGenerator: ''' Meshes geometry in GeoData objects or geo-files by calling the Gmsh executable. This is done when the function create() is called. ''' def __init__(self, geometry, el_type=2, el_size_factor=1, dofs_per_node=1, gmsh_exec_path=None, clcurv=False, min_size=None, max_size=None, meshing_algorithm=None, additional_options='', mesh_dir='', return_boundary_elements=False): ''' Parameters: geometry GeoData instance or string containing path to .geo-file el_type Integer. Element type and order. See gmsh manual for details. el_size_factor Float. Factor by which the element sizes are multiplied. dofs_per_node Number of degrees of freedom per node. gmsh_exec_path File path to where the gmsh executable is located. clcurv Set to true to make elements smaller at high curvatures. (Experimental option according to the gmsh manual) min_size Minimum element size max_size Maximum element size meshing_algorithm String. Select mesh algorithm ('meshadapt', 'del2d', 'front2d', 'del3d', 'front3d', ...). See the gmsh manual for more info. return_boundary_elements Flag for returning dictionary with boundary element information. Useful for applying loads on boundary. additional_options String containing additional command line args for gmsh. Use this if a gmsh option is not covered by the above parameters (See section 3.3 in the gmsh manual for a list of options)): ''' self.geometry = geometry self.el_type = el_type self.el_size_factor = el_size_factor self.dofs_per_node = dofs_per_node self.gmsh_exec_path = gmsh_exec_path self.clcurv = clcurv self.min_size = min_size self.max_size = max_size self.meshing_algorithm = meshing_algorithm self.additional_options = additional_options self.mesh_dir = mesh_dir self.return_boundary_elements = return_boundary_elements # gmsh elements that have rectangle faces self._ElementsWithQuadFaces = [3, 5, 10, 12, 16, 17, 92, 93] self._2ndOrderElms = [8, 9, 10, 11, 12, 13, 14, 16, 17, 18, 19] self._2dOrderIncompleteElms = [9, 11, 13, 14, 16, 17, 18, 19] # Apart from 16 the 2nd orders are totally untested. Only 16 (8-node quad) # is implemented in pycalfem though, so it does not matter. self.use_gmsh_module = True self.remove_gmsh_signal_handler = True self.initialize_gmsh = True
[docs] def create(self, is3D=False): ''' Meshes a surface or volume defined by the geometry in geoData. Parameters: is3D - Optional parameter that only needs to be set if geometry is loaded from a geo-file, i.e. if geoData is a path string. Default False. Returns: coords Node coordinates [[n0_x, n0_y, n0_z], [ ... ], [nn_x, nn_y, nn_z]] edof Element topology [[el0_dof1, ..., el0_dofn], [ ... ], [eln_dof1, ..., eln_dofn]] dofs Node dofs [[n0_dof1, ..., n0_dofn], [ ... ], [nn_dof1, ..., nn_dofn]] bdofs Boundary dofs. Dictionary containing lists of dofs for each boundary marker. Dictionary key = marker id. elementmarkers List of integer markers. Row i contains the marker of element i. Markers are similar to boundary markers and can be used to identify in which region an element lies. boundaryElements (optional) returned if self.return_boundary_elements is true. Contains dictionary with boundary elements. The keys are markers and the values are lists of elements for that marker. Running this function also creates object variables: nodesOnCurve Dictionary containing lists of node-indices. Key is a curve-ID and the value is a list of indices of all nodes on that curve, including its end points. nodesOnSurface Dictionary containing lists of node-indices. Key is a surface-ID and the value is a list of indices of the nodes on that surface, including its boundary. nodesOnVolume Dictionary containing lists of node-indices. Key is a volume-ID and the value is a list of indices of the nodes in that volume, including its surface. ''' # Nodes per element for different element types: # (taken from Chapter 9, page 89 of the gmsh manual) nodesPerElmDict = {1: 2, 2: 3, 3: 4, 4: 4, 5: 8, 6: 6, 7: 5, 8: 3, 9: 6, 10: 9, 11: 10, 12: 27, 13: 18, 14: 14, 15: 1, 16: 8, 17: 20, 18: 15, 19: 13, 20: 9, 21: 10, 22: 12, 23: 15, 24: 15, 25: 21, 26: 4, 27: 5, 28: 6, 29: 20, 30: 35, 31: 56, 92: 64, 93: 125} nodesPerElement = nodesPerElmDict[self.el_type] # Check for GMSH executable # # Consider using the gmsh_extension module if not self.use_gmsh_module: gmshExe = self.gmsh_exec_path if gmshExe == None: gmshExe = None if sys.platform == "win32": gmshExe = which("gmsh.bat") if gmshExe == None: gmshExe = which("gmsh.exe") else: gmshExe = which("gmsh") else: if not os.path.exists(gmshExe): gmshExe = os.path.join( os.getcwd(), self.gmsh_exec_path) # Try relative path if not os.path.exists(gmshExe): gmshExe = None # Relative path didnt work either if gmshExe == None: raise IOError( "Error: Could not find GMSH. Please make sure that the \GMSH executable is available on the search path (PATH).") else: print("Info : GMSH -> %s" % gmshExe) else: print("Info : GMSH -> Python-module") # Create a temporary directory for GMSH oldStyleTempDir = False if self.mesh_dir != "": tempMeshDir = self.mesh_dir if not os.path.exists(tempMeshDir): os.mkdir(tempMeshDir) else: tempMeshDir = tempfile.mkdtemp() # If geometry data is given as a .geo file we will just pass it on to gmsh later. if type(self.geometry) is str: geoFilePath = self.geometry # In this case geoData is a path string, so the dimension must be supplied by the user. dim = 3 if is3D else 2 if not os.path.exists(geoFilePath): geoFilePath = os.path.join( os.getcwd(), geoFilePath) # Try relative path if not os.path.exists(geoFilePath): raise IOError( "Error: Could not find geo-file " + geoFilePath) else: # Get the dimension of the model from geoData. dim = 3 if self.geometry.is3D else 2 if oldStyleTempDir: if not os.path.exists("./gmshMeshTemp"): os.mkdir("./gmshMeshTemp") geoFilePath = os.path.normpath(os.path.join( os.getcwd(), "gmshMeshTemp/tempGeometry.geo")) # "gmshMeshTemp/tempGeometry.geo" else: geoFilePath = os.path.normpath( os.path.join(tempMeshDir, 'tempGeometry.geo')) with open(geoFilePath, "w") as self.geofile: self._writeGeoFile() # Write geoData to file if oldStyleTempDir: # Filepath to the msh-file that will be generated. mshFileName = os.path.normpath(os.path.join( os.getcwd(), 'gmshMeshTemp/meshFile.msh')) else: mshFileName = os.path.normpath( os.path.join(tempMeshDir, 'meshFile.msh')) # construct options string: options = "" options += ' -' + str(dim) options += ' -clscale ' + str(self.el_size_factor) # scale factor options += ' -o \"%s\"' % mshFileName options += ' -clcurv' if self.clcurv else '' options += ' -clmin ' + \ str(self.min_size) if self.min_size is not None else '' options += ' -clmax ' + \ str(self.max_size) if self.max_size is not None else '' options += ' -algo ' + self.meshing_algorithm if self.meshing_algorithm is not None else '' options += ' -order 2' if self.el_type in self._2ndOrderElms else '' options += ' -format msh22' options += ' -v 5' options += ' ' + self.additional_options # Execute gmsh if self.use_gmsh_module: # Meshing using gmsh extension module if self.initialize_gmsh: gmsh.initialize(sys.argv) # This is a hack to enable the use of gmsh in # a separate thread. if self.remove_gmsh_signal_handler: gmsh.oldsig = None # Load .geo file gmsh.open(geoFilePath) gmsh.model.geo.synchronize() # Set meshing options if self.el_type in self._2ndOrderElms: gmsh.option.setNumber("Mesh.ElementOrder", 2) if self.meshing_algorithm is not None: gmsh.option.setString(self.meshing_algorithm) gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) gmsh.option.setNumber("Mesh.MeshSizeFactor", self.el_size_factor) if self.clcurv is not None: gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", self.clcurv) if self.min_size is not None: gmsh.option.setNumber('Mesh.MeshSizeMin', self.min_size) if self.max_size is not None: gmsh.option.setNumber('Mesh.MeshSizeMax', self.max_size) # Generate mesh gmsh.model.mesh.generate(2) # Write .msh file gmsh.write(mshFileName) # Close extension module if self.initialize_gmsh: gmsh.finalize() else: gmshExe = os.path.normpath(gmshExe) info("GMSH binary: "+gmshExe) output = subprocess.Popen(r'"%s" "%s" %s' % ( gmshExe, geoFilePath, options), shell=True, stdout=subprocess.PIPE).stdout.read() # Read generated msh file: # print("Opening msh file " + mshFileName)#TEMP with open(mshFileName, 'r') as mshFile: info("Mesh file : "+mshFileName) # print("Reading msh file...") ln = mshFile.readline() while(ln != '$Nodes\n'): # Read until we find the nodes ln = mshFile.readline() nbrNodes = int(mshFile.readline()) allNodes = np.zeros([nbrNodes, dim], 'd') for i in range(nbrNodes): line = list(map(float, mshFile.readline().split())) # Grab the coordinates (1:3 if 2D, 1:4 if 3D) allNodes[i, :] = line[1:dim+1] while(mshFile.readline() != '$Elements\n'): # Read until we find the elements pass # The nbr of elements (including marker elements). nbrElements = int(mshFile.readline()) elements = [] elementmarkers = [] # temp dictionary of sets. Key:MarkerID. Value:Set. The sets will be converted to lists. bdofs = {} boundaryElements = {} # nodeOnPoint = {} #dictionary pointID : nodeNumber self.nodesOnCurve = {} # dictionary lineID : set of [nodeNumber] self.nodesOnSurface = {} # dictionary surfID : set of [nodeNumber] self.nodesOnVolume = {} # dictionary volID : set of [nodeNumber] # Read all elements (points, surfaces, etc): for i in range(nbrElements): line = list(map(int, mshFile.readline().split())) eType = line[1] # second int is the element type. nbrTags = line[2] # Third int is the nbr of tags on this element. marker = line[3] # Fourth int (first tag) is the marker. # Fifth int is the ID of the geometric entity (points, curves, etc) that the element belongs to entityID = line[4] # The rest after tags are node indices. nodes = line[3+nbrTags: len(line)] # If the element type is the kind of element we are looking for: if(eType == self.el_type): # Add the nodes of the elements to the list. elements.append(nodes) # Add element marker. It is used for keeping track of elements (thickness, heat-production and such) elementmarkers.append(marker) else: # If the element is not a "real" element we store its node at marker in bdof instead: _insertInSetDict(bdofs, marker, nodes) # We also store the full information as 'boundary elements' _insertBoundaryElement(boundaryElements, eType, marker, nodes) # if eType == 15: #If point. Commmented away because points only make elements if they have non-zero markers, so nodeOnPoint is not very useful. # nodeOnPoint[entityID-1] = nodes[0] #insert node into nodeOnPoint. (ID-1 because we want 0-based indices) if eType in [1, 8, 26, 27, 28]: # If line # insert nodes into nodesOnCurve _insertInSetDict(self.nodesOnCurve, entityID - 1, _offsetIndices(nodes, -1)) elif eType in [2, 3, 9, 10, 16, 20, 21, 22, 23, 24, 25]: # If surfaceelement # insert nodes into nodesOnSurface _insertInSetDict(self.nodesOnSurface, entityID - 1, _offsetIndices(nodes, -1)) else: # if volume element. _insertInSetDict(self.nodesOnVolume, entityID - 1, _offsetIndices(nodes, -1)) elements = np.array(elements) for key in bdofs.keys(): # Convert the sets of boundary nodes to lists. bdofs[key] = list(bdofs[key]) for key in self.nodesOnCurve.keys(): # Convert set to list self.nodesOnCurve[key] = list(self.nodesOnCurve[key]) for key in self.nodesOnSurface.keys(): # Convert set to list self.nodesOnSurface[key] = list(self.nodesOnSurface[key]) for key in self.nodesOnVolume.keys(): # Convert set to list self.nodesOnVolume[key] = list(self.nodesOnVolume[key]) # Remove temporary mesh directory if not explicetly specified. if self.mesh_dir == "": shutil.rmtree(tempMeshDir) dofs = createdofs(np.size(allNodes, 0), self.dofs_per_node) if self.dofs_per_node > 1: # This if-chunk copied from pycalfem_utils.py self.topo = elements expandedElements = np.zeros( (np.size(elements, 0), nodesPerElement*self.dofs_per_node), 'i') elIdx = 0 for elementTopo in elements: for i in range(nodesPerElement): expandedElements[elIdx, i*self.dofs_per_node:( i*self.dofs_per_node+self.dofs_per_node)] = dofs[elementTopo[i]-1, :] elIdx += 1 for keyID in bdofs.keys(): bVerts = bdofs[keyID] bVertsNew = [] for i in range(len(bVerts)): for j in range(self.dofs_per_node): bVertsNew.append(dofs[bVerts[i]-1][j]) bdofs[keyID] = bVertsNew if self.return_boundary_elements: return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers, boundaryElements return allNodes, np.asarray(expandedElements), dofs, bdofs, elementmarkers if self.return_boundary_elements: return allNodes, elements, dofs, bdofs, elementmarkers, boundaryElements return allNodes, elements, dofs, bdofs, elementmarkers
def _writeGeoFile(self): # key is marker, value is a list of point indices (0-based) with that marker pointMarkers = {} curveMarkers = {} surfaceMarkers = {} volumeMarkers = {} # WRITE POINTS: for ID, [coords, elSize, marker] in self.geometry.points.items(): self.geofile.write("Point(%i) = {%s};\n" % ( ID+1, _formatList(coords + [elSize]))) _insertInSetDict(pointMarkers, marker, ID) # WRITE CURVES: for ID, [curveName, points, marker, elOnCurve, distributionString, distributionVal] in self.geometry.curves.items(): self.geofile.write("%s(%i) = {%s};\n" % ( curveName, ID+1, _formatList(points, 1))) # Transfinite Line{2} = 20 Using Bump 0.05; if elOnCurve != None: distribution = "" if distributionString == None else "Using %s %f" % ( distributionString, distributionVal) self.geofile.write("Transfinite Line{%i} = %i %s;\n" % ( ID+1, elOnCurve+1, distribution)) # +1 on elOnCurve because gmsh actually takes the number of nodes on the curve, not elements on the curve. _insertInSetDict(curveMarkers, marker, ID) # WRITE SURFACES: for ID, [surfName, outerLoop, holes, ID, marker, isStructured] in self.geometry.surfaces.items(): # First we write line loops for the surface edge and holes (if there are any holes): self._writeLineLoop(outerLoop, ID+1) holeIDs = [] for hole, i in zip(holes, range(len(holes))): # Create a hopefully unique ID-number for the line loop: Like 10015 or 1540035 # (If gmsh uses 32-bit integers for IDs then IDs over 214'748 will break) holeID = 10000 * (ID+1) + 10 * i + 5 self._writeLineLoop(hole, holeID) holeIDs.append(holeID) # Second, we write the surface itself: # If we have hole we want to include them in the surface. holeString = "" if not holeIDs else ", " + _formatList(holeIDs) # Like "Plane Surface(2) = {4, 2, 6, 8} self.geofile.write("%s(%i) = {%s%s};\n" % ( surfName, ID+1, ID+1, holeString)) # Lastly, we make the surface transfinite if it is a structured surface: if isStructured: cornerPoints = set() # Find the corner points. This is possibly unnecessary since Gmsh can do this automatically. for c in outerLoop: curvePoints = self.geometry.curves[c][1] cornerPoints.add(curvePoints[0]) cornerPoints.add(curvePoints[-1]) cornerPoints = list(cornerPoints) self.geofile.write("Transfinite Surface{%i} = {%s};\n" % ( ID+1, _formatList(cornerPoints, 1))) # Like Transfinite Surface{1} = {1,2,3,4}; # Transfinite Surface has an optional argument (about triangle orientation) that is not implemented here. _insertInSetDict(surfaceMarkers, marker, ID) # WRITE VOLUMES: for ID, [outerLoop, holes, ID, marker, isStructured] in self.geometry.volumes.items(): # Surface loops for the volume boundary and holes (if any): self._writeSurfaceLoop(outerLoop, ID+1) holeIDs = [] for hole, i in zip(holes, range(len(holes))): # ID-number for the hole surface loop holeID = 10000 * (ID+1) + 10 * i + 7 self._writeSurfaceLoop(hole, holeID) holeIDs.append(holeID) # Write the volume itself: # If we have hole we want to include them in the surface. holeString = "" if not holeIDs else " , " + _formatList(holeIDs) # Like "Plane Surface(2) = {4, 2, 6, 8} self.geofile.write( "Volume(%i) = {%s%s};\n" % (ID+1, ID+1, holeString)) # Lastly, we make the volume transfinite if it is a structured volume: if isStructured: self.geofile.write("Transfinite Volume{%i} = {};\n" % (ID+1)) # We don't find the corner points of the structured volume like we did with the surfaces. Gmsh can actually find the corners automatically. _insertInSetDict(volumeMarkers, marker, ID) # MAYBE MAKE QUADS: if(self.el_type in self._ElementsWithQuadFaces): # If we have quads surfaces on the elements self.geofile.write("Mesh.RecombineAll = 1;\n") # WRITE POINT MARKERS: for marker, IDlist in pointMarkers.items(): if marker != 0: self.geofile.write("Physical Point(%i) = {%s};\n" % ( marker, _formatList(IDlist, 1))) # WRITE CURVE MARKERS: for marker, IDlist in curveMarkers.items(): self.geofile.write("Physical Line(%i) = {%s};\n" % ( marker, _formatList(IDlist, 1))) # WRITE SURFACE MARKERS: for marker, IDlist in surfaceMarkers.items(): self.geofile.write("Physical Surface(%i) = {%s};\n" % ( marker, _formatList(IDlist, 1))) # WRITE SURFACE MARKERS: for marker, IDlist in volumeMarkers.items(): self.geofile.write("Physical Volume(%i) = {%s};\n" % ( marker, _formatList(IDlist, 1))) # If the element type is of an incomplete second order type # (i.e it is an 2nd order element without nodes in the middle of the element face), # then we need to specify this in the geo-file: if self.el_type in self._2dOrderIncompleteElms: self.geofile.write("Mesh.SecondOrderIncomplete=1;\n") def _writeLineLoop(self, lineIndices, loopID): # endPoints is used to keep track of at which points the curves start and end (i.e the direction of the curves) endPoints = [] # lineIndices is a list of curve indices (0-based here, but 1-based later in the method) for i in lineIndices: curvePoints = self.geometry.curves[i][1] endPoints.append([curvePoints[0], curvePoints[-1]]) # We need the indices to be 1-based rather than 0-based in the next loop. (Some indices will be preceded by a minus-sign) lineIndices = _offsetIndices(lineIndices, 1) isFirstLine = True nbrLinesinLoop = len(lineIndices) # In this loop we reverse the direction of some lines in the LineLoop to make them conform to the format that Gmsh expects. for k in range(nbrLinesinLoop): if isFirstLine and nbrLinesinLoop > 1: isFirstLine = False # If last point of the first line exists in the endpoints of the second line... Do nothing if endPoints[0][1] in endPoints[1]: pass # Else if the first point in the first line exists in the endpoints of the second line: elif endPoints[0][0] in endPoints[1]: endPoints[0].reverse() lineIndices[0] *= -1 # Reverse the direction of the line else: raise Exception( "ERROR: The first curve of line-loop %i does not link up to the subsequent curve" % loopID) elif endPoints[k][0] == endPoints[k-1][1]: pass elif endPoints[k][1] == endPoints[k-1][1]: endPoints[k].reverse() lineIndices[k] *= -1 # Reverse the direction of the line else: raise Exception( "ERROR: The %i th curve (starting from 0) of a line-loop %i does not link up with the preceding curve" % (k, loopID)) if k == nbrLinesinLoop-1 and endPoints[k][1] != endPoints[0][0]: # If last line AND the last point of the last curve not equal the first point of the first curve: raise Exception( "ERROR: The last curve of a line-loop %i does not join up with the first curve" % loopID) # If the model is in 2D we need to make all line loops counter-clockwise so surface normals point in the positive z-direction. if not self.geometry.is3D: lineIndices = self._makeCounterClockwise(lineIndices) self.geofile.write("Line Loop(%i) = {%s};\n" % (loopID, _formatList( lineIndices))) # (lineIndices are alreay 1-based here) def _makeCounterClockwise(self, lineIndices): '''If the lineIndices describe a line loop that is not counterclockwise, this function will return a counterclockwise version of lineIndices (i.e. all indices multiplied by -1). lineIndices is a list of integers (1-based line indices) that may be negative, but not 0''' # Method described at http://stackoverflow.com/questions/1165647/how-to-determine-if-a-list-of-polygon-points-are-in-clockwise-order summa = 0.0 # Counter-clockwise if the sum ends up negative. for index in lineIndices: sign = -1 if index < 0 else 1 # Make a copy of the line index that is positive and 0-based. realIndex = sign*index - 1 curveType = self.geometry.curves[realIndex][0] pointIDs = self.geometry.curves[realIndex][1] if curveType in ['Spline', 'BSpline']: points = [self.geometry.points[ID][0] for ID in pointIDs] # [[x,y,z], [x,y,z], ...] # Reverse the order of the points if the curve direction is reversed. points = points if sign == 1 else points[::-1] # For every point along the curve except the last: for i in range(len(pointIDs)-1): # (x2-x1)(y2+y1). summa += (points[i+1][0] - points[i][0]) * \ (points[i+1][1] + points[i][1]) elif curveType == 'Circle': # We will find a point 'd' on the middle of the circle arc, and use a-d-c as approximation of the arc. # 3-by-3 array. The rows are start-center-end points and columns are x,y,z. points = np.array([self.geometry.points[ID][0] for ID in pointIDs]) # Reverse the order of the points if the curve direction is reversed. points = points if sign == 1 else points[::-1] a = points[0, :] # start b = points[1, :] # center c = points[2, :] # end r = np.linalg.norm(a-b) # radius d = b + r * (a + 2*b + c) / np.linalg.norm(a + 2*b + c) approxArc = np.vstack((a, d, c)) for i in range(len(approxArc)-1): # (x2-x1)(y2+y1). summa += (approxArc[i+1, 0] - approxArc[i, 0]) * \ (approxArc[i+1, 1] + approxArc[i, 1]) elif curveType == 'Ellipse': # We will find a point 'd' near the middle of the circle arc, and use a-d-c as approximation of the arc. # The only difference from the circle above, is that the radius at d is approximated as the mean distance between # the center and the two other points. # 4-by-3 array. The rows are start-center-majAxis-end points and columns are x,y,z. points = np.array([self.geometry.points[ID][0] for ID in pointIDs]) # skip the major axis point (row 2) points = points[[0, 1, 3], :] # Reverse the order of the points if the curve direction is reversed. points = points if sign == 1 else points[::-1] a = points[0, :] # start b = points[1, :] # center c = points[2, :] # end r = (np.linalg.norm(a-b) + np.linalg.norm(c-b)) / \ 2 # approximate radius d = b + r * (a + 2*b + c) / np.linalg.norm(a + 2*b + c) approxArc = np.vstack((a, d, c)) for i in range(len(approxArc)-1): # (x2-x1)(y2+y1). summa += (approxArc[i+1, 0] - approxArc[i, 0]) * \ (approxArc[i+1, 1] + approxArc[i, 1]) # If the sum is positive the loop (closed polygon) is clockwise, so reverse the direction of all curves: if summa > 0: lineIndices = [-x for x in lineIndices] return lineIndices def _writeSurfaceLoop(self, outerLoop, ID): self.geofile.write("Surface Loop(%i) = {%s};\n" % ( ID, _formatList(outerLoop, 1))) # --- Compatibility properties @property def elType(self): return self.el_type @elType.setter def elType(self, value): self.el_type = value @property def elSizeFactor(self): return self.el_size_factor @elSizeFactor.setter def elSizeFactor(self, value): self.el_size_factor = value @property def dofsPerNode(self): return self.dofs_per_node @dofsPerNode.setter def dofsPerNode(self, value): self.dofs_per_node = value @property def gmshExecPath(self): return self.gmsh_exec_path @gmshExecPath.setter def gmshExecPath(self, value): self.gmsh_exec_path = value @property def minSize(self): return self.min_size @minSize.setter def minSize(self, value): self.min_size = value @property def maxSize(self): return self.max_size @maxSize.setter def maxSize(self, value): self.max_size = value @property def meshingAlgorithm(self): return self.meshing_algorithm @meshingAlgorithm.setter def meshingAlgorithm(self, value): self.meshing_algorithm = value @property def additionalOptions(self): return self.additional_options @additionalOptions.setter def additionalOptions(self, value): self.additional_options = value @property def meshDir(self): return self.mesh_dir @meshDir.setter def meshDir(self, value): self.mesh_dir = value @property def returnBoundaryElements(self): return self.return_boundary_elements @returnBoundaryElements.setter def returnBoundaryElements(self, value): self.return_boundary_elements = value
GmshMesh = GmshMeshGenerator
[docs]def trimesh2d(vertices, segments=None, holes=None, maxArea=None, quality=True, dofs_per_node=1, logFilename="tri.log", triangleExecutablePath=None): """ Triangulates an area described by a number vertices (vertices) and a set of segments that describes a closed polygon. Parameters: vertices array [nVertices x 2] with vertices describing the geometry. [[v0_x, v0_y], [ ... ], [vn_x, vn_y]] segments array [nSegments x 3] with segments describing the geometry. [[s0_v0, s0_v1,marker], [ ... ], [sn_v0, sn_v1,marker]] holes [Not currently used] maxArea Maximum area for triangle. (None) quality If true, triangles are prevented having angles < 30 degrees. (True) dofs_per_node Number of degrees of freedom per node. logFilename Filename for triangle output ("tri.log") Returns: coords Node coordinates [[n0_x, n0_y], [ ... ], [nn_x, nn_y]] edof Element topology [[el0_dof1, ..., el0_dofn], [ ... ], [eln_dof1, ..., eln_dofn]] dofs Node dofs [[n0_dof1, ..., n0_dofn], [ ... ], [nn_dof1, ..., nn_dofn]] bdofs Boundary dofs. Dictionary containing lists of dofs for each boundary marker. Dictionary key = marker id. """ # Check for triangle executable triangleExecutable = triangleExecutablePath if triangleExecutable == None: triangleExecutable = "" if sys.platform == "win32": triangleExecutable = which("triangle.exe") else: triangleExecutable = which("triangle") else: if not os.path.exists(triangleExecutable): triangleExecutable = None if triangleExecutable == None: error("Error: Could not find triangle. Please make sure that the \ntriangle executable is available on the search path (PATH).") return None, None, None, None # Create triangle options options = "" if maxArea != None: options += "-a%f " % maxArea + " " if quality: options += "-q" # Set initial variables nSegments = 0 nHoles = 0 nAttribs = 0 nBoundaryMarkers = 1 nVertices = len(vertices) # All files are created as temporary files if not os.path.exists("./trimesh.temp"): os.mkdir("./trimesh.temp") filename = "./trimesh.temp/polyfile.poly" if not segments is None: nSegments = len(segments) if not holes is None: nHoles = len(holes) # Create a .poly file polyFile = open(filename, "w") polyFile.write("%d 2 %d \n" % (nVertices, nAttribs)) i = 0 for vertex in vertices: polyFile.write("%d %g %g\n" % (i, vertex[0], vertex[1])) i = i + 1 polyFile.write("%d %d \n" % (nSegments, nBoundaryMarkers)) i = 0 for segment in segments: polyFile.write("%d %d %d %d\n" % (i, segment[0], segment[1], segment[2])) i = i + 1 polyFile.write("0\n") polyFile.close() # Execute triangle os.system("%s %s %s > tri.log" % (triangleExecutable, options, filename)) # Read results from triangle strippedName = os.path.splitext(filename)[0] nodeFilename = "%s.1.node" % strippedName elementFilename = "%s.1.ele" % strippedName polyFilename = "%s.1.poly" % strippedName # Read vertices allVertices = None boundaryVertices = {} if os.path.exists(nodeFilename): nodeFile = open(nodeFilename, "r") nodeInfo = list(map(int, nodeFile.readline().split())) nNodes = nodeInfo[0] allVertices = np.zeros([nNodes, 2], 'd') for i in range(nNodes): vertexRow = list(map(float, nodeFile.readline().split())) boundaryMarker = int(vertexRow[3]) if not (boundaryMarker in boundaryVertices): boundaryVertices[boundaryMarker] = [] allVertices[i, :] = [vertexRow[1], vertexRow[2]] boundaryVertices[boundaryMarker].append(i+1) nodeFile.close() # Read elements elements = [] if os.path.exists(elementFilename): elementFile = open(elementFilename, "r") elementInfo = list(map(int, elementFile.readline().split())) nElements = elementInfo[0] elements = np.zeros([nElements, 3], 'i') for i in range(nElements): elementRow = list(map(int, elementFile.readline().split())) elements[i, :] = [elementRow[1]+1, elementRow[2]+1, elementRow[3]+1] elementFile.close() # Clean up try: pass # os.remove(filename) # os.remove(nodeFilename) # os.remove(elementFilename) # os.remove(polyFilename) except: pass # Add dofs in edof and bcVerts dofs = cfc.createdofs(np.size(allVertices, 0), dofs_per_node) if dofs_per_node > 1: expandedElements = np.zeros( (np.size(elements, 0), 3*dofs_per_node), 'i') dofs = cfc.createdofs(np.size(allVertices, 0), dofs_per_node) elIdx = 0 for elementTopo in elements: for i in range(3): expandedElements[elIdx, i*dofs_per_node:( i*dofs_per_node+dofs_per_node)] = dofs[elementTopo[i]-1, :] elIdx += 1 for bVertIdx in boundaryVertices.keys(): bVert = boundaryVertices[bVertIdx] bVertNew = [] for i in range(len(bVert)): for j in range(dofs_per_node): bVertNew.append(dofs[bVert[i]-1][j]) boundaryVertices[bVertIdx] = bVertNew return allVertices, np.asarray(expandedElements), dofs, boundaryVertices return allVertices, elements, dofs, boundaryVertices