Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagelib
Path: blob/master/sage/plot/plot3d/implicit_surface.pyx
4036 views
1
r"""
2
Graphics 3D object for representing and triangulating isosurfaces.
3
4
AUTHORS:
5
6
- Robert Hanson (2007): initial Java version, in Jmol.
7
- Carl Witty (2009-01): first Cython version.
8
- Bill Cauchois (2009): improvements for inclusion into Sage.
9
"""
10
11
#*****************************************************************************
12
# Copyright (C) 2009 Carl Witty <[email protected]>
13
#
14
# Distributed under the terms of the GNU General Public License (GPL)
15
#
16
# This code is distributed in the hope that it will be useful,
17
# but WITHOUT ANY WARRANTY; without even the implied warranty of
18
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
19
# General Public License for more details.
20
#
21
# The full text of the GPL is available at:
22
#
23
# http://www.gnu.org/licenses/
24
#*****************************************************************************
25
26
# Pieces of this file are strongly based on the marching cubes
27
# implementation in Jmol located at src/org/jmol/jvxl/calc/MarchingCubes.java.
28
# The data tables are in fact directly copied from there.
29
30
#*****************************************************************************
31
# This copyright is inherited from MarchingCubes.java in the Jmol source code.
32
#
33
# * Copyright (C) 2007 Miguel, Bob, Jmol Development
34
# *
35
# * Contact: [email protected]
36
# *
37
# * This library is free software; you can redistribute it and/or
38
# * modify it under the terms of the GNU Lesser General Public
39
# * License as published by the Free Software Foundation; either
40
# * version 2.1 of the License, or (at your option) any later version.
41
# *
42
# * This library is distributed in the hope that it will be useful,
43
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
44
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
45
# * Lesser General License for more details.
46
# *
47
# * You should have received a copy of the GNU Lesser General Public
48
# * License along with this library; if not, write to the Free Software
49
# * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
50
#*****************************************************************************
51
52
53
# There's a framework in here for computing multiple isosurfaces of a
54
# single function. Currently, it's only used for a single
55
# implicit_plot3d where contour=... is given a list, but it's ready to
56
# be extended. I think the best UI would be if animate(...) and
57
# show(...) had a prepass where they went through their rendering
58
# trees, found ImplicitSurface objects with the same function,
59
# bounding box, and plot_points (other parameters, such as contours,
60
# hole, jmol_color, vertex_color, would be allowed to be different),
61
# and arranged to have them all plotted simultaneously. These
62
# prepasses have not been written yet. Combining multiple
63
# ImplicitSurface plots would be particularly advantageous for animate(...),
64
# but for a big animation, watch out for memory usage.
65
66
# If you have a reasonably simple surface (not a space-filling fractal),
67
# then if n is your resolution, we have n^3 evaluations of the main
68
# function, about n^2 evaluations of auxiliary functions (hole, gradient,
69
# vertex_color/jmol_color), and output of size about n^2.
70
71
# With this in mind, we pay particular attention to optimizing the n^3
72
# function evaluations. (But keep in mind that n may be as small as 20
73
# or so, so we shouldn't ignore the efficiency of the n^2 parts.)
74
75
# Also, we make sure not to ever allocate O(n^3) memory; we do the
76
# computation a slice at a time to avoid this. (Jmol always allocates
77
# n^3 memory when it reads the JVXL file, but that might be on a different
78
# computer; Tachyon would only allocate memory proportional to the
79
# output size.)
80
81
from cStringIO import StringIO
82
83
cimport numpy as np
84
import numpy as np
85
86
from sage.plot.plot3d.transform cimport point_c, face_c, Transformation
87
from sage.plot.plot3d.base cimport PrimitiveObject
88
from sage.plot.plot3d.base import RenderParams, default_texture
89
from sage.plot.plot3d.index_face_set cimport IndexFaceSet
90
from sage.rings.all import RDF
91
from sage.plot.misc import setup_for_eval_on_grid
92
93
include '../../ext/cdefs.pxi'
94
include '../../ext/stdsage.pxi'
95
include '../../gsl/gsl.pxi'
96
include "../../ext/python_string.pxi"
97
98
include "point_c.pxi"
99
100
# The default value for plot_points (a keyword argument to implicit_plot3d()),
101
# assumed when plot_points is set to "automatic".
102
DEFAULT_PLOT_POINTS = 40
103
104
cdef double nan = float(RDF('NaN'))
105
106
cdef inline bint marching_has_edge(double a, double b, double contour, double *f, bint *has_nan):
107
# XXX Would be nicer to use isnan(), because it's inlined.
108
# Is it portable enough?
109
if gsl_isnan(a) or gsl_isnan(b):
110
has_nan[0] = True
111
return False
112
113
has_nan[0] = False
114
115
if (a >= contour) == (b >= contour):
116
return False
117
118
f[0] = (contour - a) / (b - a)
119
return True
120
121
# Returns 0 or 1
122
cdef inline int marching_is_inside(double v, double contour):
123
return gsl_isnan(v) or v < contour
124
125
cdef void interpolate_point_c(point_c *result, double frac, point_c *inputs):
126
result[0].x = inputs[0].x + frac*(inputs[1].x - inputs[0].x)
127
result[0].y = inputs[0].y + frac*(inputs[1].y - inputs[0].y)
128
result[0].z = inputs[0].z + frac*(inputs[1].z - inputs[0].z)
129
130
cdef class VertexInfo:
131
# The point in "integer space"
132
cdef point_c pt
133
134
# The gradient at this point in "evaluation space"
135
cdef point_c gradient
136
137
# (R,G,B), so not really a point at all
138
cdef point_c color
139
140
# This point in "evaluation space"
141
cdef point_c eval_pt
142
143
cdef void update_eval_pt(self, point_c *eval_min, point_c *eval_scale):
144
"""
145
Use eval_min and eval_scale to transform self.pt into evaluation space
146
and store the result in self.eval_pt.
147
"""
148
self.eval_pt.x = eval_min[0].x + eval_scale[0].x*self.pt.x
149
self.eval_pt.y = eval_min[0].y + eval_scale[0].y*self.pt.y
150
self.eval_pt.z = eval_min[0].z + eval_scale[0].z*self.pt.z
151
152
def __repr__(self):
153
"""
154
TESTS::
155
156
sage: from sage.plot.plot3d.implicit_surface import VertexInfo
157
sage: VertexInfo()
158
<0.0, 0.0, 0.0>
159
"""
160
return '<%s, %s, %s>' % (self.pt.x, self.pt.y, self.pt.z)
161
162
cdef mk_VertexInfo(double x, double y, double z, point_c *eval_min, point_c *eval_scale):
163
cdef VertexInfo v
164
v = PY_NEW(VertexInfo)
165
v.pt.x = x
166
v.pt.y = y
167
v.pt.z = z
168
169
v.update_eval_pt(eval_min, eval_scale)
170
171
return v
172
173
cdef class MarchingCubes:
174
r"""
175
Handles marching cube rendering.
176
177
Protocol:
178
179
1. Create the class.
180
2. Call process_slice once for each X slice, from self.nx > x >= 0.
181
3. Call finish(), which returns a list of strings.
182
183
Note: Actually, only 4 slices ever exist; the caller will re-use old
184
storage.
185
"""
186
187
cdef readonly object xrange
188
cdef readonly object yrange
189
cdef readonly object zrange
190
cdef readonly double contour
191
cdef readonly int nx
192
cdef readonly int ny
193
cdef readonly int nz
194
cdef readonly Transformation transform
195
cdef readonly object region
196
cdef readonly object gradient
197
cdef readonly bint smooth
198
cdef readonly object vertex_color
199
cdef readonly object results
200
201
# We deal with three coordinate systems. We do most of our work
202
# in an integral coordinate system where x ranges from
203
# 0 <= x < self.nx, etc.; we do function evaluations where
204
# self.xrange[0] <= x <= self.xrange[1], etc.; and output
205
# is in a third coordinate system.
206
# (Note that in "integer space", function evaluations of the main
207
# function happen on integer coordinates; but function evaluations
208
# of the auxiliary functions will have one non-integer coordinate.)
209
210
# These parameters convert from integer space to function-evaluation
211
# space: eval_coord = eval_min + int_coord * eval_scale
212
cdef point_c eval_min
213
cdef point_c eval_scale
214
215
# The componentwise reciprocal of eval_scale; just used to change
216
# some divisions into multiplications
217
cdef point_c eval_scale_inv
218
219
cdef point_c out_origin, out_plus_x, out_plus_y, out_plus_z
220
221
def __init__(self, xrange, yrange, zrange, contour, plot_points,
222
transform=None, region=None, gradient=None, smooth=True, vertex_color=None):
223
"""
224
TESTS:
225
226
Marching cubes is an abstract base class, you can't do anything with it::
227
228
sage: from sage.plot.plot3d.implicit_surface import MarchingCubes
229
sage: cube_marcher = MarchingCubes((0, 1), (0, 1), (0, 1), 1, (10, 10, 10))
230
"""
231
232
self.xrange = xrange
233
self.yrange = yrange
234
self.zrange = zrange
235
self.contour = contour
236
self.nx = plot_points[0]
237
self.ny = plot_points[1]
238
self.nz = plot_points[2]
239
self.transform = transform
240
self.region = region
241
self.gradient = gradient
242
self.smooth = smooth
243
self.vertex_color = vertex_color
244
245
self.eval_min.x = xrange[0]
246
self.eval_scale.x = (xrange[1] - xrange[0]) / (self.nx - 1)
247
self.eval_min.y = yrange[0]
248
self.eval_scale.y = (yrange[1] - yrange[0]) / (self.ny - 1)
249
self.eval_min.z = zrange[0]
250
self.eval_scale.z = (zrange[1] - zrange[0]) / (self.nz - 1)
251
self.eval_scale_inv.x = 1/self.eval_scale.x
252
self.eval_scale_inv.y = 1/self.eval_scale.y
253
self.eval_scale_inv.z = 1/self.eval_scale.z
254
255
cdef point_c zero_pt, origin, plus_x, plus_y, plus_z
256
zero_pt.x = 0; zero_pt.y = 0; zero_pt.z = 0
257
origin = self.eval_min
258
plus_x = zero_pt; plus_x.x = self.eval_scale.x
259
plus_y = zero_pt; plus_y.y = self.eval_scale.y
260
plus_z = zero_pt; plus_z.z = self.eval_scale.z
261
if self.transform is not None:
262
self.transform.transform_point_c(&self.out_origin, origin)
263
self.transform.transform_point_c(&self.out_plus_x, plus_x)
264
self.transform.transform_point_c(&self.out_plus_y, plus_y)
265
self.transform.transform_point_c(&self.out_plus_z, plus_z)
266
else:
267
self.out_origin = origin
268
self.out_plus_x = plus_x
269
self.out_plus_y = plus_y
270
self.out_plus_z = plus_z
271
272
self.results = []
273
274
def finish(self):
275
"""
276
Returns the results of the marching cubes algorithm as a list. The format
277
is specific to the subclass implementing this method.
278
279
TESTS:
280
281
By default, it returns an empty list::
282
283
sage: from sage.plot.plot3d.implicit_surface import MarchingCubes
284
sage: cube_marcher = MarchingCubes((0, 1), (0, 1), (0, 1), 1, (10, 10, 10), None)
285
sage: cube_marcher.finish()
286
[]
287
"""
288
return self.results
289
290
cdef class MarchingCubesTriangles(MarchingCubes):
291
"""
292
A subclass of MarchingCubes that returns its results as a list of triangles,
293
including their vertices and normals (if smooth=True).
294
"""
295
296
cdef readonly np.ndarray x_vertices
297
cdef readonly np.ndarray y_vertices
298
cdef readonly np.ndarray z_vertices
299
300
cdef readonly np.ndarray y_vertices_swapped
301
cdef readonly np.ndarray z_vertices_swapped
302
303
cdef readonly slices
304
305
def __init__(self, *args, **kwargs):
306
"""
307
TESTS::
308
309
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
310
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 1, (10, 10, 10))
311
"""
312
313
MarchingCubes.__init__(self, *args, **kwargs)
314
315
self.x_vertices = np.empty((self.ny, self.nz), dtype=object)
316
self.y_vertices = np.empty((2, self.ny-1, self.nz), dtype=object)
317
self.z_vertices = np.empty((2, self.ny, self.nz-1), dtype=object)
318
319
# Create new arrays that share the same underlying data, but
320
# have 0 and 1 reversed for the first coordinate.
321
self.y_vertices_swapped = self.y_vertices[::-1, ...]
322
self.z_vertices_swapped = self.z_vertices[::-1, ...]
323
324
self.slices = []
325
326
def process_slice(self, unsigned int x, np.ndarray slice):
327
"""
328
Process a single slice of function evaluations at the specified x coordinate.
329
330
EXAMPLES::
331
332
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
333
sage: import numpy as np
334
sage: cube_marcher = MarchingCubesTriangles((-2, 2), (-2, 2), (-2, 2), 4, (10,)*3, smooth=False)
335
sage: f = lambda x, y, z: x^2 + y^2 + z^2
336
sage: slices = np.zeros((10, 10, 10), dtype=np.double)
337
sage: for x in reversed(xrange(0, 10)):
338
... for y in xrange(0, 10):
339
... for z in xrange(0, 10):
340
... slices[x, y, z] = f(*[a * (4 / 9) -2 for a in (x, y, z)])
341
... cube_marcher.process_slice(x, slices[x, :, :])
342
sage: faces = cube_marcher.finish()
343
sage: faces[0][0]
344
{'y': -1.1..., 'x': 1.5..., 'z': -0.5...}
345
346
We render the isosurface using IndexFaceSet::
347
348
sage: from sage.plot.plot3d.index_face_set import IndexFaceSet
349
sage: IndexFaceSet([tuple((p['x'], p['y'], p['z']) for p in face) for face in faces])
350
"""
351
# We go to a lot of effort to avoid repeating computations.
352
# (I hope that the related bookkeeping is actually faster
353
# than repeating the computations, but I haven't tested.)
354
355
# We get evaluation points, one slice at a time. But we
356
# don't want to process slices of evaluation points;
357
# we want to process slices of cubes. (There is one fewer slice
358
# of cubes than there is of evaluation points.)
359
360
# Without any caching, we would repeat this for each cube:
361
# Find which vertices are inside/outside the surface.
362
# For edges which cross the surface, interpolate to
363
# find the exact crossing location; these will be the vertices
364
# of triangles.
365
# Compute the color and surface normal for each of these vertices.
366
# Assemble the vertices into triangles, using the "marching cubes"
367
# tables.
368
369
# The problem with this is that each vertex is actually shared
370
# among four neighbor cubes (except on the edge of the array),
371
# so we would be repeating work (potentially a lot of work, if
372
# the user-specified gradient or color routines are expensive)
373
# four times.
374
375
# So we cache the information associated with each vertex.
376
# Let's call an edge an X, Y, or Z edge depending on which
377
# axis it is parallel to; and a vertex is an X, Y, or Z
378
# vertex depending on which kind of edge it lies on.
379
380
# X vertices are shared between cubes that are all within a
381
# single cube slice. However, Y and Z vertices are shared
382
# between adjacent slices, so we need to keep those caches
383
# around. But we still need only two slices of vertex cache
384
# at a time (the two that are adjacent to the current cube slice).
385
386
# To reduce memory allocations, we allocate these caches at the
387
# beginning of the run (they are ndarrays of objects). We
388
# set a VertexInfo object in the cache when we first compute it;
389
# then we look in the cache when we need it for the other three
390
# cubes.
391
392
# The X vertex cache is a 2-D ndarray. The Y and Z vertex caches
393
# are 3-D, where the first (x) dimension is indexed by 0 or 1.
394
395
# The Cython buffer interface (that we use for fast access to
396
# ndarray's) has to reinitialize itself separately in each
397
# function. For this reason, we use fewer, bigger functions;
398
# in particular, we don't call any functions per-cube that
399
# require buffer access.
400
401
# To compute interpolated gradients using central differencing,
402
# we need up to 4 slices. Consider a gradient computed at
403
# an X vertex, between slices 1 and 2. This is interpolated
404
# between the gradient at slice 1 and the gradient at slice 2.
405
# Computing the gradient at slice 1 requires slices 0 and 2;
406
# computing the gradient at slice 2 requires slices 1 and 3.
407
408
# This means we need to queue up slices and process them
409
# in a somewhat strange order. This function only does
410
# the queuing, and then passes all the real work off to
411
# other functions.
412
413
self.slices = ([slice] + self.slices)[:4]
414
415
if len(self.slices) >= 2:
416
self._update_yz_vertices(x+1,
417
self.slices[0],
418
self.slices[1],
419
self.slices[2] if len(self.slices) >= 3 else None)
420
421
if len(self.slices) >= 3:
422
self._update_x_vertices(x+1,
423
self.slices[0],
424
self.slices[1],
425
self.slices[2],
426
self.slices[3] if len(self.slices) >= 4 else None)
427
self.process_cubes(self.slices[1], self.slices[2])
428
429
if x == 0:
430
self._update_yz_vertices(x,
431
None,
432
self.slices[0],
433
self.slices[1])
434
435
self._update_x_vertices(x,
436
None,
437
self.slices[0],
438
self.slices[1],
439
self.slices[2] if len(self.slices) >= 3 else None)
440
441
self.process_cubes(self.slices[0], self.slices[1])
442
443
cpdef _update_yz_vertices(self, int x, np.ndarray _prev, np.ndarray _cur, np.ndarray _next):
444
"""
445
TESTS::
446
447
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
448
sage: import numpy as np
449
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (2,)*3, smooth=False)
450
sage: prev_slice = next_slice = np.ones((2, 2), dtype=np.double)
451
sage: cur_slice = prev_slice.copy()
452
sage: cur_slice[0, 0] = -1 # Seed the slice data with an "interesting" value.
453
sage: cube_marcher._update_yz_vertices(1, prev_slice, cur_slice, next_slice)
454
sage: cube_marcher.z_vertices.tolist()
455
[[[<1.0, 0.0, 0.5>], [None]], [[None], [None]]]
456
sage: cube_marcher.y_vertices.tolist()
457
[[[<1.0, 0.5, 0.0>, None]], [[None, None]]]
458
sage: cube_marcher.x_vertices.any() # This shouldn't affect the X vertices.
459
False
460
"""
461
(self.y_vertices, self.y_vertices_swapped) = \
462
(self.y_vertices_swapped, self.y_vertices)
463
(self.z_vertices, self.z_vertices_swapped) = \
464
(self.z_vertices_swapped, self.z_vertices)
465
466
cdef bint has_prev = (_prev is not None)
467
cdef bint has_next = (_next is not None)
468
469
cdef np.ndarray[double, ndim=2] prev = _prev
470
cdef np.ndarray[double, ndim=2] cur = _cur
471
cdef np.ndarray[double, ndim=2] next = _next
472
473
cdef np.ndarray[object, ndim=2] y_vertices = self.y_vertices[0,...]
474
cdef np.ndarray[object, ndim=2] z_vertices = self.z_vertices[0,...]
475
476
cdef int ny = self.ny
477
cdef int nz = self.nz
478
479
cdef int y
480
cdef int z
481
cdef VertexInfo v
482
cdef double frac
483
cdef bint has_nan
484
cdef point_c gradients[2]
485
cdef int i
486
for y from 0 <= y < ny - 1:
487
for z from 0 <= z < nz:
488
if marching_has_edge(cur[y,z], cur[y+1,z], self.contour, &frac, &has_nan):
489
v = mk_VertexInfo(x, y+frac, z, &self.eval_min, &self.eval_scale)
490
if self.region is not None:
491
if not self.in_region(v):
492
y_vertices[y,z] = None
493
continue
494
if self.smooth:
495
# We must compute a gradient.
496
if self.gradient is not None:
497
self.apply_point_func(&v.gradient, self.gradient, v)
498
else:
499
# Use central differencing.
500
for i from 0 <= i < 2:
501
self.get_gradient(&gradients[i],
502
x, y+i, z,
503
cur[y+i,z],
504
prev[y+i,z] if has_prev else 0,
505
next[y+i,z] if has_next else 0,
506
cur[y+i-1,z] if y+i>0 else 0,
507
cur[y+i+1,z] if y+i<ny-1 else 0,
508
cur[y+i,z-1] if z>0 else 0,
509
cur[y+i,z+1] if z<nz-1 else 0)
510
interpolate_point_c(&v.gradient, frac, gradients)
511
if self.vertex_color:
512
self.apply_point_func(&v.color, self.vertex_color, v)
513
y_vertices[y,z] = v
514
else:
515
y_vertices[y,z] = None
516
517
# OK, that updated the Y vertices. Now we do almost exactly
518
# the same thing to update Z vertices.
519
for y from 0 <= y < ny:
520
for z from 0 <= z < nz - 1:
521
if marching_has_edge(cur[y,z], cur[y,z+1], self.contour, &frac, &has_nan):
522
v = mk_VertexInfo(x, y, z+frac, &self.eval_min, &self.eval_scale)
523
if self.region is not None:
524
if not self.in_region(v):
525
z_vertices[y,z] = None
526
continue
527
if self.smooth:
528
# We must compute a gradient.
529
if self.gradient is not None:
530
self.apply_point_func(&v.gradient, self.gradient, v)
531
else:
532
# Use central differencing.
533
for i from 0 <= i < 2:
534
self.get_gradient(&gradients[i],
535
x, y, z+i,
536
cur[y,z+i],
537
prev[y,z+i] if has_prev else 0,
538
next[y,z+i] if has_next else 0,
539
cur[y-1,z+i] if y>0 else 0,
540
cur[y+1,z+i] if y<ny-1 else 0,
541
cur[y,z+i-1] if z+i>0 else 0,
542
cur[y,z+i+1] if z+i<nz-1 else 0)
543
interpolate_point_c(&v.gradient, frac, gradients)
544
if self.vertex_color:
545
self.apply_point_func(&v.color, self.vertex_color, v)
546
z_vertices[y,z] = v
547
else:
548
z_vertices[y,z] = None
549
550
cpdef _update_x_vertices(self, int x, np.ndarray _prev, np.ndarray _left, np.ndarray _right, np.ndarray _next):
551
"""
552
TESTS::
553
554
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
555
sage: import numpy as np
556
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (4, 2, 2), smooth=False)
557
sage: prev_slice = right_slice = next_slice = np.ones((2, 2), dtype=np.double)
558
sage: left_slice = prev_slice.copy()
559
sage: left_slice[1, 1] = -1
560
sage: cube_marcher._update_x_vertices(1, prev_slice, left_slice, right_slice, next_slice)
561
sage: cube_marcher.x_vertices.tolist()
562
[[None, None], [None, <1.5, 1.0, 1.0>]]
563
sage: cube_marcher.y_vertices.any() or cube_marcher.z_vertices.any() # This shouldn't affect the Y or Z vertices.
564
False
565
"""
566
cdef bint has_prev = (_prev is not None)
567
cdef bint has_next = (_next is not None)
568
569
cdef np.ndarray[double, ndim=2] prev = _prev
570
cdef np.ndarray[double, ndim=2] left = _left
571
cdef np.ndarray[double, ndim=2] right = _right
572
cdef np.ndarray[double, ndim=2] next = _next
573
574
cdef np.ndarray[object, ndim=2] x_vertices = self.x_vertices
575
576
cdef int ny = self.ny
577
cdef int nz = self.nz
578
579
cdef int y
580
cdef int z
581
582
cdef VertexInfo v
583
cdef double frac
584
cdef bint has_nan
585
cdef point_c gradients[2]
586
for y from 0 <= y < ny:
587
for z from 0 <= z < nz:
588
if marching_has_edge(left[y,z], right[y,z], self.contour, &frac, &has_nan):
589
v = mk_VertexInfo(x+frac, y, z, &self.eval_min, &self.eval_scale)
590
if self.region is not None:
591
if not self.in_region(v):
592
x_vertices[y,z] = None
593
continue
594
if self.smooth:
595
# We must compute a gradient.
596
if self.gradient is not None:
597
self.apply_point_func(&v.gradient, self.gradient, v)
598
else:
599
# Use central differencing.
600
self.get_gradient(&gradients[0],
601
x, y, z,
602
left[y,z],
603
prev[y,z] if has_prev else 0,
604
right[y,z],
605
left[y-1,z] if y>0 else 0,
606
left[y+1,z] if y<ny-1 else 0,
607
left[y,z-1] if z>0 else 0,
608
left[y,z+1] if z<nz-1 else 0)
609
self.get_gradient(&gradients[1],
610
x, y, z,
611
right[y,z],
612
left[y,z],
613
next[y,z] if has_next else 0,
614
right[y-1,z] if y>0 else 0,
615
right[y+1,z] if y<ny-1 else 0,
616
right[y,z-1] if z>0 else 0,
617
right[y,z+1] if z<nz-1 else 0)
618
interpolate_point_c(&v.gradient, frac, gradients)
619
if self.vertex_color:
620
self.apply_point_func(&v.color, self.vertex_color, v)
621
x_vertices[y,z] = v
622
else:
623
x_vertices[y,z] = None
624
625
cdef bint in_region(self, VertexInfo v):
626
return (self.region(v.eval_pt.x, v.eval_pt.y, v.eval_pt.z) > 0)
627
628
cdef apply_point_func(self, point_c *pt, fn, VertexInfo v):
629
if isinstance(fn, tuple):
630
pt[0].x = fn[0](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
631
pt[0].y = fn[1](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
632
pt[0].z = fn[2](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
633
else:
634
t = fn(v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
635
pt[0].x = t[0]
636
pt[0].y = t[1]
637
pt[0].z = t[2]
638
639
cdef get_gradient(self,
640
point_c *g,
641
int x, int y, int z,
642
double center,
643
double lx, double ux,
644
double ly, double uy,
645
double lz, double uz):
646
# What a mess! It would be much nicer-looking code to pass slices
647
# in here and do the subscripting in here. Unfortunately,
648
# that would also be much slower, because we'd have to re-initialize
649
# the Cython buffer interface on each call.
650
651
cdef double dx = ux - lx
652
cdef double dy = uy - ly
653
cdef double dz = uz - lz
654
655
cdef double gx = dx * self.eval_scale_inv.x
656
cdef double gy = dy * self.eval_scale_inv.y
657
cdef double gz = dz * self.eval_scale_inv.z
658
659
if x > 0 and x < self.nx - 1: gx *= 0.5
660
if y > 0 and y < self.ny - 1: gy *= 0.5
661
if z > 0 and z < self.nz - 1: gz *= 0.5
662
663
g[0].x = gx
664
g[0].y = gy
665
g[0].z = gz
666
667
cpdef process_cubes(self, np.ndarray _left, np.ndarray _right):
668
"""
669
TESTS::
670
671
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
672
sage: import numpy as np
673
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (3, 2, 2), smooth=False)
674
sage: slices = [np.ones((2, 2), dtype=np.double) for i in xrange(0, 3)]
675
sage: slices[0][1, 1] = -1
676
sage: cube_marcher._update_yz_vertices(0, None, slices[0], slices[1])
677
sage: cube_marcher._update_x_vertices(0, None, slices[0], slices[1], slices[2])
678
sage: cube_marcher.process_cubes(slices[0], slices[1])
679
sage: cube_marcher.finish()
680
[({'y': 1.0, 'x': 0.0, 'z': 0.5}, {'y': 1.0, 'x': 0.25, 'z': 1.0}, {'y': 0.5, 'x': 0.0, 'z': 1.0})]
681
"""
682
cdef np.ndarray[double, ndim=2] left = _left
683
cdef np.ndarray[double, ndim=2] right = _right
684
685
cdef np.ndarray[object, ndim=2] x_vertices = self.x_vertices
686
cdef np.ndarray[object, ndim=3] y_vertices = self.y_vertices
687
cdef np.ndarray[object, ndim=3] z_vertices = self.z_vertices
688
689
cdef int ny = self.ny
690
cdef int nz = self.nz
691
692
cdef int y
693
cdef int z
694
695
# based on generateSurfaceData in MarchingCubes.java
696
cdef int insideMask
697
698
# Cool ASCII art from MarchingCubes.java:
699
# * Y
700
# * 4 --------4--------- 5
701
# * /| /|
702
# * / | / |
703
# * / | / |
704
# * 7 8 5 |
705
# * / | / 9
706
# * / | / |
707
# * 7 --------6--------- 6 |
708
# * | | | |
709
# * | 0 ---------0--|----- 1 X
710
# * | / | /
711
# * 11 / 10 /
712
# * | 3 | 1
713
# * | / | /
714
# * | / | /
715
# * 3 ---------2-------- 2
716
# * Z
717
718
# We see from the above that vertices are labeled 0 to 7, and
719
# edges are labeled 0 to 11.
720
721
cdef list all_vertex_info = [None] * 12
722
cdef tuple my_triangles
723
724
cdef int i
725
726
for y from 0 <= y < ny-1:
727
for z from 0 <= z < nz-1:
728
# For each vertex (0 to 7), set the corresponding bit
729
# of insideMask iff the vertex is inside the surface.
730
insideMask = 0
731
insideMask |= marching_is_inside(left[y,z], self.contour)<<0
732
insideMask |= marching_is_inside(right[y,z], self.contour)<<1
733
insideMask |= marching_is_inside(right[y,z+1], self.contour)<<2
734
insideMask |= marching_is_inside(left[y,z+1], self.contour)<<3
735
insideMask |= marching_is_inside(left[y+1,z], self.contour)<<4
736
insideMask |= marching_is_inside(right[y+1,z], self.contour)<<5
737
insideMask |= marching_is_inside(right[y+1,z+1], self.contour)<<6
738
insideMask |= marching_is_inside(left[y+1,z+1], self.contour)<<7
739
740
if insideMask == 0 or insideMask == 255: continue
741
742
# OK, we have a cube on the surface. Copy all of the vertex
743
# info into an array for easier reference.
744
745
all_vertex_info[0] = x_vertices[y,z]
746
all_vertex_info[1] = z_vertices[1,y,z]
747
all_vertex_info[2] = x_vertices[y,z+1]
748
all_vertex_info[3] = z_vertices[0,y,z]
749
all_vertex_info[4] = x_vertices[y+1,z]
750
all_vertex_info[5] = z_vertices[1,y+1,z]
751
all_vertex_info[6] = x_vertices[y+1,z+1]
752
all_vertex_info[7] = z_vertices[0,y+1,z]
753
all_vertex_info[8] = y_vertices[0,y,z]
754
all_vertex_info[9] = y_vertices[1,y,z]
755
all_vertex_info[10]= y_vertices[1,y,z+1]
756
all_vertex_info[11]= y_vertices[0,y,z+1]
757
758
my_triangles = triangle_table2[insideMask]
759
760
for i in range(0, len(my_triangles), 4):
761
# In wireframe mode, my_triangles[i+3] specifies
762
# whether or not to draw the corresponding edge.
763
# See MarchingCubes.java for details.
764
self.add_triangle(all_vertex_info[my_triangles[i]],
765
all_vertex_info[my_triangles[i+1]],
766
all_vertex_info[my_triangles[i+2]])
767
768
cpdef add_triangle(self, VertexInfo v1, VertexInfo v2, VertexInfo v3):
769
"""
770
Called when a new triangle is generated by the marching cubes algorithm
771
to update the results array.
772
773
TESTS::
774
775
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles, VertexInfo
776
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (10,)*3, smooth=False)
777
sage: cube_marcher.add_triangle(VertexInfo(), VertexInfo(), VertexInfo())
778
sage: cube_marcher.finish()
779
[({'y': 0.0, 'x': 0.0, 'z': 0.0}, {'y': 0.0, 'x': 0.0, 'z': 0.0}, {'y': 0.0, 'x': 0.0, 'z': 0.0})]
780
"""
781
782
if v1 is None or v2 is None or v3 is None:
783
# This happens if there is a NaN nearby, or if a hole was specified here.
784
return
785
786
cdef:
787
point_c v1_ev_pt, v2_ev_pt, v3_ev_pt
788
point_c n1_ev_vec, n2_ev_vec, n3_ev_vec
789
790
if self.transform is not None:
791
self.transform.transform_point_c(&v1_ev_pt, v1.eval_pt)
792
self.transform.transform_point_c(&v2_ev_pt, v2.eval_pt)
793
self.transform.transform_point_c(&v3_ev_pt, v3.eval_pt)
794
else:
795
v1_ev_pt = v1.eval_pt
796
v2_ev_pt = v2.eval_pt
797
v3_ev_pt = v3.eval_pt
798
face = (v1_ev_pt, v2_ev_pt, v3_ev_pt)
799
800
if self.smooth:
801
# XXX I believe this is wrong for non-uniform transforms
802
if self.transform is not None:
803
self.transform.transform_vector_c(&n1_ev_vec, v1.gradient)
804
self.transform.transform_vector_c(&n2_ev_vec, v2.gradient)
805
self.transform.transform_vector_c(&n3_ev_vec, v3.gradient)
806
else:
807
n1_ev_vec = v1.gradient
808
n2_ev_vec = v2.gradient
809
n3_ev_vec = v3.gradient
810
face += (n1_ev_vec, n2_ev_vec, n3_ev_vec)
811
812
self.results.append(face)
813
814
cpdef render_implicit(f, xrange, yrange, zrange, plot_points, cube_marchers):
815
"""
816
INPUT:
817
818
- ``f`` - a (fast!) callable function
819
820
- ``xrange`` - a 2-tuple (x_min, x_max)
821
822
- ``yrange`` - a 2-tuple (y_min, y_may)
823
824
- ``zrange`` - a 2-tuple (z_min, z_maz)
825
826
- ``plot_points`` - a triple of integers indicating the number of
827
function evaluations in each direction.
828
829
- ``cube_marchers`` - a list of cube marchers, one for each contour.
830
831
832
OUTPUT:
833
834
A representation of the isosurface, in the format specified by the individual
835
cube marchers.
836
837
TESTS::
838
839
sage: from sage.plot.plot3d.implicit_surface import render_implicit, MarchingCubesTriangles
840
sage: plot_points, f = (40,)*3, lambda x, y, z: x + y + z
841
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 1, (10,)*3)
842
sage: results = render_implicit(lambda x, y, z: x + y + z, \
843
... (0, 1), (0, 1), (0, 1), (10,)*3, [cube_marcher])
844
sage: results[0][0]
845
{'y': 0.0, 'x': 1.0, 'z': 0.0}
846
"""
847
848
cdef int nx = plot_points[0]
849
cdef int ny = plot_points[1]
850
cdef int nz = plot_points[2]
851
852
cdef point_c eval_min, eval_scale
853
854
eval_min.x = xrange[0]
855
eval_scale.x = (xrange[1] - xrange[0]) / (nx - 1)
856
eval_min.y = yrange[0]
857
eval_scale.y = (yrange[1] - yrange[0]) / (ny - 1)
858
eval_min.z = zrange[0]
859
eval_scale.z = (zrange[1] - zrange[0]) / (nz - 1)
860
861
# A possible future extension would be to allow passing in "f"
862
# as a numpy ndarray. If that were done, we could slightly modify
863
# the following code to just pass slices of f to the renderers
864
# (no copying of the data would be required).
865
866
# The current marching cube renderers need only at most four slices at
867
# a time.
868
869
cdef np.ndarray[double, ndim=3] data = np.zeros((4, ny, nz), dtype=np.double)
870
cdef np.ndarray[double, ndim=2] slice
871
872
cdef unsigned int x, y, z
873
cdef int n
874
875
cdef double eval_x, eval_y, eval_z
876
877
cdef MarchingCubes marcher
878
879
for n from 0 <= n < nx:
880
x = nx-1-n
881
eval_x = eval_min.x + eval_scale.x * x
882
slice = data[n % 4, :, :]
883
for y from 0 <= y < ny:
884
eval_y = eval_min.y + eval_scale.y * y
885
for z from 0 <= z < nz:
886
eval_z = eval_min.z + eval_scale.z * z
887
slice[y, z] = f(eval_x, eval_y, eval_z)
888
889
for marcher in cube_marchers:
890
marcher.process_slice(x, slice)
891
892
results = []
893
894
for marcher in cube_marchers:
895
results.extend(marcher.finish())
896
897
return results
898
899
cdef class ImplicitSurface(IndexFaceSet):
900
cdef readonly object f
901
cdef readonly object vars
902
cdef readonly tuple xrange
903
cdef readonly tuple yrange
904
cdef readonly tuple zrange
905
cdef readonly list contours
906
cdef readonly object region
907
cdef readonly bint smooth
908
cdef readonly object gradient
909
cdef readonly object vertex_color
910
cdef readonly tuple plot_points
911
912
def __init__(self, f, xrange, yrange, zrange,
913
contour=0, plot_points="automatic",
914
region=None, smooth=True, gradient=None, vertex_color=None,
915
**kwds):
916
"""
917
TESTS::
918
919
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
920
sage: var('x,y,z')
921
(x, y, z)
922
sage: G = ImplicitSurface(x^2 + y^2 + z^2, (x,-2, 2), (y,-2, 2), (z,-2, 2), contour=4)
923
sage: show(G)
924
"""
925
IndexFaceSet.__init__(self, [], [], **kwds)
926
from sage.ext.fast_eval import fast_float
927
928
orig_f = f
929
self.f, ranges, self.vars = setup_for_eval_on_grid(f, [xrange, yrange, zrange], return_vars=True)
930
self.xrange = ranges[0][:2]
931
self.yrange = ranges[1][:2]
932
self.zrange = ranges[2][:2]
933
if isinstance(contour, (list, tuple)):
934
contours = contour
935
else:
936
contours = [contour]
937
self.contours = [float(c) for c in contours]
938
if region is not None:
939
self.region = fast_float(region, *self.vars)
940
941
# Comments from Carl Witty, who first wrote this some of this code
942
# See Trac 9483
943
# When I first wrote the code, I had the idea to create a
944
# direct-to-tachyon backend that would use vertex normals
945
# to create much nicer-looking plots with smaller numbers
946
# of plot_points, and laid the groundwork for this backend
947
# with the gradient and smooth arguments. But I abandoned the
948
# code without writing this backend (and leaving many other parts
949
# of the code unfinished).
950
# When William Cauchois took over and finished the code (thank you,
951
# William!), he only wrote an IndexFaceSet backend, that can't
952
# (currently) make use of vertex normals. So the gradient code is
953
# currently useless.
954
# But it's still open for somebody to write a direct-to-tachyon backend,
955
# or to extend IndexFaceSet to support vertex normals.
956
957
# Since IndexFaceSet doesn't even support smooth shading, we overwrite
958
# the passed-in smooth parameter.
959
smooth=False
960
961
962
self.smooth = smooth
963
if smooth and gradient is None:
964
try:
965
gradient = (orig_f.diff(self.vars[0]),
966
orig_f.diff(self.vars[1]),
967
orig_f.diff(self.vars[2]))
968
except:
969
# Would be nice to have more nuanced error handling here.
970
971
# If anything goes wrong, we'll just use central differencing.
972
pass
973
if gradient is not None:
974
self.gradient = fast_float(gradient, *self.vars)
975
if vertex_color is not None:
976
self.vertex_color = fast_float(vertex_color, *self.vars)
977
if plot_points == "automatic":
978
plot_points = DEFAULT_PLOT_POINTS
979
my_plot_points = []
980
for i in range(3):
981
if isinstance(plot_points, (list, tuple)):
982
n = int(plot_points[i])
983
else:
984
n = int(plot_points)
985
if n < 2:
986
raise ValueError
987
my_plot_points.append(n)
988
self.plot_points = tuple(my_plot_points)
989
990
def bounding_box(self):
991
"""
992
Return a bounding box for the ``ImplicitSurface``, as a tuple of two
993
3-dimensional points.
994
995
EXAMPLES:
996
997
Note that the bounding box corresponds exactly to the x-, y-, and z- range::
998
999
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1000
sage: G = ImplicitSurface(0, (0, 1), (0, 1), (0, 1))
1001
sage: G.bounding_box()
1002
((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))
1003
"""
1004
return ((self.xrange[0], self.yrange[0], self.zrange[0]),
1005
(self.xrange[1], self.yrange[1], self.zrange[1]))
1006
1007
def obj_repr(self, render_params):
1008
"""
1009
Return a representation of this object in the .obj format.
1010
1011
TESTS::
1012
1013
We graph a simple plane::
1014
1015
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1016
sage: var('x,y,z')
1017
(x, y, z)
1018
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1019
sage: obj = G.obj_repr(G.default_render_params())
1020
sage: vertices = obj[2]
1021
1022
The number of vertices in the OBJ representation should equal the number
1023
of vertices in the face set::
1024
1025
sage: len(vertices) == len(G.vertex_list())
1026
True
1027
1028
The vertices in the OBJ representation should also be approximately equal
1029
to the vertices in the face set -- the small error is due to rounding
1030
which occurs during output (we test only the first 20 points for the
1031
sake of speed)::
1032
1033
sage: def points_equal(a, b, epsilon=(1e-5)):
1034
... return all(abs(x0-x1) < epsilon for x0, x1 in zip(a, b))
1035
sage: list = []
1036
sage: assert len(vertices) >= 20 # I should hope so, we're rendering at the default resolution!
1037
sage: for vertex, surf_vertex in zip(vertices, G.vertex_list())[0:20]:
1038
... list.append(points_equal(map(float, vertex.split(' ')[1:]), surf_vertex))
1039
sage: all(list)
1040
True
1041
"""
1042
self.triangulate()
1043
return IndexFaceSet.obj_repr(self, render_params)
1044
1045
def tachyon_repr(self, render_params):
1046
"""
1047
Return a representation of this object suitable for use with the Tachyon
1048
renderer.
1049
1050
TESTS::
1051
1052
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1053
sage: var('x,y,z')
1054
(x, y, z)
1055
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1056
sage: G.tachyon_repr(G.default_render_params())[0].startswith('TRI')
1057
True
1058
"""
1059
self.triangulate()
1060
return IndexFaceSet.tachyon_repr(self, render_params)
1061
1062
def jmol_repr(self, render_params):
1063
"""
1064
Return a representation of this object suitable for use with the Jmol
1065
renderer.
1066
1067
TESTS::
1068
1069
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1070
sage: var('x,y,z')
1071
(x, y, z)
1072
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1073
sage: show(G, viewer='jmol')
1074
"""
1075
self.triangulate()
1076
return IndexFaceSet.jmol_repr(self, render_params)
1077
1078
def json_repr(self, render_params):
1079
"""
1080
Return a representation of this object in JavaScript Object Notation (JSON).
1081
1082
TESTS::
1083
1084
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1085
sage: var('x,y,z')
1086
(x, y, z)
1087
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1088
sage: G.json_repr(G.default_render_params())[0].startswith('{vertices:')
1089
True
1090
"""
1091
self.triangulate()
1092
return IndexFaceSet.json_repr(self, render_params)
1093
1094
def triangulate(self, force=False):
1095
"""
1096
The IndexFaceSet will be empty until you call this method, which generates
1097
the faces and vertices according to the parameters specified in the
1098
constructor for ImplicitSurface. Note that if you call this method more
1099
than once, subsequent invocations will have no effect (this is an
1100
optimization to avoid repeated work) unless you specify force=True in the
1101
keywords.
1102
1103
EXAMPLES::
1104
1105
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1106
sage: var('x,y,z')
1107
(x, y, z)
1108
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1109
sage: len(G.vertex_list()), len(G.face_list())
1110
(0, 0)
1111
sage: G.triangulate()
1112
sage: len(G.vertex_list()) > 0, len(G.face_list()) > 0
1113
(True, True)
1114
sage: G.show() # This should be fast, since the mesh is already triangulated.
1115
"""
1116
if self.fcount != 0 and not force:
1117
# The mesh is already triangulated
1118
return
1119
1120
options = dict(xrange=self.xrange, yrange=self.yrange, zrange=self.zrange,
1121
region=self.region, smooth=self.smooth,
1122
gradient=self.gradient,
1123
vertex_color=self.vertex_color,
1124
plot_points=self.plot_points)
1125
cube_marchers = [MarchingCubesTriangles(contour=x, **options) for x in self.contours]
1126
results = render_implicit(self.f, self.xrange, self.yrange, self.zrange,
1127
self.plot_points, cube_marchers)
1128
cdef:
1129
face_c* dest_face
1130
point_c* dest_vertex
1131
int fcount = len(results)
1132
1133
self.realloc(fcount * 3, fcount, fcount * 3)
1134
for i from 0 <= i < fcount:
1135
dest_face = &self._faces[i]
1136
src_face = results[i]
1137
1138
dest_face.n = 3
1139
dest_face.vertices = &self.face_indices[3 * i]
1140
1141
for j from 0 <= j < 3:
1142
dest_face.vertices[j] = (3 * i) + j
1143
dest_vertex = &self.vs[(3 * i) + j]
1144
dest_vertex.x = src_face[j]['x']
1145
dest_vertex.y = src_face[j]['y']
1146
dest_vertex.z = src_face[j]['z']
1147
1148
self.vcount = fcount * 3
1149
self.fcount = fcount
1150
self.icount = fcount * 3
1151
1152
# Data table (courtesy of MarchingCubes.java)
1153
triangle_table2 = ( None,
1154
( 0, 8, 3, 7 ),
1155
( 0, 1, 9, 7 ), ( 1, 8, 3, 6, 9, 8, 1, 5 ), ( 1, 2, 10, 7 ),
1156
( 0, 8, 3, 7, 1, 2, 10, 7 ), ( 9, 2, 10, 6, 0, 2, 9, 5 ),
1157
( 2, 8, 3, 6, 2, 10, 8, 1, 10, 9, 8, 3 ), ( 3, 11, 2, 7 ),
1158
( 0, 11, 2, 6, 8, 11, 0, 5 ), ( 1, 9, 0, 7, 2, 3, 11, 7 ),
1159
( 1, 11, 2, 6, 1, 9, 11, 1, 9, 8, 11, 3 ), ( 3, 10, 1, 6, 11, 10, 3, 5 ),
1160
( 0, 10, 1, 6, 0, 8, 10, 1, 8, 11, 10, 3 ),
1161
( 3, 9, 0, 6, 3, 11, 9, 1, 11, 10, 9, 3 ), ( 9, 8, 10, 5, 10, 8, 11, 6 ),
1162
( 4, 7, 8, 7 ), ( 4, 3, 0, 6, 7, 3, 4, 5 ), ( 0, 1, 9, 7, 8, 4, 7, 7 ),
1163
( 4, 1, 9, 6, 4, 7, 1, 1, 7, 3, 1, 3 ), ( 1, 2, 10, 7, 8, 4, 7, 7 ),
1164
( 3, 4, 7, 6, 3, 0, 4, 3, 1, 2, 10, 7 ),
1165
( 9, 2, 10, 6, 9, 0, 2, 3, 8, 4, 7, 7 ),
1166
( 2, 10, 9, 3, 2, 9, 7, 0, 2, 7, 3, 6, 7, 9, 4, 6 ),
1167
( 8, 4, 7, 7, 3, 11, 2, 7 ), ( 11, 4, 7, 6, 11, 2, 4, 1, 2, 0, 4, 3 ),
1168
( 9, 0, 1, 7, 8, 4, 7, 7, 2, 3, 11, 7 ),
1169
( 4, 7, 11, 3, 9, 4, 11, 1, 9, 11, 2, 2, 9, 2, 1, 6 ),
1170
( 3, 10, 1, 6, 3, 11, 10, 3, 7, 8, 4, 7 ),
1171
( 1, 11, 10, 6, 1, 4, 11, 0, 1, 0, 4, 3, 7, 11, 4, 5 ),
1172
( 4, 7, 8, 7, 9, 0, 11, 1, 9, 11, 10, 6, 11, 0, 3, 6 ),
1173
( 4, 7, 11, 3, 4, 11, 9, 4, 9, 11, 10, 6 ), ( 9, 5, 4, 7 ),
1174
( 9, 5, 4, 7, 0, 8, 3, 7 ), ( 0, 5, 4, 6, 1, 5, 0, 5 ),
1175
( 8, 5, 4, 6, 8, 3, 5, 1, 3, 1, 5, 3 ), ( 1, 2, 10, 7, 9, 5, 4, 7 ),
1176
( 3, 0, 8, 7, 1, 2, 10, 7, 4, 9, 5, 7 ),
1177
( 5, 2, 10, 6, 5, 4, 2, 1, 4, 0, 2, 3 ),
1178
( 2, 10, 5, 3, 3, 2, 5, 1, 3, 5, 4, 2, 3, 4, 8, 6 ),
1179
( 9, 5, 4, 7, 2, 3, 11, 7 ), ( 0, 11, 2, 6, 0, 8, 11, 3, 4, 9, 5, 7 ),
1180
( 0, 5, 4, 6, 0, 1, 5, 3, 2, 3, 11, 7 ),
1181
( 2, 1, 5, 3, 2, 5, 8, 0, 2, 8, 11, 6, 4, 8, 5, 5 ),
1182
( 10, 3, 11, 6, 10, 1, 3, 3, 9, 5, 4, 7 ),
1183
( 4, 9, 5, 7, 0, 8, 1, 5, 8, 10, 1, 2, 8, 11, 10, 3 ),
1184
( 5, 4, 0, 3, 5, 0, 11, 0, 5, 11, 10, 6, 11, 0, 3, 6 ),
1185
( 5, 4, 8, 3, 5, 8, 10, 4, 10, 8, 11, 6 ), ( 9, 7, 8, 6, 5, 7, 9, 5 ),
1186
( 9, 3, 0, 6, 9, 5, 3, 1, 5, 7, 3, 3 ),
1187
( 0, 7, 8, 6, 0, 1, 7, 1, 1, 5, 7, 3 ), ( 1, 5, 3, 5, 3, 5, 7, 6 ),
1188
( 9, 7, 8, 6, 9, 5, 7, 3, 10, 1, 2, 7 ),
1189
( 10, 1, 2, 7, 9, 5, 0, 5, 5, 3, 0, 2, 5, 7, 3, 3 ),
1190
( 8, 0, 2, 3, 8, 2, 5, 0, 8, 5, 7, 6, 10, 5, 2, 5 ),
1191
( 2, 10, 5, 3, 2, 5, 3, 4, 3, 5, 7, 6 ),
1192
( 7, 9, 5, 6, 7, 8, 9, 3, 3, 11, 2, 7 ),
1193
( 9, 5, 7, 3, 9, 7, 2, 0, 9, 2, 0, 6, 2, 7, 11, 6 ),
1194
( 2, 3, 11, 7, 0, 1, 8, 5, 1, 7, 8, 2, 1, 5, 7, 3 ),
1195
( 11, 2, 1, 3, 11, 1, 7, 4, 7, 1, 5, 6 ),
1196
( 9, 5, 8, 5, 8, 5, 7, 6, 10, 1, 3, 3, 10, 3, 11, 6 ),
1197
( 5, 7, 0, 1, 5, 0, 9, 6, 7, 11, 0, 1, 1, 0, 10, 5, 11, 10, 0, 1 ),
1198
( 11, 10, 0, 1, 11, 0, 3, 6, 10, 5, 0, 1, 8, 0, 7, 5, 5, 7, 0, 1 ),
1199
( 11, 10, 5, 3, 7, 11, 5, 5 ), ( 10, 6, 5, 7 ),
1200
( 0, 8, 3, 7, 5, 10, 6, 7 ), ( 9, 0, 1, 7, 5, 10, 6, 7 ),
1201
( 1, 8, 3, 6, 1, 9, 8, 3, 5, 10, 6, 7 ), ( 1, 6, 5, 6, 2, 6, 1, 5 ),
1202
( 1, 6, 5, 6, 1, 2, 6, 3, 3, 0, 8, 7 ),
1203
( 9, 6, 5, 6, 9, 0, 6, 1, 0, 2, 6, 3 ),
1204
( 5, 9, 8, 3, 5, 8, 2, 0, 5, 2, 6, 6, 3, 2, 8, 5 ),
1205
( 2, 3, 11, 7, 10, 6, 5, 7 ), ( 11, 0, 8, 6, 11, 2, 0, 3, 10, 6, 5, 7 ),
1206
( 0, 1, 9, 7, 2, 3, 11, 7, 5, 10, 6, 7 ),
1207
( 5, 10, 6, 7, 1, 9, 2, 5, 9, 11, 2, 2, 9, 8, 11, 3 ),
1208
( 6, 3, 11, 6, 6, 5, 3, 1, 5, 1, 3, 3 ),
1209
( 0, 8, 11, 3, 0, 11, 5, 0, 0, 5, 1, 6, 5, 11, 6, 6 ),
1210
( 3, 11, 6, 3, 0, 3, 6, 1, 0, 6, 5, 2, 0, 5, 9, 6 ),
1211
( 6, 5, 9, 3, 6, 9, 11, 4, 11, 9, 8, 6 ), ( 5, 10, 6, 7, 4, 7, 8, 7 ),
1212
( 4, 3, 0, 6, 4, 7, 3, 3, 6, 5, 10, 7 ),
1213
( 1, 9, 0, 7, 5, 10, 6, 7, 8, 4, 7, 7 ),
1214
( 10, 6, 5, 7, 1, 9, 7, 1, 1, 7, 3, 6, 7, 9, 4, 6 ),
1215
( 6, 1, 2, 6, 6, 5, 1, 3, 4, 7, 8, 7 ),
1216
( 1, 2, 5, 5, 5, 2, 6, 6, 3, 0, 4, 3, 3, 4, 7, 6 ),
1217
( 8, 4, 7, 7, 9, 0, 5, 5, 0, 6, 5, 2, 0, 2, 6, 3 ),
1218
( 7, 3, 9, 1, 7, 9, 4, 6, 3, 2, 9, 1, 5, 9, 6, 5, 2, 6, 9, 1 ),
1219
( 3, 11, 2, 7, 7, 8, 4, 7, 10, 6, 5, 7 ),
1220
( 5, 10, 6, 7, 4, 7, 2, 1, 4, 2, 0, 6, 2, 7, 11, 6 ),
1221
( 0, 1, 9, 7, 4, 7, 8, 7, 2, 3, 11, 7, 5, 10, 6, 7 ),
1222
( 9, 2, 1, 6, 9, 11, 2, 2, 9, 4, 11, 1, 7, 11, 4, 5, 5, 10, 6, 7 ),
1223
( 8, 4, 7, 7, 3, 11, 5, 1, 3, 5, 1, 6, 5, 11, 6, 6 ),
1224
( 5, 1, 11, 1, 5, 11, 6, 6, 1, 0, 11, 1, 7, 11, 4, 5, 0, 4, 11, 1 ),
1225
( 0, 5, 9, 6, 0, 6, 5, 2, 0, 3, 6, 1, 11, 6, 3, 5, 8, 4, 7, 7 ),
1226
( 6, 5, 9, 3, 6, 9, 11, 4, 4, 7, 9, 5, 7, 11, 9, 1 ),
1227
( 10, 4, 9, 6, 6, 4, 10, 5 ), ( 4, 10, 6, 6, 4, 9, 10, 3, 0, 8, 3, 7 ),
1228
( 10, 0, 1, 6, 10, 6, 0, 1, 6, 4, 0, 3 ),
1229
( 8, 3, 1, 3, 8, 1, 6, 0, 8, 6, 4, 6, 6, 1, 10, 6 ),
1230
( 1, 4, 9, 6, 1, 2, 4, 1, 2, 6, 4, 3 ),
1231
( 3, 0, 8, 7, 1, 2, 9, 5, 2, 4, 9, 2, 2, 6, 4, 3 ),
1232
( 0, 2, 4, 5, 4, 2, 6, 6 ), ( 8, 3, 2, 3, 8, 2, 4, 4, 4, 2, 6, 6 ),
1233
( 10, 4, 9, 6, 10, 6, 4, 3, 11, 2, 3, 7 ),
1234
( 0, 8, 2, 5, 2, 8, 11, 6, 4, 9, 10, 3, 4, 10, 6, 6 ),
1235
( 3, 11, 2, 7, 0, 1, 6, 1, 0, 6, 4, 6, 6, 1, 10, 6 ),
1236
( 6, 4, 1, 1, 6, 1, 10, 6, 4, 8, 1, 1, 2, 1, 11, 5, 8, 11, 1, 1 ),
1237
( 9, 6, 4, 6, 9, 3, 6, 0, 9, 1, 3, 3, 11, 6, 3, 5 ),
1238
( 8, 11, 1, 1, 8, 1, 0, 6, 11, 6, 1, 1, 9, 1, 4, 5, 6, 4, 1, 1 ),
1239
( 3, 11, 6, 3, 3, 6, 0, 4, 0, 6, 4, 6 ), ( 6, 4, 8, 3, 11, 6, 8, 5 ),
1240
( 7, 10, 6, 6, 7, 8, 10, 1, 8, 9, 10, 3 ),
1241
( 0, 7, 3, 6, 0, 10, 7, 0, 0, 9, 10, 3, 6, 7, 10, 5 ),
1242
( 10, 6, 7, 3, 1, 10, 7, 1, 1, 7, 8, 2, 1, 8, 0, 6 ),
1243
( 10, 6, 7, 3, 10, 7, 1, 4, 1, 7, 3, 6 ),
1244
( 1, 2, 6, 3, 1, 6, 8, 0, 1, 8, 9, 6, 8, 6, 7, 6 ),
1245
( 2, 6, 9, 1, 2, 9, 1, 6, 6, 7, 9, 1, 0, 9, 3, 5, 7, 3, 9, 1 ),
1246
( 7, 8, 0, 3, 7, 0, 6, 4, 6, 0, 2, 6 ), ( 7, 3, 2, 3, 6, 7, 2, 5 ),
1247
( 2, 3, 11, 7, 10, 6, 8, 1, 10, 8, 9, 6, 8, 6, 7, 6 ),
1248
( 2, 0, 7, 1, 2, 7, 11, 6, 0, 9, 7, 1, 6, 7, 10, 5, 9, 10, 7, 1 ),
1249
( 1, 8, 0, 6, 1, 7, 8, 2, 1, 10, 7, 1, 6, 7, 10, 5, 2, 3, 11, 7 ),
1250
( 11, 2, 1, 3, 11, 1, 7, 4, 10, 6, 1, 5, 6, 7, 1, 1 ),
1251
( 8, 9, 6, 1, 8, 6, 7, 6, 9, 1, 6, 1, 11, 6, 3, 5, 1, 3, 6, 1 ),
1252
( 0, 9, 1, 7, 11, 6, 7, 7 ),
1253
( 7, 8, 0, 3, 7, 0, 6, 4, 3, 11, 0, 5, 11, 6, 0, 1 ), ( 7, 11, 6, 7 ),
1254
( 7, 6, 11, 7 ), ( 3, 0, 8, 7, 11, 7, 6, 7 ),
1255
( 0, 1, 9, 7, 11, 7, 6, 7 ), ( 8, 1, 9, 6, 8, 3, 1, 3, 11, 7, 6, 7 ),
1256
( 10, 1, 2, 7, 6, 11, 7, 7 ), ( 1, 2, 10, 7, 3, 0, 8, 7, 6, 11, 7, 7 ),
1257
( 2, 9, 0, 6, 2, 10, 9, 3, 6, 11, 7, 7 ),
1258
( 6, 11, 7, 7, 2, 10, 3, 5, 10, 8, 3, 2, 10, 9, 8, 3 ),
1259
( 7, 2, 3, 6, 6, 2, 7, 5 ), ( 7, 0, 8, 6, 7, 6, 0, 1, 6, 2, 0, 3 ),
1260
( 2, 7, 6, 6, 2, 3, 7, 3, 0, 1, 9, 7 ),
1261
( 1, 6, 2, 6, 1, 8, 6, 0, 1, 9, 8, 3, 8, 7, 6, 3 ),
1262
( 10, 7, 6, 6, 10, 1, 7, 1, 1, 3, 7, 3 ),
1263
( 10, 7, 6, 6, 1, 7, 10, 4, 1, 8, 7, 2, 1, 0, 8, 3 ),
1264
( 0, 3, 7, 3, 0, 7, 10, 0, 0, 10, 9, 6, 6, 10, 7, 5 ),
1265
( 7, 6, 10, 3, 7, 10, 8, 4, 8, 10, 9, 6 ), ( 6, 8, 4, 6, 11, 8, 6, 5 ),
1266
( 3, 6, 11, 6, 3, 0, 6, 1, 0, 4, 6, 3 ),
1267
( 8, 6, 11, 6, 8, 4, 6, 3, 9, 0, 1, 7 ),
1268
( 9, 4, 6, 3, 9, 6, 3, 0, 9, 3, 1, 6, 11, 3, 6, 5 ),
1269
( 6, 8, 4, 6, 6, 11, 8, 3, 2, 10, 1, 7 ),
1270
( 1, 2, 10, 7, 3, 0, 11, 5, 0, 6, 11, 2, 0, 4, 6, 3 ),
1271
( 4, 11, 8, 6, 4, 6, 11, 3, 0, 2, 9, 5, 2, 10, 9, 3 ),
1272
( 10, 9, 3, 1, 10, 3, 2, 6, 9, 4, 3, 1, 11, 3, 6, 5, 4, 6, 3, 1 ),
1273
( 8, 2, 3, 6, 8, 4, 2, 1, 4, 6, 2, 3 ), ( 0, 4, 2, 5, 4, 6, 2, 3 ),
1274
( 1, 9, 0, 7, 2, 3, 4, 1, 2, 4, 6, 6, 4, 3, 8, 6 ),
1275
( 1, 9, 4, 3, 1, 4, 2, 4, 2, 4, 6, 6 ),
1276
( 8, 1, 3, 6, 8, 6, 1, 0, 8, 4, 6, 3, 6, 10, 1, 3 ),
1277
( 10, 1, 0, 3, 10, 0, 6, 4, 6, 0, 4, 6 ),
1278
( 4, 6, 3, 1, 4, 3, 8, 6, 6, 10, 3, 1, 0, 3, 9, 5, 10, 9, 3, 1 ),
1279
( 10, 9, 4, 3, 6, 10, 4, 5 ), ( 4, 9, 5, 7, 7, 6, 11, 7 ),
1280
( 0, 8, 3, 7, 4, 9, 5, 7, 11, 7, 6, 7 ),
1281
( 5, 0, 1, 6, 5, 4, 0, 3, 7, 6, 11, 7 ),
1282
( 11, 7, 6, 7, 8, 3, 4, 5, 3, 5, 4, 2, 3, 1, 5, 3 ),
1283
( 9, 5, 4, 7, 10, 1, 2, 7, 7, 6, 11, 7 ),
1284
( 6, 11, 7, 7, 1, 2, 10, 7, 0, 8, 3, 7, 4, 9, 5, 7 ),
1285
( 7, 6, 11, 7, 5, 4, 10, 5, 4, 2, 10, 2, 4, 0, 2, 3 ),
1286
( 3, 4, 8, 6, 3, 5, 4, 2, 3, 2, 5, 1, 10, 5, 2, 5, 11, 7, 6, 7 ),
1287
( 7, 2, 3, 6, 7, 6, 2, 3, 5, 4, 9, 7 ),
1288
( 9, 5, 4, 7, 0, 8, 6, 1, 0, 6, 2, 6, 6, 8, 7, 6 ),
1289
( 3, 6, 2, 6, 3, 7, 6, 3, 1, 5, 0, 5, 5, 4, 0, 3 ),
1290
( 6, 2, 8, 1, 6, 8, 7, 6, 2, 1, 8, 1, 4, 8, 5, 5, 1, 5, 8, 1 ),
1291
( 9, 5, 4, 7, 10, 1, 6, 5, 1, 7, 6, 2, 1, 3, 7, 3 ),
1292
( 1, 6, 10, 6, 1, 7, 6, 2, 1, 0, 7, 1, 8, 7, 0, 5, 9, 5, 4, 7 ),
1293
( 4, 0, 10, 1, 4, 10, 5, 6, 0, 3, 10, 1, 6, 10, 7, 5, 3, 7, 10, 1 ),
1294
( 7, 6, 10, 3, 7, 10, 8, 4, 5, 4, 10, 5, 4, 8, 10, 1 ),
1295
( 6, 9, 5, 6, 6, 11, 9, 1, 11, 8, 9, 3 ),
1296
( 3, 6, 11, 6, 0, 6, 3, 4, 0, 5, 6, 2, 0, 9, 5, 3 ),
1297
( 0, 11, 8, 6, 0, 5, 11, 0, 0, 1, 5, 3, 5, 6, 11, 3 ),
1298
( 6, 11, 3, 3, 6, 3, 5, 4, 5, 3, 1, 6 ),
1299
( 1, 2, 10, 7, 9, 5, 11, 1, 9, 11, 8, 6, 11, 5, 6, 6 ),
1300
( 0, 11, 3, 6, 0, 6, 11, 2, 0, 9, 6, 1, 5, 6, 9, 5, 1, 2, 10, 7 ),
1301
( 11, 8, 5, 1, 11, 5, 6, 6, 8, 0, 5, 1, 10, 5, 2, 5, 0, 2, 5, 1 ),
1302
( 6, 11, 3, 3, 6, 3, 5, 4, 2, 10, 3, 5, 10, 5, 3, 1 ),
1303
( 5, 8, 9, 6, 5, 2, 8, 0, 5, 6, 2, 3, 3, 8, 2, 5 ),
1304
( 9, 5, 6, 3, 9, 6, 0, 4, 0, 6, 2, 6 ),
1305
( 1, 5, 8, 1, 1, 8, 0, 6, 5, 6, 8, 1, 3, 8, 2, 5, 6, 2, 8, 1 ),
1306
( 1, 5, 6, 3, 2, 1, 6, 5 ),
1307
( 1, 3, 6, 1, 1, 6, 10, 6, 3, 8, 6, 1, 5, 6, 9, 5, 8, 9, 6, 1 ),
1308
( 10, 1, 0, 3, 10, 0, 6, 4, 9, 5, 0, 5, 5, 6, 0, 1 ),
1309
( 0, 3, 8, 7, 5, 6, 10, 7 ), ( 10, 5, 6, 7 ),
1310
( 11, 5, 10, 6, 7, 5, 11, 5 ), ( 11, 5, 10, 6, 11, 7, 5, 3, 8, 3, 0, 7 ),
1311
( 5, 11, 7, 6, 5, 10, 11, 3, 1, 9, 0, 7 ),
1312
( 10, 7, 5, 6, 10, 11, 7, 3, 9, 8, 1, 5, 8, 3, 1, 3 ),
1313
( 11, 1, 2, 6, 11, 7, 1, 1, 7, 5, 1, 3 ),
1314
( 0, 8, 3, 7, 1, 2, 7, 1, 1, 7, 5, 6, 7, 2, 11, 6 ),
1315
( 9, 7, 5, 6, 9, 2, 7, 0, 9, 0, 2, 3, 2, 11, 7, 3 ),
1316
( 7, 5, 2, 1, 7, 2, 11, 6, 5, 9, 2, 1, 3, 2, 8, 5, 9, 8, 2, 1 ),
1317
( 2, 5, 10, 6, 2, 3, 5, 1, 3, 7, 5, 3 ),
1318
( 8, 2, 0, 6, 8, 5, 2, 0, 8, 7, 5, 3, 10, 2, 5, 5 ),
1319
( 9, 0, 1, 7, 5, 10, 3, 1, 5, 3, 7, 6, 3, 10, 2, 6 ),
1320
( 9, 8, 2, 1, 9, 2, 1, 6, 8, 7, 2, 1, 10, 2, 5, 5, 7, 5, 2, 1 ),
1321
( 1, 3, 5, 5, 3, 7, 5, 3 ), ( 0, 8, 7, 3, 0, 7, 1, 4, 1, 7, 5, 6 ),
1322
( 9, 0, 3, 3, 9, 3, 5, 4, 5, 3, 7, 6 ), ( 9, 8, 7, 3, 5, 9, 7, 5 ),
1323
( 5, 8, 4, 6, 5, 10, 8, 1, 10, 11, 8, 3 ),
1324
( 5, 0, 4, 6, 5, 11, 0, 0, 5, 10, 11, 3, 11, 3, 0, 3 ),
1325
( 0, 1, 9, 7, 8, 4, 10, 1, 8, 10, 11, 6, 10, 4, 5, 6 ),
1326
( 10, 11, 4, 1, 10, 4, 5, 6, 11, 3, 4, 1, 9, 4, 1, 5, 3, 1, 4, 1 ),
1327
( 2, 5, 1, 6, 2, 8, 5, 0, 2, 11, 8, 3, 4, 5, 8, 5 ),
1328
( 0, 4, 11, 1, 0, 11, 3, 6, 4, 5, 11, 1, 2, 11, 1, 5, 5, 1, 11, 1 ),
1329
( 0, 2, 5, 1, 0, 5, 9, 6, 2, 11, 5, 1, 4, 5, 8, 5, 11, 8, 5, 1 ),
1330
( 9, 4, 5, 7, 2, 11, 3, 7 ),
1331
( 2, 5, 10, 6, 3, 5, 2, 4, 3, 4, 5, 2, 3, 8, 4, 3 ),
1332
( 5, 10, 2, 3, 5, 2, 4, 4, 4, 2, 0, 6 ),
1333
( 3, 10, 2, 6, 3, 5, 10, 2, 3, 8, 5, 1, 4, 5, 8, 5, 0, 1, 9, 7 ),
1334
( 5, 10, 2, 3, 5, 2, 4, 4, 1, 9, 2, 5, 9, 4, 2, 1 ),
1335
( 8, 4, 5, 3, 8, 5, 3, 4, 3, 5, 1, 6 ), ( 0, 4, 5, 3, 1, 0, 5, 5 ),
1336
( 8, 4, 5, 3, 8, 5, 3, 4, 9, 0, 5, 5, 0, 3, 5, 1 ), ( 9, 4, 5, 7 ),
1337
( 4, 11, 7, 6, 4, 9, 11, 1, 9, 10, 11, 3 ),
1338
( 0, 8, 3, 7, 4, 9, 7, 5, 9, 11, 7, 2, 9, 10, 11, 3 ),
1339
( 1, 10, 11, 3, 1, 11, 4, 0, 1, 4, 0, 6, 7, 4, 11, 5 ),
1340
( 3, 1, 4, 1, 3, 4, 8, 6, 1, 10, 4, 1, 7, 4, 11, 5, 10, 11, 4, 1 ),
1341
( 4, 11, 7, 6, 9, 11, 4, 4, 9, 2, 11, 2, 9, 1, 2, 3 ),
1342
( 9, 7, 4, 6, 9, 11, 7, 2, 9, 1, 11, 1, 2, 11, 1, 5, 0, 8, 3, 7 ),
1343
( 11, 7, 4, 3, 11, 4, 2, 4, 2, 4, 0, 6 ),
1344
( 11, 7, 4, 3, 11, 4, 2, 4, 8, 3, 4, 5, 3, 2, 4, 1 ),
1345
( 2, 9, 10, 6, 2, 7, 9, 0, 2, 3, 7, 3, 7, 4, 9, 3 ),
1346
( 9, 10, 7, 1, 9, 7, 4, 6, 10, 2, 7, 1, 8, 7, 0, 5, 2, 0, 7, 1 ),
1347
( 3, 7, 10, 1, 3, 10, 2, 6, 7, 4, 10, 1, 1, 10, 0, 5, 4, 0, 10, 1 ),
1348
( 1, 10, 2, 7, 8, 7, 4, 7 ), ( 4, 9, 1, 3, 4, 1, 7, 4, 7, 1, 3, 6 ),
1349
( 4, 9, 1, 3, 4, 1, 7, 4, 0, 8, 1, 5, 8, 7, 1, 1 ),
1350
( 4, 0, 3, 3, 7, 4, 3, 5 ), ( 4, 8, 7, 7 ),
1351
( 9, 10, 8, 5, 10, 11, 8, 3 ), ( 3, 0, 9, 3, 3, 9, 11, 4, 11, 9, 10, 6 ),
1352
( 0, 1, 10, 3, 0, 10, 8, 4, 8, 10, 11, 6 ),
1353
( 3, 1, 10, 3, 11, 3, 10, 5 ), ( 1, 2, 11, 3, 1, 11, 9, 4, 9, 11, 8, 6 ),
1354
( 3, 0, 9, 3, 3, 9, 11, 4, 1, 2, 9, 5, 2, 11, 9, 1 ),
1355
( 0, 2, 11, 3, 8, 0, 11, 5 ), ( 3, 2, 11, 7 ),
1356
( 2, 3, 8, 3, 2, 8, 10, 4, 10, 8, 9, 6 ), ( 9, 10, 2, 3, 0, 9, 2, 5 ),
1357
( 2, 3, 8, 3, 2, 8, 10, 4, 0, 1, 8, 5, 1, 10, 8, 1 ), ( 1, 10, 2, 7 ),
1358
( 1, 3, 8, 3, 9, 1, 8, 5 ), ( 0, 9, 1, 7 ), ( 0, 3, 8, 7 ), None )
1359
1360