Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/plot/plot3d/tri_plot.py
4038 views
1
r"""
2
Adaptive refinement code for 3d surface plotting
3
4
AUTHOR:
5
-- Tom Boothby -- Algorithm design, code
6
-- Joshua Kantor -- Algorithm design
7
-- Marshall Hampton -- Docstrings and doctests
8
9
TODO:
10
-- Parametrizations (cylindrical, spherical)
11
-- Massive optimization
12
13
###########################################################################
14
# Copyright (C) 2007 Tom Boothby <[email protected]>
15
# 2007 Joshua Kantor <[email protected]>
16
#
17
# Distributed under the terms of the GNU General Public License (GPL)
18
# http://www.gnu.org/licenses/
19
###########################################################################
20
21
22
"""
23
24
from sage.plot.colors import hue
25
from math import sqrt
26
from random import random
27
28
class Triangle:
29
"""
30
A graphical triangle class.
31
"""
32
def __init__(self,a,b,c,color=0):
33
"""
34
a, b, c : triples (x,y,z) representing corners on a triangle in 3-space.
35
36
TESTS::
37
38
sage: from sage.plot.plot3d.tri_plot import Triangle
39
sage: tri = Triangle([0,0,0],[-1,2,3],[0,2,0])
40
sage: tri._a
41
[0, 0, 0]
42
sage: tri.__init__([0,0,1],[-1,2,3],[0,2,0])
43
sage: tri._a
44
[0, 0, 1]
45
"""
46
self._a = a
47
self._b = b
48
self._c = c
49
self._color = color
50
51
def __repr__(self):
52
"""
53
Returns a string description of an instance of the Triangle class of the form
54
a b c color
55
where a, b, and c are corner coordinates and color is the color.
56
57
TESTS::
58
59
sage: from sage.plot.plot3d.tri_plot import Triangle
60
sage: tri = Triangle([0,0,0],[-1,2,3],[0,2,0])
61
sage: print tri.__repr__()
62
[0, 0, 0] [-1, 2, 3] [0, 2, 0] 0
63
"""
64
return "%s %s %s %s"%(self._a, self._b, self._c, self._color)
65
66
def set_color(self, color):
67
"""
68
This method will reset the color of the triangle.
69
70
TESTS::
71
72
sage: from sage.plot.plot3d.tri_plot import Triangle
73
sage: tri = Triangle([0,0,0],[-1,2,3],[0,2,1])
74
sage: tri._color
75
0
76
sage: tri.set_color(1)
77
sage: tri._color
78
1
79
"""
80
self._color = color
81
82
def get_vertices(self):
83
"""
84
Returns a tuple of vertex coordinates of the triangle.
85
86
TESTS::
87
88
sage: from sage.plot.plot3d.tri_plot import Triangle
89
sage: tri = Triangle([0,0,0],[-1,2,3],[0,2,1])
90
sage: tri.get_vertices()
91
([0, 0, 0], [-1, 2, 3], [0, 2, 1])
92
"""
93
return (self._a, self._b, self._c)
94
95
class SmoothTriangle(Triangle):
96
"""
97
A class for smoothed triangles.
98
"""
99
def __init__(self,a,b,c,da,db,dc,color=0):
100
"""
101
a, b, c : triples (x,y,z) representing corners on a triangle in 3-space
102
da, db, dc : triples (dx,dy,dz) representing the normal vector at each point a,b,c
103
104
TESTS::
105
106
sage: from sage.plot.plot3d.tri_plot import SmoothTriangle
107
sage: t = SmoothTriangle([1,2,3],[2,3,4],[0,0,0],[0,0,1],[0,1,0],[1,0,0])
108
sage: t._a
109
[1, 2, 3]
110
"""
111
self._a = a
112
self._b = b
113
self._c = c
114
self._da = da
115
self._db = db
116
self._dc = dc
117
self._color = color
118
119
def __repr__(self):
120
"""
121
Returns a string representation of the SmoothTriangle of the form
122
a b c da db dc color
123
where a, b, and c are the triangle corner coordinates,
124
da, db, dc are normals at each corner, and color is the color.
125
126
TESTS::
127
128
sage: from sage.plot.plot3d.tri_plot import SmoothTriangle
129
sage: t = SmoothTriangle([1,2,3],[2,3,4],[0,0,0],[0,0,1],[0,1,0],[1,0,0])
130
sage: print t.__repr__()
131
[1, 2, 3] [2, 3, 4] [0, 0, 0] 0 [0, 0, 1] [0, 1, 0] [1, 0, 0]
132
"""
133
return "%s %s %s %s %s %s %s"%(self._a, self._b, self._c, self._color, self._da, self._db, self._dc)
134
135
def get_normals(self):
136
"""
137
Returns the normals to vertices a, b, and c.
138
139
TESTS::
140
141
sage: from sage.plot.plot3d.tri_plot import SmoothTriangle
142
sage: t = SmoothTriangle([1,2,3],[2,3,4],[0,0,0],[0,0,1],[0,1,0],[2,0,0])
143
sage: t.get_normals()
144
([0, 0, 1], [0, 1, 0], [2, 0, 0])
145
"""
146
return (self._da, self._db, self._dc)
147
148
149
class TriangleFactory:
150
def triangle(self, a, b, c, color = None):
151
"""
152
Parameters:
153
a, b, c : triples (x,y,z) representing corners on a triangle in 3-space
154
155
Returns:
156
a Triangle object with the specified coordinates
157
158
TESTS::
159
160
sage: from sage.plot.plot3d.tri_plot import TriangleFactory
161
sage: factory = TriangleFactory()
162
sage: tri = factory.triangle([0,0,0],[0,0,1],[1,1,0])
163
sage: tri.get_vertices()
164
([0, 0, 0], [0, 0, 1], [1, 1, 0])
165
"""
166
if color is None:
167
return Triangle(a,b,c)
168
else:
169
return Triangle(a,b,c,color)
170
171
def smooth_triangle(self, a, b, c, da, db, dc, color = None):
172
"""
173
Parameters:
174
a, b, c : triples (x,y,z) representing corners on a triangle in 3-space
175
da, db, dc : triples (dx,dy,dz) representing the normal vector at each point a,b,c
176
177
Returns:
178
a SmoothTriangle object with the specified coordinates and normals
179
180
TESTS::
181
182
sage: from sage.plot.plot3d.tri_plot import TriangleFactory
183
sage: factory = TriangleFactory()
184
sage: sm_tri = factory.smooth_triangle([0,0,0],[0,0,1],[1,1,0],[0,0,1],[0,2,0],[1,0,0])
185
sage: sm_tri.get_normals()
186
([0, 0, 1], [0, 2, 0], [1, 0, 0])
187
"""
188
if color is None:
189
return SmoothTriangle(a,b,c,da,db,dc)
190
else:
191
return SmoothTriangle(a,b,c,da,db,dc,color)
192
193
def get_colors(self, list):
194
"""
195
Parameters:
196
list: an iterable collection of values which can be cast into colors -- typically an
197
RGB triple, or an RGBA 4-tuple
198
199
Returns:
200
a list of single parameters which can be passed into the set_color method of the
201
Triangle or SmoothTriangle objects generated by this factory.
202
203
TESTS::
204
205
sage: from sage.plot.plot3d.tri_plot import TriangleFactory
206
sage: factory = TriangleFactory()
207
sage: factory.get_colors([1,2,3])
208
[1, 2, 3]
209
"""
210
return list
211
212
213
214
class TrianglePlot:
215
"""
216
Recursively plots a function of two variables by building squares of 4 triangles, checking at
217
every stage whether or not each square should be split into four more squares. This way,
218
more planar areas get fewer triangles, and areas with higher curvature get more triangles.
219
"""
220
221
def str(self):
222
"""
223
Returns a string listing the objects in the instance of the TrianglePlot class.
224
225
TESTS::
226
227
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
228
sage: tf = TriangleFactory()
229
sage: t = TrianglePlot(tf, lambda x,y: x^3+y*x-1, (-1, 3), (-2, 100), max_depth = 4)
230
sage: len(t.str())
231
68980
232
"""
233
return "".join([str(o) for o in self._objects])
234
235
def __init__(self, triangle_factory, f, (min_x, max_x), (min_y, max_y), g = None,
236
min_depth=4, max_depth=8, num_colors = None, max_bend=.3):
237
"""
238
239
TESTS::
240
241
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
242
sage: tf = TriangleFactory()
243
sage: t = TrianglePlot(tf, lambda x,y: x^2+y^2, (0, 1), (0, 1))
244
sage: t._f(1,1)
245
2
246
"""
247
self._triangle_factory = triangle_factory
248
self._f = f
249
self._g = g
250
self._min_depth = min_depth
251
self._max_depth = max_depth
252
self._max_bend = max_bend
253
self._objects = []
254
if min(max_x - min_x, max_y - min_y) == 0:
255
raise ValueError, 'Plot rectangle is really a line. Make sure min_x != max_x and min_y != max_y.'
256
self._num_colors = num_colors
257
if g is None:
258
def fcn(x,y):
259
return [self._f(x,y)]
260
else:
261
def fcn(x,y):
262
return [self._f(x,y), self._g(x,y)]
263
264
self._fcn = fcn
265
266
267
# generate the necessary data to kick-start the recursion
268
mid_x = (min_x + max_x)/2
269
mid_y = (min_y + max_y)/2
270
sw_z = fcn(min_x,min_y)
271
nw_z = fcn(min_x,max_y)
272
se_z = fcn(max_x,min_y)
273
ne_z = fcn(max_x,max_y)
274
mid_z = fcn(mid_x,mid_y)
275
276
self._min = min(sw_z[0], nw_z[0], se_z[0], ne_z[0], mid_z[0])
277
self._max = max(sw_z[0], nw_z[0], se_z[0], ne_z[0], mid_z[0])
278
279
# jump in and start building blocks
280
outer = self.plot_block(min_x, mid_x, max_x, min_y, mid_y, max_y, sw_z, nw_z, se_z, ne_z, mid_z, 0)
281
282
# build the boundary triangles
283
self.triangulate(outer.left, outer.left_c)
284
self.triangulate(outer.top, outer.top_c)
285
self.triangulate(outer.right, outer.right_c)
286
self.triangulate(outer.bottom, outer.bottom_c)
287
288
zrange = self._max - self._min
289
if num_colors is not None and zrange != 0:
290
colors = triangle_factory.get_colors([hue(float(i/num_colors)) for i in range(num_colors)])
291
292
for o in self._objects:
293
vertices = o.get_vertices()
294
avg_z = (vertices[0][2] + vertices[1][2] + vertices[2][2])/3
295
o.set_color(colors[int(num_colors * (avg_z - self._min) / zrange)])
296
297
298
def plot_block(self, min_x, mid_x, max_x, min_y, mid_y, max_y, sw_z, nw_z, se_z, ne_z, mid_z, depth):
299
"""
300
Recursive triangulation function for plotting.
301
First six inputs are scalars, next 5 are 2-dimensional lists, and the depth argument
302
keeps track of the depth of recursion.
303
304
TESTS::
305
306
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
307
sage: tf = TriangleFactory()
308
sage: t = TrianglePlot(tf, lambda x,y: x^2 + y^2, (-1,1), (-1, 1), max_depth=3)
309
sage: q = t.plot_block(0,.5,1,0,.5,1,[0,1],[0,1],[0,1],[0,1],[0,1],2)
310
sage: q.left
311
[[(0, 0, 0)], [(0, 0.500000000000000, 0.250000000000000)], [(0, 1, 0)]]
312
"""
313
314
if depth < self._max_depth:
315
# recursion is still an option -- step in one last level if we're within tolerance
316
# and just keep going if we're not.
317
# assumption: it's cheap to build triangles, so we might as well use all the data
318
# we calculate
319
320
# big square boundary midpoints
321
mid_w_z = self._fcn(min_x, mid_y)
322
mid_n_z = self._fcn(mid_x, max_y)
323
mid_e_z = self._fcn(max_x, mid_y)
324
mid_s_z = self._fcn(mid_x, min_y)
325
326
next_depth = depth+1
327
if depth < self._min_depth:
328
# midpoints locations of sub_squares
329
qtr1_x = (min_x + mid_x)/2
330
qtr1_y = (min_y + mid_y)/2
331
qtr3_x = (mid_x + max_x)/2
332
qtr3_y = (mid_y + max_y)/2
333
334
sw_depth = next_depth
335
nw_depth = next_depth
336
se_depth = next_depth
337
ne_depth = next_depth
338
else:
339
#compute the midpoint-to-corner vectors
340
sw_v = (min_x - mid_x, min_y - mid_y, sw_z[0] - mid_z[0])
341
nw_v = (min_x - mid_x, max_y - mid_y, nw_z[0] - mid_z[0])
342
se_v = (max_x - mid_x, min_y - mid_y, se_z[0] - mid_z[0])
343
ne_v = (max_x - mid_x, max_y - mid_y, ne_z[0] - mid_z[0])
344
345
#compute triangle normal unit vectors by taking the cross-products
346
#of the midpoint-to-corner vectors. always go around clockwise
347
#so we're guaranteed to have a positive value near 1 when neighboring
348
#triangles are parallel
349
#However -- crossunit doesn't really return a unit vector. It returns
350
#the length of the vector to avoid numerical instability when the
351
#length is nearly zero -- rather than divide by nearly zero, we multiply
352
#the other side of the inequality by nearly zero -- in general, this
353
#should work a bit better because of the density of floating-point
354
#numbers near zero.
355
norm_w = crossunit(sw_v, nw_v)
356
norm_n = crossunit(nw_v, ne_v)
357
norm_e = crossunit(ne_v, se_v)
358
norm_s = crossunit(se_v, sw_v)
359
360
#compute the dot products of the triangle unit norms
361
e_sw = norm_w[0]*norm_s[0] + norm_w[1]*norm_s[1] + norm_w[2]*norm_s[2]
362
e_nw = norm_w[0]*norm_n[0] + norm_w[1]*norm_n[1] + norm_w[2]*norm_n[2]
363
e_se = norm_e[0]*norm_s[0] + norm_e[1]*norm_s[1] + norm_e[2]*norm_s[2]
364
e_ne = norm_e[0]*norm_n[0] + norm_e[1]*norm_n[1] + norm_e[2]*norm_n[2]
365
366
if e_sw < self._max_bend*norm_s[3]*norm_w[3]:
367
sw_depth = next_depth
368
else:
369
sw_depth = self._max_depth
370
if e_nw < self._max_bend*norm_n[3]*norm_w[3]:
371
nw_depth = next_depth
372
else:
373
nw_depth = self._max_depth
374
if e_se < self._max_bend*norm_s[3]*norm_e[3]:
375
se_depth = next_depth
376
else:
377
se_depth = self._max_depth
378
if e_ne < self._max_bend*norm_n[3]*norm_e[3]:
379
ne_depth = next_depth
380
else:
381
ne_depth = self._max_depth
382
383
qtr1_x = min_x + (.325 + random()/4)*(mid_x-min_x)
384
qtr3_x = mid_x + (.325 + random()/4)*(max_x-mid_x)
385
qtr1_y = min_y + (.325 + random()/4)*(mid_y-min_y)
386
qtr3_y = mid_y + (.325 + random()/4)*(max_y-mid_y)
387
388
# function evaluated at the midpoints (possibly random)
389
mid_sw_z = self._fcn(qtr1_x,qtr1_y)
390
mid_nw_z = self._fcn(qtr1_x,qtr3_y)
391
mid_se_z = self._fcn(qtr3_x,qtr1_y)
392
mid_ne_z = self._fcn(qtr3_x,qtr3_y)
393
394
395
self.extrema([mid_w_z[0], mid_n_z[0], mid_e_z[0], mid_s_z[0], mid_sw_z[0], mid_se_z[0], mid_nw_z[0], mid_sw_z[0]])
396
397
# recurse into the sub-squares
398
sw = self.plot_block(min_x, qtr1_x, mid_x, min_y, qtr1_y, mid_y, sw_z, mid_w_z, mid_s_z, mid_z, mid_sw_z, sw_depth)
399
nw = self.plot_block(min_x, qtr1_x, mid_x, mid_y, qtr3_y, max_y, mid_w_z, nw_z, mid_z, mid_n_z, mid_nw_z, nw_depth)
400
se = self.plot_block(mid_x, qtr3_x, max_x, min_y, qtr1_y, mid_y, mid_s_z, mid_z, se_z, mid_e_z, mid_se_z, se_depth)
401
ne = self.plot_block(mid_x, qtr3_x, max_x, mid_y, qtr3_y, max_y, mid_z, mid_n_z, mid_e_z, ne_z, mid_ne_z, ne_depth)
402
403
# join the sub-squares
404
self.interface(1, sw.right, sw.right_c, se.left, se.left_c)
405
self.interface(1, nw.right, nw.right_c, ne.left, ne.left_c)
406
self.interface(0, sw.top, sw.top_c, nw.bottom, nw.bottom_c)
407
self.interface(0, se.top, se.top_c, ne.bottom, ne.bottom_c)
408
409
#get the boundary information about the subsquares
410
left = sw.left + nw.left[1:]
411
left_c = sw.left_c + nw.left_c
412
right = se.right + ne.right[1:]
413
right_c = se.right_c + ne.right_c
414
top = nw.top + ne.top[1:]
415
top_c = nw.top_c + ne.top_c
416
bottom = sw.bottom + se.bottom[1:]
417
bottom_c = sw.bottom_c + se.bottom_c
418
419
else:
420
# just build the square we're in
421
if self._g is None:
422
sw = [(min_x,min_y,sw_z[0])]
423
nw = [(min_x,max_y,nw_z[0])]
424
se = [(max_x,min_y,se_z[0])]
425
ne = [(max_x,max_y,ne_z[0])]
426
c = [[(mid_x,mid_y,mid_z[0])]]
427
else:
428
sw = [(min_x,min_y,sw_z[0]),sw_z[1]]
429
nw = [(min_x,max_y,nw_z[0]),nw_z[1]]
430
se = [(max_x,min_y,se_z[0]),se_z[1]]
431
ne = [(max_x,max_y,ne_z[0]),ne_z[1]]
432
c = [[(mid_x,mid_y,mid_z[0]),mid_z[1]]]
433
434
435
left = [sw,nw]
436
left_c = c
437
top = [nw,ne]
438
top_c = c
439
right = [se,ne]
440
right_c = c
441
bottom = [sw,se]
442
bottom_c = c
443
444
return PlotBlock(left, left_c, top, top_c, right, right_c, bottom, bottom_c)
445
446
def interface(self, n, p, p_c, q, q_c):
447
"""
448
Takes a pair of lists of points, and compares the (n)th coordinate, and
449
"zips" the lists together into one. The "centers", supplied in p_c and
450
q_c are matched up such that the lists describe triangles whose sides
451
are "perfectly" aligned. This algorithm assumes that p and q start and
452
end at the same point, and are sorted smallest to largest.
453
454
TESTS::
455
456
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
457
sage: tf = TriangleFactory()
458
sage: t = TrianglePlot(tf, lambda x,y: x^2 - y*x, (0, -2), (0, 2), max_depth=3)
459
sage: t.interface(1, [[(-1/4, 0, 1/16)], [(-1/4, 1/4, 1/8)]], [[(-1/8, 1/8, 1/32)]], [[(-1/4, 0, 1/16)], [(-1/4, 1/4, 1/8)]], [[(-3/8, 1/8, 3/16)]])
460
sage: t._objects[-1]
461
(-1/4, 0, 1/16) (-1/4, 1/4, 1/8) (-3/8, 1/8, 3/16) 0
462
"""
463
m = [p[0]] # a sorted union of p and q
464
mpc = [p_c[0]] # centers from p_c corresponding to m
465
mqc = [q_c[0]] # centers from q_c corresponding to m
466
467
i = 1
468
j = 1
469
470
while i < len(p_c) or j < len(q_c):
471
if p[i][0][n] == q[j][0][n]:
472
m.append(p[i])
473
mpc.append(p_c[i])
474
mqc.append(q_c[j])
475
i += 1
476
j += 1
477
elif p[i][0][n] < q[j][0][n]:
478
m.append(p[i])
479
mpc.append(p_c[i])
480
mqc.append(mqc[-1])
481
i += 1
482
else:
483
m.append(q[j])
484
mpc.append(mpc[-1])
485
mqc.append(q_c[j])
486
j += 1
487
488
m.append(p[-1])
489
490
self.triangulate(m, mpc)
491
self.triangulate(m, mqc)
492
493
494
def triangulate(self, p, c):
495
"""
496
Pass in a list of edge points (p) and center points (c).
497
Triangles will be rendered between consecutive edge points and the
498
center point with the same index number as the earlier edge point.
499
500
TESTS::
501
502
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
503
sage: tf = TriangleFactory()
504
sage: t = TrianglePlot(tf, lambda x,y: x^2 - y*x, (0, -2), (0, 2))
505
sage: t.triangulate([[[1,0,0],[0,0,1]],[[0,1,1],[1,1,1]]],[[[0,3,1]]])
506
sage: t._objects[-1].get_vertices()
507
([1, 0, 0], [0, 1, 1], [0, 3, 1])
508
"""
509
510
if self._g is None:
511
for i in range(0,len(p)-1):
512
self._objects.append(self._triangle_factory.triangle(p[i][0], p[i+1][0], c[i][0]))
513
else:
514
for i in range(0,len(p)-1):
515
self._objects.append(self._triangle_factory.smooth_triangle(p[i][0], p[i+1][0], c[i][0],p[i][1], p[i+1][1], c[i][1]))
516
517
518
def extrema(self, list):
519
"""
520
If the num_colors option has been set, this expands the TrianglePlot's _min and _max
521
attributes to include the minimum and maximum of the argument list.
522
523
TESTS::
524
525
sage: from sage.plot.plot3d.tri_plot import TrianglePlot, TriangleFactory
526
sage: tf = TriangleFactory()
527
sage: t = TrianglePlot(tf, lambda x,y: x^2+y^2, (0, 1), (0, 1), num_colors = 3)
528
sage: t._min, t._max
529
(0, 2)
530
sage: t.extrema([-1,2,3,4])
531
sage: t._min, t._max
532
(-1, 4)
533
"""
534
if self._num_colors is not None:
535
self._min = min(list+[self._min])
536
self._max = max(list+[self._max])
537
538
539
def crossunit(u,v):
540
"""
541
This function computes triangle normal unit vectors by taking the
542
cross-products of the midpoint-to-corner vectors. It always goes
543
around clockwise so we're guaranteed to have a positive value near
544
1 when neighboring triangles are parallel. However -- crossunit
545
doesn't really return a unit vector. It returns the length of the
546
vector to avoid numerical instability when the length is nearly zero
547
-- rather than divide by nearly zero, we multiply the other side of
548
the inequality by nearly zero -- in general, this should work a bit
549
better because of the density of floating-point numbers near zero.
550
551
TESTS::
552
553
sage: from sage.plot.plot3d.tri_plot import crossunit
554
sage: crossunit([0,-1,0],[0,0,1])
555
(-1, 0, 0, 1.0)
556
"""
557
p = (u[1]*v[2] - v[1]*u[2], u[0]*v[2] - v[0]*u[2], u[0]*v[1] - u[1]*v[0])
558
l = sqrt(p[0]**2 + p[1]**2 + p[2]**2)
559
return (p[0], p[1], p[2], l)
560
561
562
class PlotBlock:
563
"""
564
A container class to hold information about spatial blocks.
565
"""
566
def __init__(self, left, left_c, top, top_c, right, right_c, bottom, bottom_c):
567
"""
568
569
TESTS::
570
571
sage: from sage.plot.plot3d.tri_plot import PlotBlock
572
sage: pb = PlotBlock([0,0,0],[0,1,0],[0,0,1],[0,0,.5],[1,0,0],[1,1,0],[-2,-2,0],[0,0,0])
573
sage: pb.left
574
[0, 0, 0]
575
"""
576
self.left = left
577
self.left_c = left_c
578
self.top = top
579
self.top_c = top_c
580
self.right = right
581
self.right_c = right_c
582
self.bottom = bottom
583
self.bottom_c = bottom_c
584
585