Rubin
Hintergrund
Es gibt drei Familien regelmäßiger Polytope, die sich in unendliche Dimensionen erstrecken:
Die Simplexe, zu denen das Tetraeder gehört (ich werde sie hier oft als Hypertetraeder bezeichnen, obwohl der Begriff Simplex korrekter ist.) Ihre schlafi-Symbole haben die Form {3,3,...,3,3}
die n-Würfel, zu denen der Würfel gehört. Ihre schlafi Symbole sind von der Form{4,3,...,3,3}
Die Orthoplexe, zu denen das Oktaeder gehört (ich werde sie hier oft als Hyperoktaeder bezeichnen). Ihre Schlafi-Symbole haben die Form {3,3,...,3,4}
Es gibt eine weitere unendliche Familie regelmäßiger Polytope, Symbol {m}, die der zweidimensionalen Polygone, die eine beliebige Anzahl von Kanten m haben können.
Darüber hinaus gibt es nur fünf weitere Spezialfälle für reguläre Polytope: das dreidimensionale Ikosaeder {3,5}und das Dodekaeder {5,3}; ihre 4-dimensionalen Analoga sind die 600-Zellen {3,3,5}und 120-Zellen {5,3,3}; und ein weiteres 4-dimensionales Polytop, das 24-Zellen-Polytop {3,4,3}(dessen nächstliegende Analoga in 3 Dimensionen das Kuboktaeder und sein duales das rhombische Dodekaeder sind.)
Hauptfunktion
Unten ist die polytopeHauptfunktion, die das Schlafzimmer-Symbol interpretiert. Es erwartet ein Array mit Zahlen und gibt ein Array mit einer Reihe von Arrays wie folgt zurück:
Ein Array aller Scheitelpunkte, jeweils ausgedrückt als n-Element-Koordinatenarray (wobei n die Anzahl der Dimensionen ist)
Ein Array aller Kanten, die jeweils als 2-Element der Scheitelpunktindizes ausgedrückt werden
Ein Array aller Flächen, jeweils ausgedrückt als m-Element der Eckpunktindizes (wobei m die Anzahl der Eckpunkte pro Fläche ist).
und so weiter, je nach Anzahl der Dimensionen.
Es berechnet 2D-Polytope selbst, ruft Funktionen für die dreidimensionalen Familien auf und verwendet Nachschlagetabellen für die fünf Sonderfälle. Es erwartet, die darüber deklarierten Funktionen und Tabellen zu finden.
include Math
#code in subsequent sections of this answer should be inserted here
polytope=->schl{
if schl.size==1 #if a single digit calculate and return a polygon
return [(1..schl[0]).map{|i|[sin(PI*2*i/schl[0]),cos(PI*2*i/schl[0])]},(1..schl[0]).map{|i|[i%schl[0],(i+1)%schl[0]]}]
elsif i=[[3,5],[5,3]].index(schl) #if a 3d special, lookup from tables
return [[vv,ee,ff],[uu,aa,bb]][i]
elsif i=[[3,3,5],[5,3,3],[3,4,3]].index(schl) #if a 4d special. lookup fromm tables
return [[v,e,f,g],[u,x,y,z],[o,p,q,r]][i]
elsif schl.size==schl.count(3) #if all threes, call tetr for a hypertetrahedron
return tetr[schl.size+1]
elsif schl.size-1==schl.count(3) #if all except one number 3
return cube[schl.size+1] if schl[0]==4 #and the 1st digit is 4, call cube for a hypercube
return octa[schl.size+1] if schl[-1]==4 #and the last digit is 4, call octa for a hyperoctahedron
end
return "error" #in any other case return an error
}
Funktionen für die Tetraeder-, Würfel- und Oktaederfamilie
https://en.wikipedia.org/wiki/Simplex
https://en.wikipedia.org/wiki/5-cell (4d simplex)
http://mathworld.wolfram.com/Simplex.html
Erklärung der Tetraederfamilie - Koordinaten
Ein n-dimensionales Simplex / Hypertetraeder hat n + 1 Punkte. Es ist sehr einfach, die Eckpunkte des n-dimensionalen Simplex in n + 1 Dimensionen anzugeben.
So (1,0,0),(0,1,0),(0,0,1)beschreibt ein 2D - Dreieck in 3 Dimensionen eingebettet und (1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)beschreibt einen 3D Tetraeders in 4 Dimensionen eingebettet. Dies lässt sich leicht überprüfen, indem bestätigt wird, dass alle Abstände zwischen Scheitelpunkten sqrt (2) sind.
Im Internet werden verschiedene komplizierte Algorithmen angegeben, um die Eckpunkte für den n-dimensionalen Simplex im n-dimensionalen Raum zu finden. Ich fand einen bemerkenswert einfachen in Will Jagys Kommentaren zu dieser Antwort /mathpro//a/38725 . Der letzte Punkt liegt auf der Linie p=q=...=x=y=zin einem Abstand von sqrt (2) von den anderen. Somit kann das obige Dreieck durch Hinzufügen eines Punktes an entweder (-1/3,-1/3,-1/3)oder in ein Tetraeder umgewandelt werden (1,1,1). Diese 2 möglichen Werte der Koordinaten für den letzten Punkt sind durch (1-(1+n)**0.5)/nund gegeben(1+(1+n)**0.5)/n
Da die Frage besagt, dass die Größe des n-Topes keine Rolle spielt, ziehe ich es vor, mit n zu multiplizieren und der Einfachheit halber die Koordinaten (n,0,0..0)bis zum (0..0,0,n)Endpunkt bei (t,t,..,t,t)t = 1-(1+n)**0.5zu verwenden.
Da der Mittelpunkt dieses Tetraeders nicht am Ursprung liegt, muss eine Korrektur aller Koordinaten durch die Linie vorgenommen werden, s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}die den Abstand des Mittelpunkts vom Ursprung ermittelt und diesen subtrahiert. Ich habe dies als separate Operation behalten. Allerdings habe ich verwendet , s[i]+=nwo s[i]=ntun würde, auf die Tatsache hinweisen , dass , wenn die Anordnung von initialisiert s=[0]*nuns stattdessen hier Offset korrekt setzen könnte und tun , um die Zentrierung Korrektur am Anfang und nicht am Ende.
Erklärung der Tetraederfamilie - Graphentopologie
Die Grafik des Simplex ist die vollständige Grafik: Jeder Scheitelpunkt ist genau einmal mit jedem anderen Scheitelpunkt verbunden. Wenn wir einen n-Simplex haben, können wir jeden Scheitelpunkt entfernen, um einen n-1-Simplex zu erhalten, bis zu dem Punkt, an dem wir ein Dreieck oder sogar eine Kante haben.
Daher müssen wir insgesamt 2 ** (n + 1) Elemente katalogisieren, die jeweils durch eine Binärzahl dargestellt werden. Dies reicht von allen 0s für Nichts über eins 1für einen Scheitelpunkt und zwei 1s für eine Kante bis zu allen 1s für das gesamte Polytop.
Wir richten ein Array mit leeren Arrays ein, um die Elemente jeder Größe zu speichern. Dann schleifen wir von Null bis (2 ** n + 1), um jede der möglichen Untergruppen von Scheitelpunkten zu erzeugen, und speichern sie in dem Array entsprechend der Größe jeder Untergruppe.
Wir interessieren uns weder für etwas Kleineres als eine Kante (einen Scheitelpunkt oder eine Null) noch für das gesamte Polytop (da im Beispiel in der Frage nicht der gesamte Würfel angegeben ist), also kehren wir zurück tg[2..n], um diese unerwünschten Elemente zu entfernen. Bevor wir zurückkehren, heften wir [tv] mit den Scheitelpunktkoordinaten an den Anfang.
Code
tetr=->n{
#Tetrahedron Family Vertices
tv=(0..n).map{|i|
s=[0]*n
if i==n
s.map!{(1-(1+n)**0.5)}
else
s[i]+=n
end
s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}
s}
#Tetrahedron Family Graph
tg=(0..n+1).map{[]}
(2**(n+1)).times{|i|
s=[]
(n+1).times{|j|s<<j if i>>j&1==1}
tg[s.size]<<s
}
return [tv]+tg[2..n]}
cube=->n{
#Cube Family Vertices
cv=(0..2**n-1).map{|i|s=[];n.times{|j|s<<(i>>j&1)*2-1};s}
#Cube Family Graph
cg=(0..n+1).map{[]}
(3**n).times{|i| #for each point
s=[]
cv.size.times{|j| #and each vertex
t=true #assume vertex goes with point
n.times{|k| #and each pair of opposite sides
t&&= (i/(3**k)%3-1)*cv[j][k]!=-1 #if the vertex has kingsmove distance >1 from point it does not belong
}
s<<j if t #add the vertex if it belongs
}
cg[log2(s.size)+1]<<s if s.size > 0
}
return [cv]+cg[2..n]}
octa=->n{
#Octahedron Family Vertices
ov=(0..n*2-1).map{|i|s=[0]*n;s[i/2]=(-1)**i;s}
#Octahedron Family Graph
og=(0..n).map{[]}
(3**n).times{|i| #for each point
s=[]
ov.size.times{|j| #and each vertex
n.times{|k| #and each pair of opposite sides
s<<j if (i/(3**k)%3-1)*ov[j][k]==1 #if the vertex is located in the side corresponding to the point, add the vertex to the list
}
}
og[s.size]<<s
}
return [ov]+og[2..n]}
Erklärung der Würfel- und Oktaederfamilien - Koordinaten
Der n-Würfel hat 2**nScheitelpunkte , die jeweils durch eine Anordnung von n repräsentiert 1s und -1s (alle Möglichkeiten erlaubt sind.) Wir iterieren Indizes 0auf 2**n-1der Liste aller Knoten und für jeden Vertex durch Iterieren durch die Bits der ein Array aufbauen indexieren und -1oder 1zum Array hinzufügen (niedrigstwertiges Bit bis höchstwertiges Bit). Somit wird Binary 1101zum 4d-Punkt [1,-1,1,1].
Das n-Oktaeder oder der n-Orthoplex hat 2nEckpunkte mit allen Koordinaten null bis auf einen, die ein sein 1oder sind -1. Die Reihenfolge der Eckpunkte in dem generierten Array ist [[1,0,0..],[-1,0,0..],[0,1,0..],[0,-1,0..],[0,0,1..],[0,0,-1..]...]. Beachten Sie, dass, da das Oktaeder das Dual des Würfels ist, die Scheitelpunkte des Oktaeders durch die Zentren der Flächen des Würfels definiert sind, der es umgibt.
Erklärung der Würfel- und Oktaederfamilien - Graphentopologie
Die Hyperkubusseiten und die Tatsache, dass das Hyperoktaeder das Dual des Hyperkubus ist, haben einige Inspirationen hervorgerufen .
Für den n-Cube gibt es 3**nArtikel zum Katalogisieren. Zum Beispiel hat der Würfel 3 3**3= 27 Elemente. Dies lässt sich an einem Rubik-Würfel erkennen, der 1 Zentrum, 6 Flächen, 12 Kanten und 8 Eckpunkte für insgesamt 27 hat. Wir durchlaufen -1,0 und -1 in allen Dimensionen, die einen n-Würfel mit der Seitenlänge 2x2x2 definieren .. und gebe alle Eckpunkte zurück, die sich NICHT auf der gegenüberliegenden Seite des Würfels befinden. Der Mittelpunkt des Würfels gibt also alle 2 ** n Scheitelpunkte zurück, und wenn Sie eine Einheit entlang einer beliebigen Achse vom Mittelpunkt entfernen, wird die Anzahl der Scheitelpunkte um die Hälfte verringert.
Wie bei der Tetraederfamilie erzeugen wir zunächst ein leeres Array von Arrays und füllen es entsprechend der Anzahl der Eckpunkte pro Element. Beachten Sie, dass wir log2(s.size)+1anstelle von einfach verwenden , da die Anzahl der Eckpunkte beim Durchlaufen von Kanten, Flächen, Würfeln usw. um 2 ** n variiert s.size. Wieder müssen wir den Hypercube selbst und alle Elemente mit weniger als 2 Eckpunkten entfernen, bevor wir von der Funktion zurückkehren.
Die Oktaeder- / Orthoplex-Familie sind die Dualen der Würfelfamilie, daher gibt es auch hier wieder 3**nArtikel zum Katalogisieren. Hier werden -1,0,1alle Dimensionen durchlaufen. Wenn die Koordinate eines Scheitelpunkts ungleich der entsprechenden Koordinate des Punkts ist, wird der Scheitelpunkt zu der Liste hinzugefügt, die diesem Punkt entspricht. Eine Kante entspricht also einem Punkt mit zwei Nicht-Null-Koordinaten, ein Dreieck einem Punkt mit drei Nicht-Null-Koordinaten und ein Tetraeder einem Punkt mit vier Nicht-Null-Kontakten (im 4d-Raum).
Die resultierenden Vertex-Arrays für jeden Punkt werden wie in den anderen Fällen in einem großen Array gespeichert, und wir müssen alle Elemente mit weniger als 2 Vertices entfernen, bevor wir zurückkehren. In diesem Fall müssen wir jedoch nicht das gesamte übergeordnete N-Top entfernen, da der Algorithmus es nicht aufzeichnet.
Die Implementierungen des Codes für den Cube wurden so ähnlich wie möglich gestaltet. Dies hat zwar eine gewisse Eleganz, es ist jedoch wahrscheinlich, dass effizientere Algorithmen basierend auf denselben Prinzipien entwickelt werden könnten.
https://en.wikipedia.org/wiki/Hypercube
http://mathworld.wolfram.com/Hypercube.html
https://en.wikipedia.org/wiki/Cross-polytope
http://mathworld.wolfram.com/CrossPolytope.html
Code zum Generieren von Tabellen für die 3D-Sonderfälle
Eine Orientierung mit dem Ikosaeder / Dodekaeder, die mit der fünffachen Symmetrieachse parallel zur letzten Dimension orientiert ist, wurde verwendet, da dies für die konsistenteste Kennzeichnung der Teile sorgte. Die Nummerierung der Eckpunkte und Flächen für das Ikosaeder entspricht dem Diagramm in den Codekommentaren und ist für das Dodekaeder umgekehrt.
Laut https://en.wikipedia.org/wiki/Regular_icosahedron beträgt der Breitengrad der 10 unpolaren Eckpunkte des Ikosaeders +/- arctan (1/2). Die Koordinaten der ersten 10 Eckpunkte des Ikosaeders werden aus berechnet dies auf zwei Kreisen mit Radius 2 in einem Abstand +/- 2 von der xy-Ebene. Dies bewirkt, dass der Gesamtradius der Umkreiskugel sqrt (5) so ist, dass die letzten 2 Eckpunkte bei (0,0, + / - sqrt (2)) liegen.
Die Koordinaten der Eckpunkte des Dodekaeders werden berechnet, indem die Koordinaten der drei Ikosaeder-Eckpunkte, die sie umgeben, summiert werden.
=begin
TABLE NAMES vertices edges faces
icosahedron vv ee ff
dodecahedron uu aa bb
10
/ \ / \ / \ / \ / \
/10 \ /12 \ /14 \ /16 \ /18 \
-----1-----3-----5-----7-----9
\ 0 / \ 2 / \ 4 / \ 6 / \ 8 / \
\ / 1 \ / 3 \ / 5 \ / 7 \ / 9 \
0-----2-----4-----6-----8-----
\11 / \13 / \15 / \17 / \19 /
\ / \ / \ / \ / \ /
11
=end
vv=[];ee=[];ff=[]
10.times{|i|
vv[i]=[2*sin(PI/5*i),2*cos(PI/5*i),(-1)**i]
ee[i]=[i,(i+1)%10];ee[i+10]=[i,(i+2)%10];ee[i+20]=[i,11-i%2]
ff[i]=[(i-1)%10,i,(i+1)%10];ff[i+10]=[(i-1)%10,10+i%2,(i+1)%10]
}
vv+=[[0,0,-5**0.5],[0,0,5**0.5]]
uu=[];aa=[];bb=[]
10.times{|i|
uu[i]=(0..2).map{|j|vv[ff[i][0]][j]+vv[ff[i][1]][j]+vv[ff[i][2]][j]}
uu[i+10]=(0..2).map{|j|vv[ff[i+10][0]][j]+vv[ff[i+10][1]][j]+vv[ff[i+10][2]][j]}
aa[i]=[i,(i+1)%10];aa[i+10]=[i,(i+10)%10];aa[i+20]=[(i-1)%10+10,(i+1)%10+10]
bb[i]=[(i-1)%10+10,(i-1)%10,i,(i+1)%10,(i+1)%10+10]
}
bb+=[[10,12,14,16,18],[11,13,15,17,19]]
Code zum Generieren der Tabellen für die 4d-Sonderfälle
Das ist ein bisschen hacken. Die Ausführung dieses Codes dauert einige Sekunden. Es ist besser, die Ausgabe in einer Datei zu speichern und nach Bedarf zu laden.
Die Liste der 120 Scheitelpunktkoordinaten für die 600-Zelle stammt von http://mathworld.wolfram.com/600-Cell.html . Die 24 Scheitelpunktkoordinaten, die kein goldenes Verhältnis aufweisen, bilden die Scheitelpunkte einer 24-Zelle. Wikipedia hat das gleiche Schema, aber einen Fehler in der relativen Skala dieser 24 Koordinaten und der anderen 96.
#TABLE NAMES vertices edges faces cells
#600 cell (analogue of icosahedron) v e f g
#120 cell (analogue of dodecahedron) u x y z
#24 cell o p q r
#600-CELL
# 120 vertices of 600cell. First 24 are also vertices of 24-cell
v=[[2,0,0,0],[0,2,0,0],[0,0,2,0],[0,0,0,2],[-2,0,0,0],[0,-2,0,0],[0,0,-2,0],[0,0,0,-2]]+
(0..15).map{|j|[(-1)**(j/8),(-1)**(j/4),(-1)**(j/2),(-1)**j]}+
(0..95).map{|i|j=i/12
a,b,c,d=1.618*(-1)**(j/4),(-1)**(j/2),0.618*(-1)**j,0
h=[[a,b,c,d],[b,a,d,c],[c,d,a,b],[d,c,b,a]][i%12/3]
(i%3).times{h[0],h[1],h[2]=h[1],h[2],h[0]}
h}
#720 edges of 600cell. Identified by minimum distance of 2/phi between them
e=[]
120.times{|i|120.times{|j|
e<<[i,j] if i<j && ((v[i][0]-v[j][0])**2+(v[i][1]-v[j][1])**2+(v[i][2]-v[j][2])**2+(v[i][3]-v[j][3])**2)**0.5<1.3
}}
#1200 faces of 600cell.
#If 2 edges share a common vertex and the other 2 vertices form an edge in the list, it is a valid triangle.
f=[]
720.times{|i|720.times{|j|
f<< [e[i][0],e[i][1],e[j][1]] if i<j && e[i][0]==e[j][0] && e.index([e[i][1],e[j][1]])
}}
#600 cells of 600cell.
#If 2 triangles share a common edge and the other 2 vertices form an edge in the list, it is a valid tetrahedron.
g=[]
1200.times{|i|1200.times{|j|
g<< [f[i][0],f[i][1],f[i][2],f[j][2]] if i<j && f[i][0]==f[j][0] && f[i][1]==f[j][1] && e.index([f[i][2],f[j][2]])
}}
#120 CELL (dual of 600 cell)
#600 vertices of 120cell, correspond to the centres of the cells of the 600cell
u=g.map{|i|s=[0,0,0,0];i.each{|j|4.times{|k|s[k]+=v[j][k]/4.0}};s}
#1200 edges of 120cell at centres of faces of 600-cell. Search for pairs of tetrahedra with common face
x=f.map{|i|s=[];600.times{|j|s<<j if i==(i & g[j])};s}
#720 pentagonal faces, surrounding edges of 600-cell. Search for sets of 5 tetrahedra with common edge
y=e.map{|i|s=[];600.times{|j|s<<j if i==(i & g[j])};s}
#120 dodecahedral cells surrounding vertices of 600-cell. Search for sets of 20 tetrahedra with common vertex
z=(0..119).map{|i|s=[];600.times{|j|s<<j if [i]==([i] & g[j])};s}
#24-CELL
#24 vertices, a subset of the 600cell
o=v[0..23]
#96 edges, length 2, found by minimum distances between vertices
p=[]
24.times{|i|24.times{|j|
p<<[i,j] if i<j && ((v[i][0]-v[j][0])**2+(v[i][1]-v[j][1])**2+(v[i][2]-v[j][2])**2+(v[i][3]-v[j][3])**2)**0.5<2.1
}}
#96 triangles
#If 2 edges share a common vertex and the other 2 vertices form an edge in the list, it is a valid triangle.
q=[]
96.times{|i|96.times{|j|
q<< [p[i][0],p[i][1],p[j][1]] if i<j && p[i][0]==p[j][0] && p.index([p[i][1],p[j][1]])
}}
#24 cells. Calculates the centre of the cell and the 6 vertices nearest it
r=(0..23).map{|i|a,b=(-1)**i,(-1)**(i/2)
c=[[a,b,0,0],[a,0,b,0],[a,0,0,b],[0,a,b,0],[0,a,0,b],[0,0,a,b]][i/4]
s=[]
24.times{|j|t=v[j]
s<<j if (c[0]-t[0])**2+(c[1]-t[1])**2+(c[2]-t[2])**2+(c[3]-t[3])**2<=2
}
s}
https://en.wikipedia.org/wiki/600-cell
http://mathworld.wolfram.com/600-Cell.html
https://en.wikipedia.org/wiki/120-cell
http://mathworld.wolfram.com/120-Cell.html
https://en.wikipedia.org/wiki/24-cell
http://mathworld.wolfram.com/24-Cell.html
Anwendungsbeispiel und Ausgabe
cell24 = polytope[[3,4,3]]
puts "vertices"
cell24[0].each{|i|p i}
puts "edges"
cell24[1].each{|i|p i}
puts "faces"
cell24[2].each{|i|p i}
puts "cells"
cell24[3].each{|i|p i}
vertices
[2, 0, 0, 0]
[0, 2, 0, 0]
[0, 0, 2, 0]
[0, 0, 0, 2]
[-2, 0, 0, 0]
[0, -2, 0, 0]
[0, 0, -2, 0]
[0, 0, 0, -2]
[1, 1, 1, 1]
[1, 1, 1, -1]
[1, 1, -1, 1]
[1, 1, -1, -1]
[1, -1, 1, 1]
[1, -1, 1, -1]
[1, -1, -1, 1]
[1, -1, -1, -1]
[-1, 1, 1, 1]
[-1, 1, 1, -1]
[-1, 1, -1, 1]
[-1, 1, -1, -1]
[-1, -1, 1, 1]
[-1, -1, 1, -1]
[-1, -1, -1, 1]
[-1, -1, -1, -1]
edges
[0, 8]
[0, 9]
[0, 10]
[0, 11]
[0, 12]
[0, 13]
[0, 14]
[0, 15]
[1, 8]
[1, 9]
[1, 10]
[1, 11]
[1, 16]
[1, 17]
[1, 18]
[1, 19]
[2, 8]
[2, 9]
[2, 12]
[2, 13]
[2, 16]
[2, 17]
[2, 20]
[2, 21]
[3, 8]
[3, 10]
[3, 12]
[3, 14]
[3, 16]
[3, 18]
[3, 20]
[3, 22]
[4, 16]
[4, 17]
[4, 18]
[4, 19]
[4, 20]
[4, 21]
[4, 22]
[4, 23]
[5, 12]
[5, 13]
[5, 14]
[5, 15]
[5, 20]
[5, 21]
[5, 22]
[5, 23]
[6, 10]
[6, 11]
[6, 14]
[6, 15]
[6, 18]
[6, 19]
[6, 22]
[6, 23]
[7, 9]
[7, 11]
[7, 13]
[7, 15]
[7, 17]
[7, 19]
[7, 21]
[7, 23]
[8, 9]
[8, 10]
[8, 12]
[8, 16]
[9, 11]
[9, 13]
[9, 17]
[10, 11]
[10, 14]
[10, 18]
[11, 15]
[11, 19]
[12, 13]
[12, 14]
[12, 20]
[13, 15]
[13, 21]
[14, 15]
[14, 22]
[15, 23]
[16, 17]
[16, 18]
[16, 20]
[17, 19]
[17, 21]
[18, 19]
[18, 22]
[19, 23]
[20, 21]
[20, 22]
[21, 23]
[22, 23]
faces
[0, 8, 9]
[0, 8, 10]
[0, 8, 12]
[0, 9, 11]
[0, 9, 13]
[0, 10, 11]
[0, 10, 14]
[0, 11, 15]
[0, 12, 13]
[0, 12, 14]
[0, 13, 15]
[0, 14, 15]
[1, 8, 9]
[1, 8, 10]
[1, 8, 16]
[1, 9, 11]
[1, 9, 17]
[1, 10, 11]
[1, 10, 18]
[1, 11, 19]
[1, 16, 17]
[1, 16, 18]
[1, 17, 19]
[1, 18, 19]
[2, 8, 9]
[2, 8, 12]
[2, 8, 16]
[2, 9, 13]
[2, 9, 17]
[2, 12, 13]
[2, 12, 20]
[2, 13, 21]
[2, 16, 17]
[2, 16, 20]
[2, 17, 21]
[2, 20, 21]
[3, 8, 10]
[3, 8, 12]
[3, 8, 16]
[3, 10, 14]
[3, 10, 18]
[3, 12, 14]
[3, 12, 20]
[3, 14, 22]
[3, 16, 18]
[3, 16, 20]
[3, 18, 22]
[3, 20, 22]
[4, 16, 17]
[4, 16, 18]
[4, 16, 20]
[4, 17, 19]
[4, 17, 21]
[4, 18, 19]
[4, 18, 22]
[4, 19, 23]
[4, 20, 21]
[4, 20, 22]
[4, 21, 23]
[4, 22, 23]
[5, 12, 13]
[5, 12, 14]
[5, 12, 20]
[5, 13, 15]
[5, 13, 21]
[5, 14, 15]
[5, 14, 22]
[5, 15, 23]
[5, 20, 21]
[5, 20, 22]
[5, 21, 23]
[5, 22, 23]
[6, 10, 11]
[6, 10, 14]
[6, 10, 18]
[6, 11, 15]
[6, 11, 19]
[6, 14, 15]
[6, 14, 22]
[6, 15, 23]
[6, 18, 19]
[6, 18, 22]
[6, 19, 23]
[6, 22, 23]
[7, 9, 11]
[7, 9, 13]
[7, 9, 17]
[7, 11, 15]
[7, 11, 19]
[7, 13, 15]
[7, 13, 21]
[7, 15, 23]
[7, 17, 19]
[7, 17, 21]
[7, 19, 23]
[7, 21, 23]
cells
[0, 1, 8, 9, 10, 11]
[1, 4, 16, 17, 18, 19]
[0, 5, 12, 13, 14, 15]
[4, 5, 20, 21, 22, 23]
[0, 2, 8, 9, 12, 13]
[2, 4, 16, 17, 20, 21]
[0, 6, 10, 11, 14, 15]
[4, 6, 18, 19, 22, 23]
[0, 3, 8, 10, 12, 14]
[3, 4, 16, 18, 20, 22]
[0, 7, 9, 11, 13, 15]
[4, 7, 17, 19, 21, 23]
[1, 2, 8, 9, 16, 17]
[2, 5, 12, 13, 20, 21]
[1, 6, 10, 11, 18, 19]
[5, 6, 14, 15, 22, 23]
[1, 3, 8, 10, 16, 18]
[3, 5, 12, 14, 20, 22]
[1, 7, 9, 11, 17, 19]
[5, 7, 13, 15, 21, 23]
[2, 3, 8, 12, 16, 20]
[3, 6, 10, 14, 18, 22]
[2, 7, 9, 13, 17, 21]
[6, 7, 11, 15, 19, 23]