Ruby
Background
There are three families of regular polytope extending into infinite dimensions:
the simplexes, of which the tetrahedron is a member (I will often refer to them here as hypertetrahedra, though the term simplex is more correct.) Their schlafi symbols are of the form {3,3,...,3,3}
the n-cubes, of which the cube is a member. Their schlafi symbols are of the form {4,3,...,3,3}
the orthoplexes, of which the octahedron is a member (I will often refer to them here as hyperoctahedra) Their schlafi symbols are of the form {3,3,...,3,4}
There is one further infinite family of regular polytopes, symbol {m}
, that of the 2 dimensional polygons, which may have any number of edges m.
In addition to this, there are just five other special cases of regular polytope: the 3-dimensional icosahedron {3,5}
and dodecahedron {5,3}
; their 4-dimensional analogues the 600-cell {3,3,5}
and 120-cell {5,3,3}
; and one other 4 dimensional polytope, the 24-cell {3,4,3}
(whose closest analogues in 3 dimensions are the cuboctahedron and its dual the rhombic dodecahedron.)
Main function
Below is the main polytope
function that interprets the schlafi symbol. It expects an array of numbers, and returns an array containing a bunch of arrays as follows:
An array of all vertices, each expressed as an n-element array of coordinates (where n is the number of dimensions)
An array of all edges, each expressed as a 2-element of vertex indices
An array of all faces, each expressed as an m-element of vertex indices (where m is the number of vertices per face)
and so on as appropriate for the number of dimensions.
It calculates 2d polytopes itself, calls functions for the 3 infinite dimensional families, and uses lookup tables for the five special cases. It expects to find the functions and tables declared above it.
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
}
Functions for the tetrahedron, cube and octahedron families
https://en.wikipedia.org/wiki/Simplex
https://en.wikipedia.org/wiki/5-cell (4d simplex)
http://mathworld.wolfram.com/Simplex.html
Tetrahedron family explanation - coordinates
an n-dimensional simplex/hypertetrahedron has n+1 points. It is very easy to give the vertices of the n-dimensional simplex in n+1 dimensions.
Thus (1,0,0),(0,1,0),(0,0,1)
describes a 2d triangle embedded in 3 dimensions and (1,0,0,0),(0,1,0,0),(0,0,1,0),(0,0,0,1)
describes a 3d tetrahedron embedded in 4 dimensions. This is easily verified by confirming that all distances between vertices are sqrt(2).
Various complicated algorithms are given on the internet for finding the vertices for the n-dimensional simplex in n-dimensional space. I found a remarkably simple one in Will Jagy's comments on this answer https://mathoverflow.net/a/38725 . The last point lies on the line p=q=...=x=y=z
at a distance of sqrt(2) from the others. Thus the triangle above can be converted to a tetrahedron by addition of a point at either (-1/3,-1/3,-1/3)
or (1,1,1)
. These 2 possible values of the coordinates for the last point are given by (1-(1+n)**0.5)/n
and (1+(1+n)**0.5)/n
As the question says the size of the n-tope does not matter, I prefer to multiply through by n and use the coordinates (n,0,0..0)
up to (0..0,0,n)
with the final point at (t,t,..,t,t)
where t = 1-(1+n)**0.5
for simplicity.
As the centre of this tetrahedron is not at the origin, a correction to all coordinates must be done by the line s.map!{|j|j-((1-(1+n)**0.5)+n)/(1+n)}
which finds how far the centre is from the origin and subtracts it. I have kept this as a separate operation. However I have used s[i]+=n
where s[i]=n
would do, to allude to the fact that when the array is initialised by s=[0]*n
we could put the correct offset in here instead and do the centring correction at the outset rather than the end.
Tetrahedron family explanation - graph topology
The graph of the simplex is the complete graph: every vertex is connected exactly once to every other vertex. If we have an n simplex, we can remove any vertex to give an n-1 simplex, down to the point where we have a triangle or even an edge.
Therefore we have a total of 2**(n+1) items to catalogue, each represented by a binary number. This ranges from all 0
s for nothingness, through one 1
for a vertex and two 1
s for an edge, up to all 1
s for the complete polytope.
We set up an array of empty arrays to store the elements of each size. Then we loop from zero to (2**n+1) to generate each of the possible subsets of vertices, and store them in the array according to the size of each subset.
We are not interested in anything smaller than an edge (a vertex or a zero) nor in the complete polytope (as the complete cube is not given in the example in the question), so we return tg[2..n]
to remove these unwanted elements. Before returning, we tack [tv] containing the vertex coordinates onto the beginning.
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]}
cube and octahedron families explanation - coordinates
The n-cube has 2**n
vertices, each represented by an array of n 1
s and -1
s (all possibilities are allowed.) We iterate through indexes 0
to 2**n-1
of the list of all vertices, and build an array for each vertex by iterating through the bits of the index and adding -1
or 1
to the array (least significant bit to most significant bit.) Thus Binary 1101
becomes the 4d point [1,-1,1,1]
.
The n-octahedron or n-orthoplex has 2n
vertices, with all coordinates zero except one, which an be 1
or -1
. The order of the vertices in the array generated is [[1,0,0..],[-1,0,0..],[0,1,0..],[0,-1,0..],[0,0,1..],[0,0,-1..]...]
. Note that as the octahedron is the dual of the cube, the vertices of the octahedron are defined by the centres of the faces of the cube that surrounds it.
cube and octahedron families explanation - graph topology
Some inspiration was taken from Hypercube sides and the fact that the hyperoctahedron is the dual of the hypercube.
For the n-cube, there are 3**n
items to catalogue. For example, the 3 cube has 3**3
= 27 elements. This can be seen by studying a rubik's cube, which has 1 centre, 6 faces, 12 edges and 8 vertices for a total of 27. We iterate through -1,0 and -1 in all dimensions defining an n-cube of sidelength 2x2x2.. and return all vertices that are NOT on the opposite side of the cube. Thus the centrepoint of the cube returns all 2**n vertices, and moving one unit away from the centre along any axis reduces the number of vertices by half.
As with the tetrahedron family, we start by generating an empty array of arrays and populate it according to the number of vertices per element. Note that because the number of vertices varies as 2**n as we go up through edges, faces, cubes, etc, we use log2(s.size)+1
instead of simply s.size
. Again, we have to remove the hypercube itself and all elements with less than 2 vertices before returning from the function.
The octahedron/orthoplex family are the duals of the cube family, therefore again there are 3**n
items to catalogue. Here we iterate through -1,0,1
for all the dimensions and if the nonzero coordinate of a vertex is equal to the corresponding coordinate of the point, the vertex is added to the list corresponding to that point. Thus an edge corresponds to a point with two nonzero coordinates, a triangle to a point with 3 nonzero coordinates and a tetrahedron to a point with 4 nonzero contacts (in 4d space.)
The resulting arrays of vertex for each point are stored in a large array as for the other cases, and we have to remove any elements with less than 2 vertices before returning. But in this case we do not have to remove the complete parent n-tope because the algorithm does not record it.
The implementations of the code for the cube were designed to be as similar as possible. While this has a certain elegance, it is likely that more efficient algorithms based on the same principles could be devised.
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 for generating tables for the 3d special cases
An orientation with the icosahedron / dodecahedron oriented with the fivefold symmetry axis parallel to the last dimension was used, as it made for the most consistent labelling of the parts. The numbering of vertices and faces for the icosahedron is per the diagram in the code comments, and reversed for the dodecahedron.
According to https://en.wikipedia.org/wiki/Regular_icosahedron the latitude of the 10 non-polar vertices of the icosahedron is +/-arctan(1/2) The coordinates of the first 10 vertices of the icosahedron are calculated from this, on two circles of radius 2 at distance +/-2 from the xy plane. This makes the overall radius of the circumsphere sqrt(5) so the last 2 vertices are at (0,0,+/-sqrt(2)).
The coordinates of the vertices of the dodecahedron are calculated by summing the coordinates of the three icosahedron vertices that surround them.
=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 for generating the tables for the 4d special cases
This is a bit of a hack. This code takes a few seconds to run. It would be better to store the output in a file and load it in as required.
The list of 120 vertex coordinates for the 600cell is from http://mathworld.wolfram.com/600-Cell.html . The 24 vertex coordinates which do not feature a golden ratio form the vertices of a 24-cell. Wikipedia has the same scheme but has an error in the relative scale of these 24 coordinates and the other 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
Example of use and output
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]