1 |
|
2 |
############################################################################## |
3 |
# |
4 |
# Copyright (c) 2003-2020 by The University of Queensland |
5 |
# http://www.uq.edu.au |
6 |
# |
7 |
# Primary Business: Queensland, Australia |
8 |
# Licensed under the Apache License, version 2.0 |
9 |
# http://www.apache.org/licenses/LICENSE-2.0 |
10 |
# |
11 |
# Development until 2012 by Earth Systems Science Computational Center (ESSCC) |
12 |
# Development 2012-2013 by School of Earth Sciences |
13 |
# Development from 2014 by Centre for Geoscience Computing (GeoComp) |
14 |
# Development from 2019 by School of Earth and Environmental Sciences |
15 |
# |
16 |
############################################################################## |
17 |
|
18 |
from __future__ import print_function, division |
19 |
|
20 |
__copyright__="""Copyright (c) 2003-2020 by The University of Queensland |
21 |
http://www.uq.edu.au |
22 |
Primary Business: Queensland, Australia""" |
23 |
__license__="""Licensed under the Apache License, version 2.0 |
24 |
http://www.apache.org/licenses/LICENSE-2.0""" |
25 |
__url__="https://launchpad.net/escript-finley" |
26 |
|
27 |
""" |
28 |
mesh generation using Triangle |
29 |
|
30 |
:var __author__: name of author |
31 |
:var __copyright__: copyrights |
32 |
:var __license__: licence agreement |
33 |
:var __url__: url entry point on documentation |
34 |
:var __version__: version |
35 |
:var __date__: date of the version |
36 |
""" |
37 |
|
38 |
__author__="Brett Tully, b.tully@uq.edu.au" |
39 |
|
40 |
import tempfile |
41 |
import os |
42 |
import glob |
43 |
import esys.pycad.design as design |
44 |
import math |
45 |
from esys.pycad.primitives import Point, Spline, BezierCurve, BSpline, Line, Arc, CurveLoop, RuledSurface, PlaneSurface, SurfaceLoop, Volume, PropertySet |
46 |
from esys.escript import getMPIWorldMax, getMPIRankWorld |
47 |
|
48 |
class Design(design.AbstractDesign): |
49 |
""" |
50 |
Design for Triangle. |
51 |
""" |
52 |
def __init__(self,dim=2,keep_files=False): |
53 |
""" |
54 |
Initializes the Triangle design. |
55 |
|
56 |
:param dim: spatial dimension |
57 |
:param keep_files: flag to keep work files |
58 |
""" |
59 |
if dim != 2: |
60 |
raise ValueError("only dimension 2 is supported by Triangle.") |
61 |
design.AbstractDesign.__init__(self,dim=dim,keep_files=keep_files) |
62 |
self.__scriptname="" |
63 |
self.setScriptFileName() |
64 |
self.setMeshFileName() |
65 |
self.setOptions() |
66 |
|
67 |
def setScriptFileName(self,name=None): |
68 |
""" |
69 |
Sets the filename for the Triangle input script. If no name is given |
70 |
a name with extension `poly` is generated. |
71 |
""" |
72 |
if self.__scriptname: |
73 |
os.unlink(self.__scriptname) |
74 |
if name == None: |
75 |
self.__scriptname_set=False |
76 |
self.__scriptname=tempfile.mkstemp(suffix=".poly")[1] |
77 |
else: |
78 |
self.__scriptname_set=True |
79 |
self.__scriptname=name |
80 |
self.setMeshFileName(name) |
81 |
|
82 |
def getScriptFileName(self): |
83 |
""" |
84 |
Returns the name of the gmsh script file. |
85 |
""" |
86 |
return self.__scriptname |
87 |
|
88 |
def setMeshFileName(self,name=None): |
89 |
""" |
90 |
Sets the name of the Triangle mesh file. |
91 |
""" |
92 |
if self.__mshname: |
93 |
os.unlink(self.__mshname) |
94 |
if name == None: |
95 |
self.__mshname=tempfile.mkstemp(suffix="")[1] |
96 |
else: |
97 |
# Triangle creates a default filename so all we want to pass is |
98 |
# the basename |
99 |
if (".poly" in name) or (".ele" in name) or (".node" in name): |
100 |
s=name.split(".")[:-1] |
101 |
name=s[0] |
102 |
if len(s) > 1: name+=".%s"*(len(s)-1)%tuple(s[1:]) |
103 |
self.__mshname=name |
104 |
self.setKeepFilesOn() |
105 |
|
106 |
def getMeshFileName(self): |
107 |
""" |
108 |
Returns the name of the Triangle mesh file. |
109 |
""" |
110 |
return self.__mshname |
111 |
|
112 |
def setOptions(self,cmdLineArgs=""): |
113 |
""" |
114 |
Sets command line options for the mesh generator:: |
115 |
|
116 |
triangle [-prq__a__uAcDjevngBPNEIOXzo_YS__iFlsCQVh] input_file |
117 |
|
118 |
see http://www.cs.cmu.edu/~quake/triangle.switch.html |
119 |
|
120 |
:param cmdLineArgs: the switches you would ordinarily use at the |
121 |
command line (e.g. cmdLineArgs="pq25a7.5") |
122 |
""" |
123 |
self.__cmdLineArgs=cmdLineArgs |
124 |
|
125 |
def __del__(self): |
126 |
""" |
127 |
Cleans up. |
128 |
""" |
129 |
if not self.keepFiles(): |
130 |
if not self.__scriptname_set: |
131 |
os.unlink(self.getScriptFileName()) |
132 |
if not self.__mshname_set: |
133 |
os.unlink(self.getMeshFileName()) |
134 |
|
135 |
def getCommandString(self): |
136 |
""" |
137 |
Returns the Triangle command line:: |
138 |
|
139 |
triangle [-prq__a__uAcDjevngBPNEIOXzo_YS__iFlsCQVh] input_file |
140 |
|
141 |
see http://www.cs.cmu.edu/~quake/triangle.switch.html |
142 |
""" |
143 |
if self.__cmdLineArgs == "": |
144 |
print("warning: using default command line arguments for Triangle") |
145 |
exe="triangle %s %%s"%self.__cmdLineArgs |
146 |
return exe |
147 |
|
148 |
def getMeshHandler(self): |
149 |
""" |
150 |
Returns a handle to a mesh meshing the design. In the current |
151 |
implementation a mesh file name in Triangle format is returned. |
152 |
""" |
153 |
args = self.getCommandString().split() |
154 |
args[-1]=args[-1]%self.getScriptFileName() |
155 |
if getMPIRankWorld() == 0: |
156 |
import subprocess |
157 |
open(self.getScriptFileName(),"w").write(self.getScriptString()) |
158 |
ret = subprocess.call(args) / 256 |
159 |
else: |
160 |
ret=0 |
161 |
ret=getMPIWorldMax(ret) |
162 |
if ret > 0: |
163 |
raise RuntimeError("Could not build mesh: %s"%" ".join(args)) |
164 |
else: |
165 |
# <hack> so that users can set the mesh filename they want. |
166 |
name=self.getScriptFileName() |
167 |
if (".poly" in name) or (".ele" in name) or (".node" in name): |
168 |
s=name.split(".")[:-1] |
169 |
name=s[0] |
170 |
if len(s) > 1: name+=".%s"*(len(s)-1)%tuple(s[1:]) |
171 |
files=glob.glob("%s.1.*"%name) |
172 |
for f in files: |
173 |
sufx=f.split(".")[-1] |
174 |
mshName=self.getMeshFileName()+"."+sufx |
175 |
os.rename(f,mshName) |
176 |
# </hack> |
177 |
return self.getMeshFileName() |
178 |
|
179 |
def getScriptString(self): |
180 |
""" |
181 |
Returns the Triangle script to generate the mesh. |
182 |
""" |
183 |
# comment lines are prefixed by a '#' in triangle *.poly files |
184 |
out="# generated by esys.pycad\n" |
185 |
vertices="";vertCnt=0 |
186 |
segments="";segCnt=0 |
187 |
holestr="";holeCnt=0 |
188 |
ctrlPts={} |
189 |
for prim in self.getItems(): |
190 |
|
191 |
p=prim.getUnderlyingPrimitive() |
192 |
|
193 |
if isinstance(p, PropertySet): |
194 |
PSprims=p.getItems() |
195 |
for PSp in PSprims: |
196 |
if isinstance(PSp, Point): |
197 |
c=PSp.getCoordinates() |
198 |
vertCnt+=1 |
199 |
vertices+="%s %s %s %s\n"%(vertCnt,c[0],c[1],p.getID()) |
200 |
|
201 |
elif isinstance(PSp, Line): |
202 |
# get end points and add them as vertices |
203 |
# add line segments - bnd_mkr's are p.getID() |
204 |
# and p.getID() should be mapped to the FID's from the GIS |
205 |
pts=list(PSp.getControlPoints()) |
206 |
for pt in pts: |
207 |
c=pt.getCoordinates() |
208 |
if pt not in list(ctrlPts.keys()): |
209 |
vertCnt+=1 |
210 |
vertices+="%s %s %s %s\n"%(vertCnt,c[0],c[1],p.getID()) |
211 |
ctrlPts[pt]=vertCnt |
212 |
ptIndx=pts.index(pt) |
213 |
if ptIndx != 0: |
214 |
segCnt+=1 |
215 |
segments+="%s %s %s %s\n"%(segCnt, |
216 |
ctrlPts[pts[ptIndx-1]], |
217 |
ctrlPts[pts[ptIndx]], |
218 |
p.getID()) |
219 |
|
220 |
elif isinstance(PSp, Spline) or isinstance(PSp, BezierCurve) or \ |
221 |
isinstance(PSp, BSpline) or isinstance(PSp, Arc): |
222 |
TypeError("Triangle can only handle linear curves: not %s objects."%str(type(p))) |
223 |
|
224 |
elif isinstance(PSp, PlaneSurface): |
225 |
|
226 |
outerBnd=PSp.getBoundaryLoop() |
227 |
holes=PSp.getHoles() |
228 |
|
229 |
for curve in outerBnd.getCurves(): |
230 |
pts=list(curve.getControlPoints()) |
231 |
for pt in pts: |
232 |
c=pt.getCoordinates() |
233 |
if pt not in list(ctrlPts.keys()): |
234 |
vertCnt+=1 |
235 |
vertices+="%s %s %s %s\n"%(vertCnt,c[0],c[1],p.getID()) |
236 |
ctrlPts[pt]=vertCnt |
237 |
ptIndx=pts.index(pt) |
238 |
if ptIndx != 0: |
239 |
segCnt+=1 |
240 |
segments+="%s %s %s %s\n"%(segCnt, |
241 |
ctrlPts[pts[ptIndx-1]], |
242 |
ctrlPts[pts[ptIndx]], |
243 |
p.getID()) |
244 |
# in order to deal with holes in Triangle, you must place a hole node inside |
245 |
# the boundary of the polygon that describes the hole. For a convex polygon |
246 |
# (all internal angles < 180 degrees) this is easy, however, for concave |
247 |
# polygons (one or more internal angles > 180 degrees) this is much |
248 |
# more difficult. Easiest method is to find the smallest internal angle, and |
249 |
# place the hole node inside the triangle formed by the three vertices |
250 |
# associated with that internal angle. |
251 |
for hole in holes: |
252 |
holePts=[] |
253 |
for curve in hole.getCurves(): |
254 |
pts=list(curve.getControlPoints()) |
255 |
for pt in pts: |
256 |
c=pt.getCoordinates() |
257 |
if pt not in list(ctrlPts.keys()): |
258 |
vertCnt+=1 |
259 |
vertices+="%s %s %s %s\n"%(vertCnt,c[0],c[1],p.getID()) |
260 |
ctrlPts[pt]=vertCnt |
261 |
ptIndx=pts.index(pt) |
262 |
if ptIndx != 0: |
263 |
segCnt+=1 |
264 |
segments+="%s %s %s %s\n"%(segCnt, |
265 |
ctrlPts[pts[ptIndx-1]], |
266 |
ctrlPts[pts[ptIndx]], |
267 |
p.getID()) |
268 |
if pt not in holePts: |
269 |
holePts.append(pt) |
270 |
vectors=[] # the key corresponds to the ctrlPts index |
271 |
# create vectors |
272 |
for i in range(len(holePts)): |
273 |
A=holePts[i] |
274 |
vectors.append([]) |
275 |
if i == 0: |
276 |
B=holePts[1] |
277 |
C=holePts[-1] |
278 |
elif i == len(holePts)-1: |
279 |
B=holePts[0] |
280 |
C=holePts[-2] |
281 |
else: |
282 |
B=holePts[i+1] |
283 |
C=holePts[i-1] |
284 |
vectors[i].append(self.__getVector(A,B)) |
285 |
vectors[i].append(self.__getVector(A,C)) |
286 |
# get angle between vectors at each vertex |
287 |
for i in range(len(vectors)): |
288 |
angle=self.__getAngle(vectors[i][0],vectors[i][1]) |
289 |
vectors[i].append(angle) |
290 |
# find the vertex with the smallest angle |
291 |
minAngle=360. |
292 |
indx=0 |
293 |
for i in range(len(vectors)): |
294 |
if vectors[i][2] < minAngle: |
295 |
indx=i |
296 |
minAngle=vectors[i][2] |
297 |
# find a node inside the triangle stemming from the |
298 |
# vertex with the smallest internal angle. Do this by |
299 |
# adding 5% of each of the vectorsesys.pycad. to the current point |
300 |
A=holePts[indx] |
301 |
cA=A.getCoordinates() |
302 |
x=cA[0]+(vectors[indx][0][0]+vectors[indx][1][0])/20. |
303 |
y=cA[1]+(vectors[indx][0][1]+vectors[indx][1][1])/20. |
304 |
# use this node to define the hole. |
305 |
holeCnt+=1 |
306 |
holestr+="%s %s %s\n"%(holeCnt,x,y) |
307 |
|
308 |
else: |
309 |
raise TypeError("please pass correct PropertySet objects to Triangle design") |
310 |
else: |
311 |
raise TypeError("please pass only PropertySet objects to Triangle design") |
312 |
out+="# vertices #\n" |
313 |
out+="%i 2 0 1\n"%(vertCnt) |
314 |
out+=vertices |
315 |
out+="# segments #\n" |
316 |
out+="%i 1\n"%(segCnt) |
317 |
out+=segments |
318 |
out+="# holes #\n" |
319 |
out+="%i\n"%(holeCnt) |
320 |
out+=holestr |
321 |
return out |
322 |
|
323 |
def __getVector(self,A,B): |
324 |
# get the vector between two points. |
325 |
cA=A.getCoordinates() |
326 |
cB=B.getCoordinates() |
327 |
x=cB[0]-cA[0] |
328 |
y=cB[1]-cA[1] |
329 |
return [x,y] |
330 |
|
331 |
def __getAngle(self,v,w): |
332 |
# get internal (directional) angle between two vectors (in degrees). |
333 |
theta=atan2(v[1],v[0]) - atan2(w[1],w[0]) |
334 |
theta=theta*180./pi |
335 |
if theta < 0.: |
336 |
theta+=360. |
337 |
return theta |
338 |
|