Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemath
GitHub Repository: sagemath/sagesmc
Path: blob/master/src/sage/plot/plot3d/implicit_surface.pyx
8815 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 'sage/ext/cdefs.pxi'
94
include 'sage/ext/stdsage.pxi'
95
include 'sage/gsl/gsl.pxi'
96
from cpython.string cimport *
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
"""
460
(self.y_vertices, self.y_vertices_swapped) = \
461
(self.y_vertices_swapped, self.y_vertices)
462
(self.z_vertices, self.z_vertices_swapped) = \
463
(self.z_vertices_swapped, self.z_vertices)
464
465
cdef bint has_prev = (_prev is not None)
466
cdef bint has_next = (_next is not None)
467
468
cdef np.ndarray[double, ndim=2] prev = _prev
469
cdef np.ndarray[double, ndim=2] cur = _cur
470
cdef np.ndarray[double, ndim=2] next = _next
471
472
cdef np.ndarray[object, ndim=2] y_vertices = self.y_vertices[0,...]
473
cdef np.ndarray[object, ndim=2] z_vertices = self.z_vertices[0,...]
474
475
cdef int ny = self.ny
476
cdef int nz = self.nz
477
478
cdef int y
479
cdef int z
480
cdef VertexInfo v
481
cdef double frac
482
cdef bint has_nan
483
cdef point_c gradients[2]
484
cdef int i
485
for y from 0 <= y < ny - 1:
486
for z from 0 <= z < nz:
487
if marching_has_edge(cur[y,z], cur[y+1,z], self.contour, &frac, &has_nan):
488
v = mk_VertexInfo(x, y+frac, z, &self.eval_min, &self.eval_scale)
489
if self.region is not None:
490
if not self.in_region(v):
491
y_vertices[y,z] = None
492
continue
493
if self.smooth:
494
# We must compute a gradient.
495
if self.gradient is not None:
496
self.apply_point_func(&v.gradient, self.gradient, v)
497
else:
498
# Use central differencing.
499
for i from 0 <= i < 2:
500
self.get_gradient(&gradients[i],
501
x, y+i, z,
502
cur[y+i,z],
503
prev[y+i,z] if has_prev else 0,
504
next[y+i,z] if has_next else 0,
505
cur[y+i-1,z] if y+i>0 else 0,
506
cur[y+i+1,z] if y+i<ny-1 else 0,
507
cur[y+i,z-1] if z>0 else 0,
508
cur[y+i,z+1] if z<nz-1 else 0)
509
interpolate_point_c(&v.gradient, frac, gradients)
510
if self.vertex_color:
511
self.apply_point_func(&v.color, self.vertex_color, v)
512
y_vertices[y,z] = v
513
else:
514
y_vertices[y,z] = None
515
516
# OK, that updated the Y vertices. Now we do almost exactly
517
# the same thing to update Z vertices.
518
for y from 0 <= y < ny:
519
for z from 0 <= z < nz - 1:
520
if marching_has_edge(cur[y,z], cur[y,z+1], self.contour, &frac, &has_nan):
521
v = mk_VertexInfo(x, y, z+frac, &self.eval_min, &self.eval_scale)
522
if self.region is not None:
523
if not self.in_region(v):
524
z_vertices[y,z] = None
525
continue
526
if self.smooth:
527
# We must compute a gradient.
528
if self.gradient is not None:
529
self.apply_point_func(&v.gradient, self.gradient, v)
530
else:
531
# Use central differencing.
532
for i from 0 <= i < 2:
533
self.get_gradient(&gradients[i],
534
x, y, z+i,
535
cur[y,z+i],
536
prev[y,z+i] if has_prev else 0,
537
next[y,z+i] if has_next else 0,
538
cur[y-1,z+i] if y>0 else 0,
539
cur[y+1,z+i] if y<ny-1 else 0,
540
cur[y,z+i-1] if z+i>0 else 0,
541
cur[y,z+i+1] if z+i<nz-1 else 0)
542
interpolate_point_c(&v.gradient, frac, gradients)
543
if self.vertex_color:
544
self.apply_point_func(&v.color, self.vertex_color, v)
545
z_vertices[y,z] = v
546
else:
547
z_vertices[y,z] = None
548
549
cpdef _update_x_vertices(self, int x, np.ndarray _prev, np.ndarray _left, np.ndarray _right, np.ndarray _next):
550
"""
551
TESTS::
552
553
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
554
sage: import numpy as np
555
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (4, 2, 2), smooth=False)
556
sage: prev_slice = right_slice = next_slice = np.ones((2, 2), dtype=np.double)
557
sage: left_slice = prev_slice.copy()
558
sage: left_slice[1, 1] = -1
559
sage: cube_marcher._update_x_vertices(1, prev_slice, left_slice, right_slice, next_slice)
560
sage: cube_marcher.x_vertices.tolist()
561
[[None, None], [None, <1.5, 1.0, 1.0>]]
562
sage: cube_marcher.y_vertices.any() or cube_marcher.z_vertices.any() # This shouldn't affect the Y or Z vertices.
563
"""
564
cdef bint has_prev = (_prev is not None)
565
cdef bint has_next = (_next is not None)
566
567
cdef np.ndarray[double, ndim=2] prev = _prev
568
cdef np.ndarray[double, ndim=2] left = _left
569
cdef np.ndarray[double, ndim=2] right = _right
570
cdef np.ndarray[double, ndim=2] next = _next
571
572
cdef np.ndarray[object, ndim=2] x_vertices = self.x_vertices
573
574
cdef int ny = self.ny
575
cdef int nz = self.nz
576
577
cdef int y
578
cdef int z
579
580
cdef VertexInfo v
581
cdef double frac
582
cdef bint has_nan
583
cdef point_c gradients[2]
584
for y from 0 <= y < ny:
585
for z from 0 <= z < nz:
586
if marching_has_edge(left[y,z], right[y,z], self.contour, &frac, &has_nan):
587
v = mk_VertexInfo(x+frac, y, z, &self.eval_min, &self.eval_scale)
588
if self.region is not None:
589
if not self.in_region(v):
590
x_vertices[y,z] = None
591
continue
592
if self.smooth:
593
# We must compute a gradient.
594
if self.gradient is not None:
595
self.apply_point_func(&v.gradient, self.gradient, v)
596
else:
597
# Use central differencing.
598
self.get_gradient(&gradients[0],
599
x, y, z,
600
left[y,z],
601
prev[y,z] if has_prev else 0,
602
right[y,z],
603
left[y-1,z] if y>0 else 0,
604
left[y+1,z] if y<ny-1 else 0,
605
left[y,z-1] if z>0 else 0,
606
left[y,z+1] if z<nz-1 else 0)
607
self.get_gradient(&gradients[1],
608
x, y, z,
609
right[y,z],
610
left[y,z],
611
next[y,z] if has_next else 0,
612
right[y-1,z] if y>0 else 0,
613
right[y+1,z] if y<ny-1 else 0,
614
right[y,z-1] if z>0 else 0,
615
right[y,z+1] if z<nz-1 else 0)
616
interpolate_point_c(&v.gradient, frac, gradients)
617
if self.vertex_color:
618
self.apply_point_func(&v.color, self.vertex_color, v)
619
x_vertices[y,z] = v
620
else:
621
x_vertices[y,z] = None
622
623
cdef bint in_region(self, VertexInfo v):
624
return (self.region(v.eval_pt.x, v.eval_pt.y, v.eval_pt.z) > 0)
625
626
cdef apply_point_func(self, point_c *pt, fn, VertexInfo v):
627
if isinstance(fn, tuple):
628
pt[0].x = fn[0](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
629
pt[0].y = fn[1](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
630
pt[0].z = fn[2](v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
631
else:
632
t = fn(v.eval_pt.x, v.eval_pt.y, v.eval_pt.z)
633
pt[0].x = t[0]
634
pt[0].y = t[1]
635
pt[0].z = t[2]
636
637
cdef get_gradient(self,
638
point_c *g,
639
int x, int y, int z,
640
double center,
641
double lx, double ux,
642
double ly, double uy,
643
double lz, double uz):
644
# What a mess! It would be much nicer-looking code to pass slices
645
# in here and do the subscripting in here. Unfortunately,
646
# that would also be much slower, because we'd have to re-initialize
647
# the Cython buffer interface on each call.
648
649
cdef double dx = ux - lx
650
cdef double dy = uy - ly
651
cdef double dz = uz - lz
652
653
cdef double gx = dx * self.eval_scale_inv.x
654
cdef double gy = dy * self.eval_scale_inv.y
655
cdef double gz = dz * self.eval_scale_inv.z
656
657
if x > 0 and x < self.nx - 1: gx *= 0.5
658
if y > 0 and y < self.ny - 1: gy *= 0.5
659
if z > 0 and z < self.nz - 1: gz *= 0.5
660
661
g[0].x = gx
662
g[0].y = gy
663
g[0].z = gz
664
665
cpdef process_cubes(self, np.ndarray _left, np.ndarray _right):
666
"""
667
TESTS::
668
669
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles
670
sage: import numpy as np
671
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (3, 2, 2), smooth=False)
672
sage: slices = [np.ones((2, 2), dtype=np.double) for i in xrange(0, 3)]
673
sage: slices[0][1, 1] = -1
674
sage: cube_marcher._update_yz_vertices(0, None, slices[0], slices[1])
675
sage: cube_marcher._update_x_vertices(0, None, slices[0], slices[1], slices[2])
676
sage: cube_marcher.process_cubes(slices[0], slices[1])
677
sage: cube_marcher.finish()
678
[({'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})]
679
"""
680
cdef np.ndarray[double, ndim=2] left = _left
681
cdef np.ndarray[double, ndim=2] right = _right
682
683
cdef np.ndarray[object, ndim=2] x_vertices = self.x_vertices
684
cdef np.ndarray[object, ndim=3] y_vertices = self.y_vertices
685
cdef np.ndarray[object, ndim=3] z_vertices = self.z_vertices
686
687
cdef int ny = self.ny
688
cdef int nz = self.nz
689
690
cdef int y
691
cdef int z
692
693
# based on generateSurfaceData in MarchingCubes.java
694
cdef int insideMask
695
696
# Cool ASCII art from MarchingCubes.java:
697
# * Y
698
# * 4 --------4--------- 5
699
# * /| /|
700
# * / | / |
701
# * / | / |
702
# * 7 8 5 |
703
# * / | / 9
704
# * / | / |
705
# * 7 --------6--------- 6 |
706
# * | | | |
707
# * | 0 ---------0--|----- 1 X
708
# * | / | /
709
# * 11 / 10 /
710
# * | 3 | 1
711
# * | / | /
712
# * | / | /
713
# * 3 ---------2-------- 2
714
# * Z
715
716
# We see from the above that vertices are labeled 0 to 7, and
717
# edges are labeled 0 to 11.
718
719
cdef list all_vertex_info = [None] * 12
720
cdef tuple my_triangles
721
722
cdef int i
723
724
for y from 0 <= y < ny-1:
725
for z from 0 <= z < nz-1:
726
# For each vertex (0 to 7), set the corresponding bit
727
# of insideMask iff the vertex is inside the surface.
728
insideMask = 0
729
insideMask |= marching_is_inside(left[y,z], self.contour)<<0
730
insideMask |= marching_is_inside(right[y,z], self.contour)<<1
731
insideMask |= marching_is_inside(right[y,z+1], self.contour)<<2
732
insideMask |= marching_is_inside(left[y,z+1], self.contour)<<3
733
insideMask |= marching_is_inside(left[y+1,z], self.contour)<<4
734
insideMask |= marching_is_inside(right[y+1,z], self.contour)<<5
735
insideMask |= marching_is_inside(right[y+1,z+1], self.contour)<<6
736
insideMask |= marching_is_inside(left[y+1,z+1], self.contour)<<7
737
738
if insideMask == 0 or insideMask == 255: continue
739
740
# OK, we have a cube on the surface. Copy all of the vertex
741
# info into an array for easier reference.
742
743
all_vertex_info[0] = x_vertices[y,z]
744
all_vertex_info[1] = z_vertices[1,y,z]
745
all_vertex_info[2] = x_vertices[y,z+1]
746
all_vertex_info[3] = z_vertices[0,y,z]
747
all_vertex_info[4] = x_vertices[y+1,z]
748
all_vertex_info[5] = z_vertices[1,y+1,z]
749
all_vertex_info[6] = x_vertices[y+1,z+1]
750
all_vertex_info[7] = z_vertices[0,y+1,z]
751
all_vertex_info[8] = y_vertices[0,y,z]
752
all_vertex_info[9] = y_vertices[1,y,z]
753
all_vertex_info[10]= y_vertices[1,y,z+1]
754
all_vertex_info[11]= y_vertices[0,y,z+1]
755
756
my_triangles = triangle_table2[insideMask]
757
758
for i in range(0, len(my_triangles), 4):
759
# In wireframe mode, my_triangles[i+3] specifies
760
# whether or not to draw the corresponding edge.
761
# See MarchingCubes.java for details.
762
self.add_triangle(all_vertex_info[my_triangles[i]],
763
all_vertex_info[my_triangles[i+1]],
764
all_vertex_info[my_triangles[i+2]])
765
766
cpdef add_triangle(self, VertexInfo v1, VertexInfo v2, VertexInfo v3):
767
"""
768
Called when a new triangle is generated by the marching cubes algorithm
769
to update the results array.
770
771
TESTS::
772
773
sage: from sage.plot.plot3d.implicit_surface import MarchingCubesTriangles, VertexInfo
774
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 0, (10,)*3, smooth=False)
775
sage: cube_marcher.add_triangle(VertexInfo(), VertexInfo(), VertexInfo())
776
sage: cube_marcher.finish()
777
[({'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})]
778
"""
779
780
if v1 is None or v2 is None or v3 is None:
781
# This happens if there is a NaN nearby, or if a hole was specified here.
782
return
783
784
cdef:
785
point_c v1_ev_pt, v2_ev_pt, v3_ev_pt
786
point_c n1_ev_vec, n2_ev_vec, n3_ev_vec
787
788
if self.transform is not None:
789
self.transform.transform_point_c(&v1_ev_pt, v1.eval_pt)
790
self.transform.transform_point_c(&v2_ev_pt, v2.eval_pt)
791
self.transform.transform_point_c(&v3_ev_pt, v3.eval_pt)
792
else:
793
v1_ev_pt = v1.eval_pt
794
v2_ev_pt = v2.eval_pt
795
v3_ev_pt = v3.eval_pt
796
face = (v1_ev_pt, v2_ev_pt, v3_ev_pt)
797
798
if self.smooth:
799
# XXX I believe this is wrong for non-uniform transforms
800
if self.transform is not None:
801
self.transform.transform_vector_c(&n1_ev_vec, v1.gradient)
802
self.transform.transform_vector_c(&n2_ev_vec, v2.gradient)
803
self.transform.transform_vector_c(&n3_ev_vec, v3.gradient)
804
else:
805
n1_ev_vec = v1.gradient
806
n2_ev_vec = v2.gradient
807
n3_ev_vec = v3.gradient
808
face += (n1_ev_vec, n2_ev_vec, n3_ev_vec)
809
810
self.results.append(face)
811
812
cpdef render_implicit(f, xrange, yrange, zrange, plot_points, cube_marchers):
813
"""
814
INPUT:
815
816
- ``f`` - a (fast!) callable function
817
818
- ``xrange`` - a 2-tuple (x_min, x_max)
819
820
- ``yrange`` - a 2-tuple (y_min, y_may)
821
822
- ``zrange`` - a 2-tuple (z_min, z_maz)
823
824
- ``plot_points`` - a triple of integers indicating the number of
825
function evaluations in each direction.
826
827
- ``cube_marchers`` - a list of cube marchers, one for each contour.
828
829
830
OUTPUT:
831
832
A representation of the isosurface, in the format specified by the individual
833
cube marchers.
834
835
TESTS::
836
837
sage: from sage.plot.plot3d.implicit_surface import render_implicit, MarchingCubesTriangles
838
sage: plot_points, f = (40,)*3, lambda x, y, z: x + y + z
839
sage: cube_marcher = MarchingCubesTriangles((0, 1), (0, 1), (0, 1), 1, (10,)*3)
840
sage: results = render_implicit(lambda x, y, z: x + y + z, \
841
... (0, 1), (0, 1), (0, 1), (10,)*3, [cube_marcher])
842
sage: results[0][0]
843
{'y': 0.0, 'x': 1.0, 'z': 0.0}
844
"""
845
846
cdef int nx = plot_points[0]
847
cdef int ny = plot_points[1]
848
cdef int nz = plot_points[2]
849
850
cdef point_c eval_min, eval_scale
851
852
eval_min.x = xrange[0]
853
eval_scale.x = (xrange[1] - xrange[0]) / (nx - 1)
854
eval_min.y = yrange[0]
855
eval_scale.y = (yrange[1] - yrange[0]) / (ny - 1)
856
eval_min.z = zrange[0]
857
eval_scale.z = (zrange[1] - zrange[0]) / (nz - 1)
858
859
# A possible future extension would be to allow passing in "f"
860
# as a numpy ndarray. If that were done, we could slightly modify
861
# the following code to just pass slices of f to the renderers
862
# (no copying of the data would be required).
863
864
# The current marching cube renderers need only at most four slices at
865
# a time.
866
867
cdef np.ndarray[double, ndim=3] data = np.zeros((4, ny, nz), dtype=np.double)
868
cdef np.ndarray[double, ndim=2] slice
869
870
cdef unsigned int x, y, z
871
cdef int n
872
873
cdef double eval_x, eval_y, eval_z
874
875
cdef MarchingCubes marcher
876
877
for n from 0 <= n < nx:
878
x = nx-1-n
879
eval_x = eval_min.x + eval_scale.x * x
880
slice = data[n % 4, :, :]
881
for y from 0 <= y < ny:
882
eval_y = eval_min.y + eval_scale.y * y
883
for z from 0 <= z < nz:
884
eval_z = eval_min.z + eval_scale.z * z
885
slice[y, z] = f(eval_x, eval_y, eval_z)
886
887
for marcher in cube_marchers:
888
marcher.process_slice(x, slice)
889
890
results = []
891
892
for marcher in cube_marchers:
893
results.extend(marcher.finish())
894
895
return results
896
897
cdef class ImplicitSurface(IndexFaceSet):
898
cdef readonly object f
899
cdef readonly object vars
900
cdef readonly tuple xrange
901
cdef readonly tuple yrange
902
cdef readonly tuple zrange
903
cdef readonly list contours
904
cdef readonly object region
905
cdef readonly bint smooth
906
cdef readonly object gradient
907
cdef readonly object vertex_color
908
cdef readonly tuple plot_points
909
910
def __init__(self, f, xrange, yrange, zrange,
911
contour=0, plot_points="automatic",
912
region=None, smooth=True, gradient=None, vertex_color=None,
913
**kwds):
914
"""
915
TESTS::
916
917
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
918
sage: var('x,y,z')
919
(x, y, z)
920
sage: G = ImplicitSurface(x^2 + y^2 + z^2, (x,-2, 2), (y,-2, 2), (z,-2, 2), contour=4)
921
sage: show(G)
922
"""
923
IndexFaceSet.__init__(self, [], [], **kwds)
924
from sage.ext.fast_eval import fast_float
925
926
orig_f = f
927
self.f, ranges, self.vars = setup_for_eval_on_grid(f, [xrange, yrange, zrange], return_vars=True)
928
self.xrange = ranges[0][:2]
929
self.yrange = ranges[1][:2]
930
self.zrange = ranges[2][:2]
931
if isinstance(contour, (list, tuple)):
932
contours = contour
933
else:
934
contours = [contour]
935
self.contours = [float(c) for c in contours]
936
if region is not None:
937
self.region = fast_float(region, *self.vars)
938
939
# Comments from Carl Witty, who first wrote this some of this code
940
# See Trac 9483
941
# When I first wrote the code, I had the idea to create a
942
# direct-to-tachyon backend that would use vertex normals
943
# to create much nicer-looking plots with smaller numbers
944
# of plot_points, and laid the groundwork for this backend
945
# with the gradient and smooth arguments. But I abandoned the
946
# code without writing this backend (and leaving many other parts
947
# of the code unfinished).
948
# When William Cauchois took over and finished the code (thank you,
949
# William!), he only wrote an IndexFaceSet backend, that can't
950
# (currently) make use of vertex normals. So the gradient code is
951
# currently useless.
952
# But it's still open for somebody to write a direct-to-tachyon backend,
953
# or to extend IndexFaceSet to support vertex normals.
954
955
# Since IndexFaceSet doesn't even support smooth shading, we overwrite
956
# the passed-in smooth parameter.
957
smooth=False
958
959
960
self.smooth = smooth
961
if smooth and gradient is None:
962
try:
963
gradient = (orig_f.diff(self.vars[0]),
964
orig_f.diff(self.vars[1]),
965
orig_f.diff(self.vars[2]))
966
except Exception:
967
# Would be nice to have more nuanced error handling here.
968
969
# If anything goes wrong, we'll just use central differencing.
970
pass
971
if gradient is not None:
972
self.gradient = fast_float(gradient, *self.vars)
973
if vertex_color is not None:
974
self.vertex_color = fast_float(vertex_color, *self.vars)
975
if plot_points == "automatic":
976
plot_points = DEFAULT_PLOT_POINTS
977
my_plot_points = []
978
for i in range(3):
979
if isinstance(plot_points, (list, tuple)):
980
n = int(plot_points[i])
981
else:
982
n = int(plot_points)
983
if n < 2:
984
raise ValueError
985
my_plot_points.append(n)
986
self.plot_points = tuple(my_plot_points)
987
988
def bounding_box(self):
989
"""
990
Return a bounding box for the ``ImplicitSurface``, as a tuple of two
991
3-dimensional points.
992
993
EXAMPLES:
994
995
Note that the bounding box corresponds exactly to the x-, y-, and z- range::
996
997
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
998
sage: G = ImplicitSurface(0, (0, 1), (0, 1), (0, 1))
999
sage: G.bounding_box()
1000
((0.0, 0.0, 0.0), (1.0, 1.0, 1.0))
1001
"""
1002
return ((self.xrange[0], self.yrange[0], self.zrange[0]),
1003
(self.xrange[1], self.yrange[1], self.zrange[1]))
1004
1005
def obj_repr(self, render_params):
1006
"""
1007
Return a representation of this object in the .obj format.
1008
1009
TESTS::
1010
1011
We graph a simple plane::
1012
1013
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1014
sage: var('x,y,z')
1015
(x, y, z)
1016
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1017
sage: obj = G.obj_repr(G.default_render_params())
1018
sage: vertices = obj[2]
1019
1020
The number of vertices in the OBJ representation should equal the number
1021
of vertices in the face set::
1022
1023
sage: len(vertices) == len(G.vertex_list())
1024
True
1025
1026
The vertices in the OBJ representation should also be approximately equal
1027
to the vertices in the face set -- the small error is due to rounding
1028
which occurs during output (we test only the first 20 points for the
1029
sake of speed)::
1030
1031
sage: def points_equal(a, b, epsilon=(1e-5)):
1032
... return all(abs(x0-x1) < epsilon for x0, x1 in zip(a, b))
1033
sage: list = []
1034
sage: assert len(vertices) >= 20 # I should hope so, we're rendering at the default resolution!
1035
sage: for vertex, surf_vertex in zip(vertices, G.vertex_list())[0:20]:
1036
... list.append(points_equal(map(float, vertex.split(' ')[1:]), surf_vertex))
1037
sage: all(list)
1038
True
1039
"""
1040
self.triangulate()
1041
return IndexFaceSet.obj_repr(self, render_params)
1042
1043
def tachyon_repr(self, render_params):
1044
"""
1045
Return a representation of this object suitable for use with the Tachyon
1046
renderer.
1047
1048
TESTS::
1049
1050
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1051
sage: var('x,y,z')
1052
(x, y, z)
1053
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1054
sage: G.tachyon_repr(G.default_render_params())[0].startswith('TRI')
1055
True
1056
"""
1057
self.triangulate()
1058
return IndexFaceSet.tachyon_repr(self, render_params)
1059
1060
def jmol_repr(self, render_params):
1061
"""
1062
Return a representation of this object suitable for use with the Jmol
1063
renderer.
1064
1065
TESTS::
1066
1067
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1068
sage: var('x,y,z')
1069
(x, y, z)
1070
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1071
sage: show(G, viewer='jmol')
1072
"""
1073
self.triangulate()
1074
return IndexFaceSet.jmol_repr(self, render_params)
1075
1076
def json_repr(self, render_params):
1077
"""
1078
Return a representation of this object in JavaScript Object Notation (JSON).
1079
1080
TESTS::
1081
1082
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1083
sage: var('x,y,z')
1084
(x, y, z)
1085
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1086
sage: G.json_repr(G.default_render_params())[0].startswith('{vertices:')
1087
True
1088
"""
1089
self.triangulate()
1090
return IndexFaceSet.json_repr(self, render_params)
1091
1092
def triangulate(self, force=False):
1093
"""
1094
The IndexFaceSet will be empty until you call this method, which generates
1095
the faces and vertices according to the parameters specified in the
1096
constructor for ImplicitSurface. Note that if you call this method more
1097
than once, subsequent invocations will have no effect (this is an
1098
optimization to avoid repeated work) unless you specify force=True in the
1099
keywords.
1100
1101
EXAMPLES::
1102
1103
sage: from sage.plot.plot3d.implicit_surface import ImplicitSurface
1104
sage: var('x,y,z')
1105
(x, y, z)
1106
sage: G = ImplicitSurface(x + y + z, (x,-1, 1), (y,-1, 1), (z,-1, 1))
1107
sage: len(G.vertex_list()), len(G.face_list())
1108
(0, 0)
1109
sage: G.triangulate()
1110
sage: len(G.vertex_list()) > 0, len(G.face_list()) > 0
1111
(True, True)
1112
sage: G.show() # This should be fast, since the mesh is already triangulated.
1113
"""
1114
if self.fcount != 0 and not force:
1115
# The mesh is already triangulated
1116
return
1117
1118
options = dict(xrange=self.xrange, yrange=self.yrange, zrange=self.zrange,
1119
region=self.region, smooth=self.smooth,
1120
gradient=self.gradient,
1121
vertex_color=self.vertex_color,
1122
plot_points=self.plot_points)
1123
cube_marchers = [MarchingCubesTriangles(contour=x, **options) for x in self.contours]
1124
results = render_implicit(self.f, self.xrange, self.yrange, self.zrange,
1125
self.plot_points, cube_marchers)
1126
cdef:
1127
face_c* dest_face
1128
point_c* dest_vertex
1129
int fcount = len(results)
1130
1131
self.realloc(fcount * 3, fcount, fcount * 3)
1132
for i from 0 <= i < fcount:
1133
dest_face = &self._faces[i]
1134
src_face = results[i]
1135
1136
dest_face.n = 3
1137
dest_face.vertices = &self.face_indices[3 * i]
1138
1139
for j from 0 <= j < 3:
1140
dest_face.vertices[j] = (3 * i) + j
1141
dest_vertex = &self.vs[(3 * i) + j]
1142
dest_vertex.x = src_face[j]['x']
1143
dest_vertex.y = src_face[j]['y']
1144
dest_vertex.z = src_face[j]['z']
1145
1146
self.vcount = fcount * 3
1147
self.fcount = fcount
1148
self.icount = fcount * 3
1149
1150
# Data table (courtesy of MarchingCubes.java)
1151
triangle_table2 = ( None,
1152
( 0, 8, 3, 7 ),
1153
( 0, 1, 9, 7 ), ( 1, 8, 3, 6, 9, 8, 1, 5 ), ( 1, 2, 10, 7 ),
1154
( 0, 8, 3, 7, 1, 2, 10, 7 ), ( 9, 2, 10, 6, 0, 2, 9, 5 ),
1155
( 2, 8, 3, 6, 2, 10, 8, 1, 10, 9, 8, 3 ), ( 3, 11, 2, 7 ),
1156
( 0, 11, 2, 6, 8, 11, 0, 5 ), ( 1, 9, 0, 7, 2, 3, 11, 7 ),
1157
( 1, 11, 2, 6, 1, 9, 11, 1, 9, 8, 11, 3 ), ( 3, 10, 1, 6, 11, 10, 3, 5 ),
1158
( 0, 10, 1, 6, 0, 8, 10, 1, 8, 11, 10, 3 ),
1159
( 3, 9, 0, 6, 3, 11, 9, 1, 11, 10, 9, 3 ), ( 9, 8, 10, 5, 10, 8, 11, 6 ),
1160
( 4, 7, 8, 7 ), ( 4, 3, 0, 6, 7, 3, 4, 5 ), ( 0, 1, 9, 7, 8, 4, 7, 7 ),
1161
( 4, 1, 9, 6, 4, 7, 1, 1, 7, 3, 1, 3 ), ( 1, 2, 10, 7, 8, 4, 7, 7 ),
1162
( 3, 4, 7, 6, 3, 0, 4, 3, 1, 2, 10, 7 ),
1163
( 9, 2, 10, 6, 9, 0, 2, 3, 8, 4, 7, 7 ),
1164
( 2, 10, 9, 3, 2, 9, 7, 0, 2, 7, 3, 6, 7, 9, 4, 6 ),
1165
( 8, 4, 7, 7, 3, 11, 2, 7 ), ( 11, 4, 7, 6, 11, 2, 4, 1, 2, 0, 4, 3 ),
1166
( 9, 0, 1, 7, 8, 4, 7, 7, 2, 3, 11, 7 ),
1167
( 4, 7, 11, 3, 9, 4, 11, 1, 9, 11, 2, 2, 9, 2, 1, 6 ),
1168
( 3, 10, 1, 6, 3, 11, 10, 3, 7, 8, 4, 7 ),
1169
( 1, 11, 10, 6, 1, 4, 11, 0, 1, 0, 4, 3, 7, 11, 4, 5 ),
1170
( 4, 7, 8, 7, 9, 0, 11, 1, 9, 11, 10, 6, 11, 0, 3, 6 ),
1171
( 4, 7, 11, 3, 4, 11, 9, 4, 9, 11, 10, 6 ), ( 9, 5, 4, 7 ),
1172
( 9, 5, 4, 7, 0, 8, 3, 7 ), ( 0, 5, 4, 6, 1, 5, 0, 5 ),
1173
( 8, 5, 4, 6, 8, 3, 5, 1, 3, 1, 5, 3 ), ( 1, 2, 10, 7, 9, 5, 4, 7 ),
1174
( 3, 0, 8, 7, 1, 2, 10, 7, 4, 9, 5, 7 ),
1175
( 5, 2, 10, 6, 5, 4, 2, 1, 4, 0, 2, 3 ),
1176
( 2, 10, 5, 3, 3, 2, 5, 1, 3, 5, 4, 2, 3, 4, 8, 6 ),
1177
( 9, 5, 4, 7, 2, 3, 11, 7 ), ( 0, 11, 2, 6, 0, 8, 11, 3, 4, 9, 5, 7 ),
1178
( 0, 5, 4, 6, 0, 1, 5, 3, 2, 3, 11, 7 ),
1179
( 2, 1, 5, 3, 2, 5, 8, 0, 2, 8, 11, 6, 4, 8, 5, 5 ),
1180
( 10, 3, 11, 6, 10, 1, 3, 3, 9, 5, 4, 7 ),
1181
( 4, 9, 5, 7, 0, 8, 1, 5, 8, 10, 1, 2, 8, 11, 10, 3 ),
1182
( 5, 4, 0, 3, 5, 0, 11, 0, 5, 11, 10, 6, 11, 0, 3, 6 ),
1183
( 5, 4, 8, 3, 5, 8, 10, 4, 10, 8, 11, 6 ), ( 9, 7, 8, 6, 5, 7, 9, 5 ),
1184
( 9, 3, 0, 6, 9, 5, 3, 1, 5, 7, 3, 3 ),
1185
( 0, 7, 8, 6, 0, 1, 7, 1, 1, 5, 7, 3 ), ( 1, 5, 3, 5, 3, 5, 7, 6 ),
1186
( 9, 7, 8, 6, 9, 5, 7, 3, 10, 1, 2, 7 ),
1187
( 10, 1, 2, 7, 9, 5, 0, 5, 5, 3, 0, 2, 5, 7, 3, 3 ),
1188
( 8, 0, 2, 3, 8, 2, 5, 0, 8, 5, 7, 6, 10, 5, 2, 5 ),
1189
( 2, 10, 5, 3, 2, 5, 3, 4, 3, 5, 7, 6 ),
1190
( 7, 9, 5, 6, 7, 8, 9, 3, 3, 11, 2, 7 ),
1191
( 9, 5, 7, 3, 9, 7, 2, 0, 9, 2, 0, 6, 2, 7, 11, 6 ),
1192
( 2, 3, 11, 7, 0, 1, 8, 5, 1, 7, 8, 2, 1, 5, 7, 3 ),
1193
( 11, 2, 1, 3, 11, 1, 7, 4, 7, 1, 5, 6 ),
1194
( 9, 5, 8, 5, 8, 5, 7, 6, 10, 1, 3, 3, 10, 3, 11, 6 ),
1195
( 5, 7, 0, 1, 5, 0, 9, 6, 7, 11, 0, 1, 1, 0, 10, 5, 11, 10, 0, 1 ),
1196
( 11, 10, 0, 1, 11, 0, 3, 6, 10, 5, 0, 1, 8, 0, 7, 5, 5, 7, 0, 1 ),
1197
( 11, 10, 5, 3, 7, 11, 5, 5 ), ( 10, 6, 5, 7 ),
1198
( 0, 8, 3, 7, 5, 10, 6, 7 ), ( 9, 0, 1, 7, 5, 10, 6, 7 ),
1199
( 1, 8, 3, 6, 1, 9, 8, 3, 5, 10, 6, 7 ), ( 1, 6, 5, 6, 2, 6, 1, 5 ),
1200
( 1, 6, 5, 6, 1, 2, 6, 3, 3, 0, 8, 7 ),
1201
( 9, 6, 5, 6, 9, 0, 6, 1, 0, 2, 6, 3 ),
1202
( 5, 9, 8, 3, 5, 8, 2, 0, 5, 2, 6, 6, 3, 2, 8, 5 ),
1203
( 2, 3, 11, 7, 10, 6, 5, 7 ), ( 11, 0, 8, 6, 11, 2, 0, 3, 10, 6, 5, 7 ),
1204
( 0, 1, 9, 7, 2, 3, 11, 7, 5, 10, 6, 7 ),
1205
( 5, 10, 6, 7, 1, 9, 2, 5, 9, 11, 2, 2, 9, 8, 11, 3 ),
1206
( 6, 3, 11, 6, 6, 5, 3, 1, 5, 1, 3, 3 ),
1207
( 0, 8, 11, 3, 0, 11, 5, 0, 0, 5, 1, 6, 5, 11, 6, 6 ),
1208
( 3, 11, 6, 3, 0, 3, 6, 1, 0, 6, 5, 2, 0, 5, 9, 6 ),
1209
( 6, 5, 9, 3, 6, 9, 11, 4, 11, 9, 8, 6 ), ( 5, 10, 6, 7, 4, 7, 8, 7 ),
1210
( 4, 3, 0, 6, 4, 7, 3, 3, 6, 5, 10, 7 ),
1211
( 1, 9, 0, 7, 5, 10, 6, 7, 8, 4, 7, 7 ),
1212
( 10, 6, 5, 7, 1, 9, 7, 1, 1, 7, 3, 6, 7, 9, 4, 6 ),
1213
( 6, 1, 2, 6, 6, 5, 1, 3, 4, 7, 8, 7 ),
1214
( 1, 2, 5, 5, 5, 2, 6, 6, 3, 0, 4, 3, 3, 4, 7, 6 ),
1215
( 8, 4, 7, 7, 9, 0, 5, 5, 0, 6, 5, 2, 0, 2, 6, 3 ),
1216
( 7, 3, 9, 1, 7, 9, 4, 6, 3, 2, 9, 1, 5, 9, 6, 5, 2, 6, 9, 1 ),
1217
( 3, 11, 2, 7, 7, 8, 4, 7, 10, 6, 5, 7 ),
1218
( 5, 10, 6, 7, 4, 7, 2, 1, 4, 2, 0, 6, 2, 7, 11, 6 ),
1219
( 0, 1, 9, 7, 4, 7, 8, 7, 2, 3, 11, 7, 5, 10, 6, 7 ),
1220
( 9, 2, 1, 6, 9, 11, 2, 2, 9, 4, 11, 1, 7, 11, 4, 5, 5, 10, 6, 7 ),
1221
( 8, 4, 7, 7, 3, 11, 5, 1, 3, 5, 1, 6, 5, 11, 6, 6 ),
1222
( 5, 1, 11, 1, 5, 11, 6, 6, 1, 0, 11, 1, 7, 11, 4, 5, 0, 4, 11, 1 ),
1223
( 0, 5, 9, 6, 0, 6, 5, 2, 0, 3, 6, 1, 11, 6, 3, 5, 8, 4, 7, 7 ),
1224
( 6, 5, 9, 3, 6, 9, 11, 4, 4, 7, 9, 5, 7, 11, 9, 1 ),
1225
( 10, 4, 9, 6, 6, 4, 10, 5 ), ( 4, 10, 6, 6, 4, 9, 10, 3, 0, 8, 3, 7 ),
1226
( 10, 0, 1, 6, 10, 6, 0, 1, 6, 4, 0, 3 ),
1227
( 8, 3, 1, 3, 8, 1, 6, 0, 8, 6, 4, 6, 6, 1, 10, 6 ),
1228
( 1, 4, 9, 6, 1, 2, 4, 1, 2, 6, 4, 3 ),
1229
( 3, 0, 8, 7, 1, 2, 9, 5, 2, 4, 9, 2, 2, 6, 4, 3 ),
1230
( 0, 2, 4, 5, 4, 2, 6, 6 ), ( 8, 3, 2, 3, 8, 2, 4, 4, 4, 2, 6, 6 ),
1231
( 10, 4, 9, 6, 10, 6, 4, 3, 11, 2, 3, 7 ),
1232
( 0, 8, 2, 5, 2, 8, 11, 6, 4, 9, 10, 3, 4, 10, 6, 6 ),
1233
( 3, 11, 2, 7, 0, 1, 6, 1, 0, 6, 4, 6, 6, 1, 10, 6 ),
1234
( 6, 4, 1, 1, 6, 1, 10, 6, 4, 8, 1, 1, 2, 1, 11, 5, 8, 11, 1, 1 ),
1235
( 9, 6, 4, 6, 9, 3, 6, 0, 9, 1, 3, 3, 11, 6, 3, 5 ),
1236
( 8, 11, 1, 1, 8, 1, 0, 6, 11, 6, 1, 1, 9, 1, 4, 5, 6, 4, 1, 1 ),
1237
( 3, 11, 6, 3, 3, 6, 0, 4, 0, 6, 4, 6 ), ( 6, 4, 8, 3, 11, 6, 8, 5 ),
1238
( 7, 10, 6, 6, 7, 8, 10, 1, 8, 9, 10, 3 ),
1239
( 0, 7, 3, 6, 0, 10, 7, 0, 0, 9, 10, 3, 6, 7, 10, 5 ),
1240
( 10, 6, 7, 3, 1, 10, 7, 1, 1, 7, 8, 2, 1, 8, 0, 6 ),
1241
( 10, 6, 7, 3, 10, 7, 1, 4, 1, 7, 3, 6 ),
1242
( 1, 2, 6, 3, 1, 6, 8, 0, 1, 8, 9, 6, 8, 6, 7, 6 ),
1243
( 2, 6, 9, 1, 2, 9, 1, 6, 6, 7, 9, 1, 0, 9, 3, 5, 7, 3, 9, 1 ),
1244
( 7, 8, 0, 3, 7, 0, 6, 4, 6, 0, 2, 6 ), ( 7, 3, 2, 3, 6, 7, 2, 5 ),
1245
( 2, 3, 11, 7, 10, 6, 8, 1, 10, 8, 9, 6, 8, 6, 7, 6 ),
1246
( 2, 0, 7, 1, 2, 7, 11, 6, 0, 9, 7, 1, 6, 7, 10, 5, 9, 10, 7, 1 ),
1247
( 1, 8, 0, 6, 1, 7, 8, 2, 1, 10, 7, 1, 6, 7, 10, 5, 2, 3, 11, 7 ),
1248
( 11, 2, 1, 3, 11, 1, 7, 4, 10, 6, 1, 5, 6, 7, 1, 1 ),
1249
( 8, 9, 6, 1, 8, 6, 7, 6, 9, 1, 6, 1, 11, 6, 3, 5, 1, 3, 6, 1 ),
1250
( 0, 9, 1, 7, 11, 6, 7, 7 ),
1251
( 7, 8, 0, 3, 7, 0, 6, 4, 3, 11, 0, 5, 11, 6, 0, 1 ), ( 7, 11, 6, 7 ),
1252
( 7, 6, 11, 7 ), ( 3, 0, 8, 7, 11, 7, 6, 7 ),
1253
( 0, 1, 9, 7, 11, 7, 6, 7 ), ( 8, 1, 9, 6, 8, 3, 1, 3, 11, 7, 6, 7 ),
1254
( 10, 1, 2, 7, 6, 11, 7, 7 ), ( 1, 2, 10, 7, 3, 0, 8, 7, 6, 11, 7, 7 ),
1255
( 2, 9, 0, 6, 2, 10, 9, 3, 6, 11, 7, 7 ),
1256
( 6, 11, 7, 7, 2, 10, 3, 5, 10, 8, 3, 2, 10, 9, 8, 3 ),
1257
( 7, 2, 3, 6, 6, 2, 7, 5 ), ( 7, 0, 8, 6, 7, 6, 0, 1, 6, 2, 0, 3 ),
1258
( 2, 7, 6, 6, 2, 3, 7, 3, 0, 1, 9, 7 ),
1259
( 1, 6, 2, 6, 1, 8, 6, 0, 1, 9, 8, 3, 8, 7, 6, 3 ),
1260
( 10, 7, 6, 6, 10, 1, 7, 1, 1, 3, 7, 3 ),
1261
( 10, 7, 6, 6, 1, 7, 10, 4, 1, 8, 7, 2, 1, 0, 8, 3 ),
1262
( 0, 3, 7, 3, 0, 7, 10, 0, 0, 10, 9, 6, 6, 10, 7, 5 ),
1263
( 7, 6, 10, 3, 7, 10, 8, 4, 8, 10, 9, 6 ), ( 6, 8, 4, 6, 11, 8, 6, 5 ),
1264
( 3, 6, 11, 6, 3, 0, 6, 1, 0, 4, 6, 3 ),
1265
( 8, 6, 11, 6, 8, 4, 6, 3, 9, 0, 1, 7 ),
1266
( 9, 4, 6, 3, 9, 6, 3, 0, 9, 3, 1, 6, 11, 3, 6, 5 ),
1267
( 6, 8, 4, 6, 6, 11, 8, 3, 2, 10, 1, 7 ),
1268
( 1, 2, 10, 7, 3, 0, 11, 5, 0, 6, 11, 2, 0, 4, 6, 3 ),
1269
( 4, 11, 8, 6, 4, 6, 11, 3, 0, 2, 9, 5, 2, 10, 9, 3 ),
1270
( 10, 9, 3, 1, 10, 3, 2, 6, 9, 4, 3, 1, 11, 3, 6, 5, 4, 6, 3, 1 ),
1271
( 8, 2, 3, 6, 8, 4, 2, 1, 4, 6, 2, 3 ), ( 0, 4, 2, 5, 4, 6, 2, 3 ),
1272
( 1, 9, 0, 7, 2, 3, 4, 1, 2, 4, 6, 6, 4, 3, 8, 6 ),
1273
( 1, 9, 4, 3, 1, 4, 2, 4, 2, 4, 6, 6 ),
1274
( 8, 1, 3, 6, 8, 6, 1, 0, 8, 4, 6, 3, 6, 10, 1, 3 ),
1275
( 10, 1, 0, 3, 10, 0, 6, 4, 6, 0, 4, 6 ),
1276
( 4, 6, 3, 1, 4, 3, 8, 6, 6, 10, 3, 1, 0, 3, 9, 5, 10, 9, 3, 1 ),
1277
( 10, 9, 4, 3, 6, 10, 4, 5 ), ( 4, 9, 5, 7, 7, 6, 11, 7 ),
1278
( 0, 8, 3, 7, 4, 9, 5, 7, 11, 7, 6, 7 ),
1279
( 5, 0, 1, 6, 5, 4, 0, 3, 7, 6, 11, 7 ),
1280
( 11, 7, 6, 7, 8, 3, 4, 5, 3, 5, 4, 2, 3, 1, 5, 3 ),
1281
( 9, 5, 4, 7, 10, 1, 2, 7, 7, 6, 11, 7 ),
1282
( 6, 11, 7, 7, 1, 2, 10, 7, 0, 8, 3, 7, 4, 9, 5, 7 ),
1283
( 7, 6, 11, 7, 5, 4, 10, 5, 4, 2, 10, 2, 4, 0, 2, 3 ),
1284
( 3, 4, 8, 6, 3, 5, 4, 2, 3, 2, 5, 1, 10, 5, 2, 5, 11, 7, 6, 7 ),
1285
( 7, 2, 3, 6, 7, 6, 2, 3, 5, 4, 9, 7 ),
1286
( 9, 5, 4, 7, 0, 8, 6, 1, 0, 6, 2, 6, 6, 8, 7, 6 ),
1287
( 3, 6, 2, 6, 3, 7, 6, 3, 1, 5, 0, 5, 5, 4, 0, 3 ),
1288
( 6, 2, 8, 1, 6, 8, 7, 6, 2, 1, 8, 1, 4, 8, 5, 5, 1, 5, 8, 1 ),
1289
( 9, 5, 4, 7, 10, 1, 6, 5, 1, 7, 6, 2, 1, 3, 7, 3 ),
1290
( 1, 6, 10, 6, 1, 7, 6, 2, 1, 0, 7, 1, 8, 7, 0, 5, 9, 5, 4, 7 ),
1291
( 4, 0, 10, 1, 4, 10, 5, 6, 0, 3, 10, 1, 6, 10, 7, 5, 3, 7, 10, 1 ),
1292
( 7, 6, 10, 3, 7, 10, 8, 4, 5, 4, 10, 5, 4, 8, 10, 1 ),
1293
( 6, 9, 5, 6, 6, 11, 9, 1, 11, 8, 9, 3 ),
1294
( 3, 6, 11, 6, 0, 6, 3, 4, 0, 5, 6, 2, 0, 9, 5, 3 ),
1295
( 0, 11, 8, 6, 0, 5, 11, 0, 0, 1, 5, 3, 5, 6, 11, 3 ),
1296
( 6, 11, 3, 3, 6, 3, 5, 4, 5, 3, 1, 6 ),
1297
( 1, 2, 10, 7, 9, 5, 11, 1, 9, 11, 8, 6, 11, 5, 6, 6 ),
1298
( 0, 11, 3, 6, 0, 6, 11, 2, 0, 9, 6, 1, 5, 6, 9, 5, 1, 2, 10, 7 ),
1299
( 11, 8, 5, 1, 11, 5, 6, 6, 8, 0, 5, 1, 10, 5, 2, 5, 0, 2, 5, 1 ),
1300
( 6, 11, 3, 3, 6, 3, 5, 4, 2, 10, 3, 5, 10, 5, 3, 1 ),
1301
( 5, 8, 9, 6, 5, 2, 8, 0, 5, 6, 2, 3, 3, 8, 2, 5 ),
1302
( 9, 5, 6, 3, 9, 6, 0, 4, 0, 6, 2, 6 ),
1303
( 1, 5, 8, 1, 1, 8, 0, 6, 5, 6, 8, 1, 3, 8, 2, 5, 6, 2, 8, 1 ),
1304
( 1, 5, 6, 3, 2, 1, 6, 5 ),
1305
( 1, 3, 6, 1, 1, 6, 10, 6, 3, 8, 6, 1, 5, 6, 9, 5, 8, 9, 6, 1 ),
1306
( 10, 1, 0, 3, 10, 0, 6, 4, 9, 5, 0, 5, 5, 6, 0, 1 ),
1307
( 0, 3, 8, 7, 5, 6, 10, 7 ), ( 10, 5, 6, 7 ),
1308
( 11, 5, 10, 6, 7, 5, 11, 5 ), ( 11, 5, 10, 6, 11, 7, 5, 3, 8, 3, 0, 7 ),
1309
( 5, 11, 7, 6, 5, 10, 11, 3, 1, 9, 0, 7 ),
1310
( 10, 7, 5, 6, 10, 11, 7, 3, 9, 8, 1, 5, 8, 3, 1, 3 ),
1311
( 11, 1, 2, 6, 11, 7, 1, 1, 7, 5, 1, 3 ),
1312
( 0, 8, 3, 7, 1, 2, 7, 1, 1, 7, 5, 6, 7, 2, 11, 6 ),
1313
( 9, 7, 5, 6, 9, 2, 7, 0, 9, 0, 2, 3, 2, 11, 7, 3 ),
1314
( 7, 5, 2, 1, 7, 2, 11, 6, 5, 9, 2, 1, 3, 2, 8, 5, 9, 8, 2, 1 ),
1315
( 2, 5, 10, 6, 2, 3, 5, 1, 3, 7, 5, 3 ),
1316
( 8, 2, 0, 6, 8, 5, 2, 0, 8, 7, 5, 3, 10, 2, 5, 5 ),
1317
( 9, 0, 1, 7, 5, 10, 3, 1, 5, 3, 7, 6, 3, 10, 2, 6 ),
1318
( 9, 8, 2, 1, 9, 2, 1, 6, 8, 7, 2, 1, 10, 2, 5, 5, 7, 5, 2, 1 ),
1319
( 1, 3, 5, 5, 3, 7, 5, 3 ), ( 0, 8, 7, 3, 0, 7, 1, 4, 1, 7, 5, 6 ),
1320
( 9, 0, 3, 3, 9, 3, 5, 4, 5, 3, 7, 6 ), ( 9, 8, 7, 3, 5, 9, 7, 5 ),
1321
( 5, 8, 4, 6, 5, 10, 8, 1, 10, 11, 8, 3 ),
1322
( 5, 0, 4, 6, 5, 11, 0, 0, 5, 10, 11, 3, 11, 3, 0, 3 ),
1323
( 0, 1, 9, 7, 8, 4, 10, 1, 8, 10, 11, 6, 10, 4, 5, 6 ),
1324
( 10, 11, 4, 1, 10, 4, 5, 6, 11, 3, 4, 1, 9, 4, 1, 5, 3, 1, 4, 1 ),
1325
( 2, 5, 1, 6, 2, 8, 5, 0, 2, 11, 8, 3, 4, 5, 8, 5 ),
1326
( 0, 4, 11, 1, 0, 11, 3, 6, 4, 5, 11, 1, 2, 11, 1, 5, 5, 1, 11, 1 ),
1327
( 0, 2, 5, 1, 0, 5, 9, 6, 2, 11, 5, 1, 4, 5, 8, 5, 11, 8, 5, 1 ),
1328
( 9, 4, 5, 7, 2, 11, 3, 7 ),
1329
( 2, 5, 10, 6, 3, 5, 2, 4, 3, 4, 5, 2, 3, 8, 4, 3 ),
1330
( 5, 10, 2, 3, 5, 2, 4, 4, 4, 2, 0, 6 ),
1331
( 3, 10, 2, 6, 3, 5, 10, 2, 3, 8, 5, 1, 4, 5, 8, 5, 0, 1, 9, 7 ),
1332
( 5, 10, 2, 3, 5, 2, 4, 4, 1, 9, 2, 5, 9, 4, 2, 1 ),
1333
( 8, 4, 5, 3, 8, 5, 3, 4, 3, 5, 1, 6 ), ( 0, 4, 5, 3, 1, 0, 5, 5 ),
1334
( 8, 4, 5, 3, 8, 5, 3, 4, 9, 0, 5, 5, 0, 3, 5, 1 ), ( 9, 4, 5, 7 ),
1335
( 4, 11, 7, 6, 4, 9, 11, 1, 9, 10, 11, 3 ),
1336
( 0, 8, 3, 7, 4, 9, 7, 5, 9, 11, 7, 2, 9, 10, 11, 3 ),
1337
( 1, 10, 11, 3, 1, 11, 4, 0, 1, 4, 0, 6, 7, 4, 11, 5 ),
1338
( 3, 1, 4, 1, 3, 4, 8, 6, 1, 10, 4, 1, 7, 4, 11, 5, 10, 11, 4, 1 ),
1339
( 4, 11, 7, 6, 9, 11, 4, 4, 9, 2, 11, 2, 9, 1, 2, 3 ),
1340
( 9, 7, 4, 6, 9, 11, 7, 2, 9, 1, 11, 1, 2, 11, 1, 5, 0, 8, 3, 7 ),
1341
( 11, 7, 4, 3, 11, 4, 2, 4, 2, 4, 0, 6 ),
1342
( 11, 7, 4, 3, 11, 4, 2, 4, 8, 3, 4, 5, 3, 2, 4, 1 ),
1343
( 2, 9, 10, 6, 2, 7, 9, 0, 2, 3, 7, 3, 7, 4, 9, 3 ),
1344
( 9, 10, 7, 1, 9, 7, 4, 6, 10, 2, 7, 1, 8, 7, 0, 5, 2, 0, 7, 1 ),
1345
( 3, 7, 10, 1, 3, 10, 2, 6, 7, 4, 10, 1, 1, 10, 0, 5, 4, 0, 10, 1 ),
1346
( 1, 10, 2, 7, 8, 7, 4, 7 ), ( 4, 9, 1, 3, 4, 1, 7, 4, 7, 1, 3, 6 ),
1347
( 4, 9, 1, 3, 4, 1, 7, 4, 0, 8, 1, 5, 8, 7, 1, 1 ),
1348
( 4, 0, 3, 3, 7, 4, 3, 5 ), ( 4, 8, 7, 7 ),
1349
( 9, 10, 8, 5, 10, 11, 8, 3 ), ( 3, 0, 9, 3, 3, 9, 11, 4, 11, 9, 10, 6 ),
1350
( 0, 1, 10, 3, 0, 10, 8, 4, 8, 10, 11, 6 ),
1351
( 3, 1, 10, 3, 11, 3, 10, 5 ), ( 1, 2, 11, 3, 1, 11, 9, 4, 9, 11, 8, 6 ),
1352
( 3, 0, 9, 3, 3, 9, 11, 4, 1, 2, 9, 5, 2, 11, 9, 1 ),
1353
( 0, 2, 11, 3, 8, 0, 11, 5 ), ( 3, 2, 11, 7 ),
1354
( 2, 3, 8, 3, 2, 8, 10, 4, 10, 8, 9, 6 ), ( 9, 10, 2, 3, 0, 9, 2, 5 ),
1355
( 2, 3, 8, 3, 2, 8, 10, 4, 0, 1, 8, 5, 1, 10, 8, 1 ), ( 1, 10, 2, 7 ),
1356
( 1, 3, 8, 3, 9, 1, 8, 5 ), ( 0, 9, 1, 7 ), ( 0, 3, 8, 7 ), None )
1357
1358