#!/usr/bin/env python from math import cos, pi, sqrt import random from numpy import zeros from scipy.linalg import lstsq from scipy.optimize import fmin_bfgs from visual import * class Polygon(object): """A collection of vertices in cyclic order, meant to lie in a plane. The vertices can be any objects, as long as they are hashable and sortable. A good choice is one-letter strings like 'A' or 'B'. >>> pentagon = Polygon('ACBED') >>> pentagon.vertices ('A', 'C', 'B', 'E', 'D') >>> str(pentagon) 'ACBED' >>> pentagon.n() 5 >>> pentagon.edges() [('A', 'C'), ('C', 'B'), ('B', 'E'), ('E', 'D'), ('D', 'A')] >>> pentagon.edges(ordered=False) [('A', 'C'), ('B', 'C'), ('B', 'E'), ('D', 'E'), ('A', 'D')] """ def __init__(self, vertices): """Constructor. vertices: any sequence of objects. For example, a string gives one vertex for each letter. """ self.vertices = tuple(vertices) names = [str(v) for v in vertices] is_all_letters = all([len(name) == 1 for name in names]) separator = '' if is_all_letters else ',' self.name = separator.join(names) def __str__(self): """Like ABC for a triangle, or with commas if not single letters.""" return self.name def n(self): """Return the number of vertices.""" return len(self.vertices) def edges(self, ordered=True): """Return a list of pairs of vertices, in cyclic order. ordered: If false, we flip the two vertices in each pair if necessary so the two vertices are sorted. """ edges = [] n = self.n() for i in xrange(n): j = i+1 if i+1 < n else 0 v, w = self.vertices[i], self.vertices[j] if not ordered and v > w: v, w = w, v edges.append((v, w)) return edges def __cmp__(self, other): """Sort Polygons in alphabetical order by vertices. >>> 'ABE' < 'ACD' # ABE comes before ACD in the dictionary True >>> tri1 = Polygon(['A', 'B', 'E']) >>> tri2 = Polygon(['A', 'C', 'D']) >>> tri1 < tri2 True """ return cmp(self.vertices, other.vertices) def __hash__(self): return hash(self.vertices) def volume_elements(self, orien): """Return a list of VolumeElts. orien: +1 or -1, showing whether this face is counter-clockwise or clockwise in the polyhedron as a whole.""" ans = [] a = self.vertices[0] for i in xrange(1, self.n() - 1): b = self.vertices[i] c = self.vertices[i+1] if orien == +1: ans.append(VolumeElt(a, b, c)) elif orien == -1: ans.append(VolumeElt(a, c, b)) else: raise ValueError, 'orien = %s but should be +1 or -1' % orien return ans class Spring(object): """A Spring goes between two vertices and wants to have a given length.""" def __init__(self, v, w, length): """Construct a Spring between vertices v and w of the given length.""" self.v = v self.w = w self.length = length def potential(self, vx, vy, vz, wx, wy, wz): """Return the potential energy when v and w have the given coords.""" dist_v_w = sqrt((vx-wx)**2 + (vy-wy)**2 + (vz-wz)**2) return (dist_v_w - self.length) ** 2 def pot_deriv(self, var, vx, vy, vz, wx, wy, wz): """Return the partial derivative of potential(...), d/d_var. var: a string 'vx', 'vy', ..., 'wz' naming one of the six variables. """ dist_v_w = sqrt((vx-wx)**2 + (vy-wy)**2 + (vz-wz)**2) ans = 2 * (dist_v_w - self.length) / dist_v_w if var[1] == 'x': ans *= (vx - wx) elif var[1] == 'y': ans *= (vy - wy) elif var[1] == 'z': ans *= (vz - wz) else: raise ValueError, 'Unrecognized var: %s' % var if var[0] == 'v': pass # ans *= 1 elif var[0] == 'w': ans = -ans else: raise ValueError, 'Unrecognized var: %s' % var return ans class VolumeElt(object): """A parallelepiped starting at the origin spanned by three vertices.""" def __init__(self, a, b, c): self.a = a self.b = b self.c = c def volume(self, ax, ay, az, bx, by, bz, cx, cy, cz): """Return the determinant ax ay az bx by bz cx cy cz """ return ax*by*cz + ay*bz*cx + az*bx*cy - az*by*cx - ay*bx*cz - ax*bz*cy def vol_deriv(self, var, ax, ay, az, bx, by, bz, cx, cy, cz): """Return the partial derivative of volumn(...), d/d_var. var: a string 'ax', ..., 'cz' naming one of the nine variables. """ if var == 'ax': return by*cz - bz*cy if var == 'ay': return bz*cx - bx*cz if var == 'az': return bx*cy - by*cx if var == 'bx': return az*cy - ay*cz if var == 'by': return ax*cz - az*cx if var == 'bz': return ay*cx - ax*cy if var == 'cx': return ay*bz - az*by if var == 'cy': return az*bx - ax*bz if var == 'cz': return ax*by - ay*bx else: raise ValueError, 'Unrecognized var: %s' % var class RegularPolygon(Polygon): """A Polygon meant to have all sides and all interior angles equal. Here is what 'meant to' means. A Polygon or RegularPolygon can have its vertices anywhere in space. However, the spring and volume element code will attempt to make it regular--to move the vertices so that a RegularPolygon lies in a plane, the length of each side is RegularPolygon.SIDE, and angles between edges are equal. Methods named 'target' refer to properties the RegularPolygon will have after it has been made regular.""" SIDE = 1.0 def target_radius(self): """Return the radius of the circumscribed circle.""" theta = 2 * pi / self.n() return RegularPolygon.SIDE / sqrt(2 * (1 - cos(theta))) def target_area(self): """Return the area.""" ## Imagine the isosceles triangle AOB where AB is one side and ## OA, OB are radii. Let C be the midpoint of AB. Then OC is ## the altitude of triangle AOB. OA = self.target_radius() AB = RegularPolygon.SIDE AC = AB / 2 OC = sqrt(OA ** 2 - AC ** 2) areaAOB = 0.5 * AB * OC return areaAOB * self.n() def target_diagonal(self, i): """Return the length of the diagonal spanning i edges. Examples: if i is 1 or n-1, return the side length. If n is even and i is n/2, return the diameter. If i is 0 or n, return 0.""" theta = 2 * pi * i / self.n() r = self.target_radius() return r * sqrt(2 * (1 - cos(theta))) def outer_springs(self): """Return a dictionary of Springs around the outside edges. The dictionary has a key (v, w) representing the edge between vertices v and w, with v < w. The value for this key is the corresponding Spring.""" ans = {} for v, w in self.edges(ordered=False): ans[(v, w)] = Spring(v, w, RegularPolygon.SIDE) return ans def inner_springs(self): """Return a list of Springs to make the polygon flat and rigid. outer_springs() will provide the Springs for the edges. The method here provides the Springs along the internal diagonals.""" n = self.n() ans = [] ## Add Springs along the diagonals from vertex 0 to other ## vertices. There are n-3 of these. They divide the polygon ## into triangles. v0 = self.vertices[0] for i in xrange(2, n-1): ans.append(Spring(v0, self.vertices[i], self.target_diagonal(i))) ## Although the polygon is divided into triangles, it is still ## not rigid. It can fold along the edges between the ## triangles. Let's add a second set of n-3 diagonals coming ## out of vertex 1. v1 = self.vertices[1] for i in xrange(3, n): ans.append(Spring(v1, self.vertices[i], self.target_diagonal(i-1))) return ans class Polyhedron(object): """A collection of Polygons, plus how they fit together, and xyz coords. >>> tetrahedron = Polyhedron('ABC', 'BCD', 'ACD', 'ABD') >>> sorted(str(f) for f in tetrahedron.faces) ['ABC', 'ABD', 'ACD', 'BCD'] >>> tetrahedron.vertices ['A', 'B', 'C', 'D'] >>> [tetrahedron.vertex_number[v] for v in ['A', 'B', 'C', 'D']] [0, 1, 2, 3] >>> tetrahedron.edges # pairs (v, w) with v < w; then sort the pairs [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')] >>> tetrahedron.V() 4 >>> tetrahedron.E() 6 >>> tetrahedron.F() 4 >>> tetrahedron.euler_characteristic() 2 >>> triangle = tetrahedron.faces[0] >>> s = triangle.SIDE >>> r = triangle.target_radius() >>> def approx(x, y): # are x and y approximately equal? ... return abs(x - y) < 0.0000001 >>> approx(r, s/sqrt(3)) True >>> approx(tetrahedron.target_surface_area, sqrt(3) * s**2) True >>> tetrahedron.orientable True >>> [tetrahedron.face_orientation[f] for f in tetrahedron.faces] [1, -1, 1, -1] """ SPHERE_COLOR = color.green SPHERE_RADIUS = 0.1 * RegularPolygon.SIDE CYLINDER_RADIUS = 0.3 * SPHERE_RADIUS CYLINDER_COLOR = color.red DISPLAY_RATE = 24 # This way, the animation will not be jerky def __init__(self, *args): """Constructor. args: Either Polygons or strings. A string is converted to a RegularPolygon with one vertex for each letter.""" self.faces = [] for f in args: if isinstance(f, Polygon): self.faces.append(f) elif isinstance(f, str): self.faces.append(RegularPolygon(f)) else: raise TypeError, 'Not a Polygon or string: %s' % f vertex_set = set() for f in self.faces: for v in f.vertices: vertex_set.add(v) self.vertices = sorted(vertex_set) self.vertex_number = {} for i, v in enumerate(self.vertices): self.vertex_number[v] = i self.springs = [] outer_springs = {} for f in self.faces: outer_springs.update(f.outer_springs()) self.springs.extend(f.inner_springs()) self.edges = sorted(outer_springs.iterkeys()) self.springs.extend(outer_springs.itervalues()) self.target_surface_area = 0 for f in self.faces: self.target_surface_area += f.target_area() self.orientable = True self.face_orientation = {self.faces[0] : +1} self.edges_to_faces = {} edges_to_local_orien = {} for f in self.faces: for e in f.edges(ordered=False): if e not in self.edges_to_faces: self.edges_to_faces[e] = [] ff = self.edges_to_faces[e] ff.append(f) assert len(ff) <= 2, 'Edge %s meets %d faces.' % (e, len(ff)) if len(ff) == 2: orien0 = +1 if e in ff[0].edges(ordered=True) else -1 orien1 = +1 if e in ff[1].edges(ordered=True) else -1 edges_to_local_orien[e] = -1 * orien0 * orien1 while self.orientable and len(self.face_orientation) < self.F(): for e, ff in self.edges_to_faces.iteritems(): if len(ff) == 2: orien = edges_to_local_orien[e] forien0 = self.face_orientation.get(ff[0], None) forien1 = self.face_orientation.get(ff[1], None) if forien0 is not None: if forien1 is not None: if forien0 != forien1 * orien: self.orientable = False self.face_orientation = None break else: self.face_orientation[ff[1]] = forien0 * orien elif forien1 is not None: self.face_orientation[ff[0]] = forien1 * orien self.vol_elts = [] if self.orientable: for f in self.faces: orien = self.face_orientation[f] self.vol_elts.extend(f.volume_elements(orien)) self.spheres = [] V = self.V() randsize = 2 * (V ** (1.0/3.0)) * RegularPolygon.SIDE center = vector(0, 0, 0) for v in self.vertices: sph = sphere(radius=self.SPHERE_RADIUS, color=self.SPHERE_COLOR, visible=False) self.spheres.append(sph) sph.pos = vector(randsize * random.random(), randsize * random.random(), randsize * random.random()) center += sph.pos center /= self.V() for sph in self.spheres: sph.pos -= center self.labels = [] for v, sph in zip(self.vertices, self.spheres): lab = label(text=str(v), xoffset=0, yoffset=0, line=False, box=False, font='serif', visible=False) self.labels.append(lab) self.cylinders = [] for v, w in self.edges: cyl = cylinder(radius=self.CYLINDER_RADIUS, color=self.CYLINDER_COLOR, visible=False) self.cylinders.append(cyl) def V(self): """Return the number of vertices.""" return len(self.vertices) def E(self): """Return the number of edges.""" return len(self.edges) def F(self): """Return the number of faces.""" return len(self.faces) def euler_characteristic(self): """Return the Euler characteristic. Should be 2 for a closed polyhedron, 1 for a net, k for a net in k disconnected pieces, negative for a net where missing faces make lots of holes, etc.""" return self.V() - self.E() + self.F() def center(self): """Return the average xyz of the vertices, as a vector.""" cen = vector(0, 0, 0) for sph in self.spheres: cen += sph.pos return cen / self.V() def spring_potential(self, xx): ans = 0.0 for sp in self.springs: i = self.vertex_number[sp.v] j = self.vertex_number[sp.w] ans += sp.potential(xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) return ans def spring_potential_gradient(self, xx): ans = zeros(3 * self.V()) for sp in self.springs: i = self.vertex_number[sp.v] j = self.vertex_number[sp.w] ans[3*i] += sp.pot_deriv('vx', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) ans[3*i+1] += sp.pot_deriv('vy', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) ans[3*i+2] += sp.pot_deriv('vz', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) ans[3*j] += sp.pot_deriv('wx', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) ans[3*j+1] += sp.pot_deriv('wy', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) ans[3*j+2] += sp.pot_deriv('wz', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2]) return ans def volume(self, xx): ans = 0.0 for elt in self.vol_elts: i = self.vertex_number[elt.a] j = self.vertex_number[elt.b] k = self.vertex_number[elt.c] ans += elt.volume(xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) return ans def volume_gradient(self, xx): ans = zeros(3 * self.V()) for elt in self.vol_elts: i = self.vertex_number[elt.a] j = self.vertex_number[elt.b] k = self.vertex_number[elt.c] ans[3*i] += elt.vol_deriv('ax', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*i+1] += elt.vol_deriv('ay', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*i+2] += elt.vol_deriv('az', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*j] += elt.vol_deriv('bx', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*j+1] += elt.vol_deriv('by', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*j+2] += elt.vol_deriv('bz', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*k] += elt.vol_deriv('cx', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*k+1] += elt.vol_deriv('cy', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) ans[3*k+2] += elt.vol_deriv('cz', xx[3*i], xx[3*i+1], xx[3*i+2], xx[3*j], xx[3*j+1], xx[3*j+2], xx[3*k], xx[3*k+1], xx[3*k+2]) return ans def inv_abs_volume(self, xx): vol = self.volume(xx) return 1 / abs(vol) def inv_abs_volume_gradient(self, xx): vol = self.volume(xx) ans = self.volume_gradient(xx) ans *= -1 / (vol ** 2) if vol < 0: ans *= -1 return ans def embed(self, animate=True, nRT=5000, use_volume=True): """Minimize the potential and update the display. animate: If true, update the display every time the coordinates change during the optimization. use_volume: If true, minimize (spring potential) + nRT/volume. If false, minimize only the spring potential.""" xx = zeros(3 * self.V()) for i, sph in enumerate(self.spheres): xx[3*i] = sph.pos.x xx[3*i+1] = sph.pos.y xx[3*i+2] = sph.pos.z if use_volume: f = lambda aa: self.spring_potential(aa) + nRT * self.inv_abs_volume(aa) fprime = lambda aa: self.spring_potential_gradient(aa) + nRT * self.inv_abs_volume_gradient(aa) else: f = lambda aa: self.spring_potential(aa) fprime = lambda aa: self.spring_potential_gradient(aa) if animate: print 'Computing animation...' last_xx, xx_list = fmin_bfgs(f, xx, fprime, retall=True) else: last_xx = fmin_bfgs(f, xx, fprime, retall=False) xx_list = [last_xx] for sph in self.spheres: if not sph.visible: sph.visible=True for cyl in self.cylinders: if not cyl.visible: cyl.visible=True for lab in self.labels: if not lab.visible: lab.visible=True for xx in xx_list: rate(self.DISPLAY_RATE) for i, (v, sph) in enumerate(zip(self.vertices, self.spheres)): sph.pos = vector(xx[3*i], xx[3*i+1], xx[3*i+2]) self.move_cylinders_and_labels() if animate: print 'Animation complete.' def move_cylinders_and_labels(self): """Reset position (pos) of cyl's and labels after vertices move.""" for (v, w), cyl in zip(self.edges, self.cylinders): i = self.vertex_number[v] j = self.vertex_number[w] sph_v = self.spheres[i] sph_w = self.spheres[j] cyl.pos = sph_v.pos cyl.axis = sph_w.pos - sph_v.pos for sph, lab in zip(self.spheres, self.labels): lab.pos = sph.pos def display(self, animate=True, nRT=5000): """Embed, and listen for the mouse. Drag a vertex to move it. Click on the background to re-embed. Ctrl-click to exit (stop listening for the mouse). animate: Passed to embed().""" pick = None while True: self.embed(animate, nRT, use_volume=True) self.embed(animate, nRT, use_volume=False) while True: if scene.mouse.events: m1 = scene.mouse.getevent() if m1.button == 'left': if m1.drag and isinstance(m1.pick, sphere): pick = m1.pick elif m1.drop and pick: pick.color = self.SPHERE_COLOR scene.cursor.visible = True pick = None elif m1.click and not m1.pick: if m1.ctrl: return else: break if pick: pick.pos = scene.mouse.pos pick.color = color.yellow scene.cursor.visible = False self.move_cylinders_and_labels() def dual(self): """Return a scene displaying the dual of this Polyhedron. The method creates and returns a window with spheres and cylinders. It does not create the dual as a Polyhedron object. Results will be strange if the faces are not flat. On the other hand, self can be either convex or non-convex. Does nothing (returns None) if self is non-orientable.""" if not self.orientable: return None dual_scene = display(title='Dual') dual_spheres = {} dual_cylinders = [] dcyl_length_sum = 0 cen = self.center() for f in self.faces: ## Given the face f, find (hx, hy, hz) with the following ## property: the plane ## ## hx * x + hy * y + hz * z = 1 ## ## contains the (x, y, z) coordinates of every vertex v of ## f. The amazing fact is that, in the dual picture, f's ## dual vertex has coordinates (hx, hy, hz). Our dpos is ## this vector (hx, hy, hz). A = zeros((f.n(), 3)) b = zeros(f.n()) for i, v in enumerate(f.vertices): sph = self.spheres[self.vertex_number[v]] A[i] = sph.pos - cen b[i] = 1.0 dpos = lstsq(A, b, overwrite_a=True, overwrite_b=True)[0] dsph = sphere(display=dual_scene, color=self.SPHERE_COLOR, visible=False, pos=dpos) dual_spheres[f] = dsph for e, ff in self.edges_to_faces.iteritems(): dsph_v = dual_spheres[ff[0]] dsph_w = dual_spheres[ff[1]] dcyl = cylinder(display=dual_scene, color=self.CYLINDER_COLOR, visible=False) dcyl.pos = dsph_v.pos dcyl.axis = dsph_w.pos - dsph_v.pos dcyl_length_sum += dcyl.axis.mag dual_cylinders.append(dcyl) ## The only problem now is that, when the Polyhedron is big, ## the dual is little. Let's compute the average cylinder ## length, then set the radii of the spheres and cylinders ## accordingly. Earlier we set everything to visible=False ## because we didn't know the radii yet. dcyl_length_avg = dcyl_length_sum / len(dual_cylinders) dsphR = 0.1 * dcyl_length_avg # 0.1 as in SPHERE_RADIUS dcylR = 0.3 * dsphR # 0.3 as in CYLINDER_RADIUS for dsph in dual_spheres.itervalues(): dsph.radius = dsphR dsph.visible = True for dcyl in dual_cylinders: dcyl.radius = dcylR dcyl.visible=True return dual_scene def solid(old_display): """Convert a scene displaying spheres to a convex solid. Return a new scene that shows an object of type 'convex'. The vertices of the 'convex' are the centers of the visible spheres in the original picture.""" new_display = display() corners = [] for obj in old_display.objects: if isinstance(obj, sphere): corners.append(obj.pos) convex(display=new_display, pos=corners, material=materials.marble) # Make constant solids # ------------------------------------------------------ # There are 5 platonics # Platonic Solids tetrahedron = Polyhedron('ABC', 'ACD', 'ADB', 'DCB') cube = Polyhedron('ABCD', 'GBAF', 'FADE', 'EDCH', 'HCBG', 'HGFE') octahedron = Polyhedron('ABC', 'EBA', 'FCB', 'DAC', 'EAD', 'FBE', 'DCF', 'FED') dodecahedron = Polyhedron('ABCDE', 'GHIBA', 'IJKCB', 'KLMDC', 'MNOED', 'OFGAE', 'PQHGF', 'QRJIH', 'RSLKJ', 'STNML', 'TPFON', 'TSRQP') icosahedron = Polyhedron('ABC', 'AEB', 'BGC', 'CIA', 'DEA', 'EFB', 'FGB', 'GHC', 'HIC', 'IDA', 'JED', 'JFE', 'KGF', 'KHG', 'LIH', 'LDI', 'JDL', 'KFJ', 'LHK', 'JLK') # ------------------------------------------------------ # There are 13 Archimedean Solids - 5 in the truncated group, # 4 in the cubocta group, and 4 in the icosidodeca group # Archimedean Solids Truncated Group truncated_tetrahedron = Polyhedron('ABC', 'DHIEBA', 'EJKFCB', 'FLGDAC', 'GHD', 'IJE', 'KLF', 'HGLKJI') truncated_cube = Polyhedron('ABCDEFGH', 'IAH', 'JCB', 'KED', 'LGF', 'TNIHGLMS', 'VOJBAINU', 'XPKDCJOW', 'RMLFEKPQ', 'UNT', 'WOV', 'QPX', 'MRS', 'XWVUTSRQ') truncated_octahedron = Polyhedron('ABCDEF', 'AHOPIB', 'BIJC', 'CJQRKD', 'DKLE', 'ELMNGF', 'FGHA', 'VOHGNU', 'WPOV', 'XQJIPW', 'SRQX', 'TMLKRS', 'UNMT', 'XWVUTS') truncated_dodecahedron = Polyhedron('ABCDEFGHIJ', 'WQLAJKPVyz', 'YRMCBLQX12', 'aSNEDMRZ34', 'cTOGFNSb56', 'UPKIHOTd78', 'slg1XWzfkr', 'umh3ZY2glt', 'wni5ba4hmv', 'oje7dc6inx', 'qkfyVU8ejp', 'xwvutsrqpo', 'LBA', 'MDC', 'NFE', 'OHG', 'KJI', 'WXQ', 'YZR', 'abS', 'cdT', 'UVP', 'fzy', 'g21', 'h43', 'i65', 'e87', 'qrk', 'stl', 'uvm', 'wxn', 'opj') truncated_icosahedron = Polyhedron('ABCDEF', 'AGMNHB', 'CIPQJD', 'EKSTLF', 'GUmVWM', 'NYZaOH', 'IOabcP', 'QefgRJ', 'KRghiS', 'TklmUL', 'opXWVn', 'rZYXpq', 'tudcbs', 'wfeduv', 'yzjihx', '2lkjz1', '8yxwv7', '6tsrq5', '4on213', '876543', 'FLUGA', 'BHOIC', 'DJRKE', 'MWXYN', 'PcdeQ', 'SijkT', 'nVml2', 'sbaZr', 'xhgfw', '31zy8', '7vut6', '5qpo4') # It's time to play soccer! You know why? Make sure the nRT (line # 558) is 5000 # and display the truncated icosahedron. You get a soccer ball. # Archimedean Solids Cubocta Group cuboctahedron = Polyhedron('ABCD', 'ABE', 'BCF', 'CDG', 'DAH', 'AELH', 'BFIE', 'CGJF', 'DHKG', 'LIE', 'IJF', 'JKG', 'KLH', 'IJKL') small_rhombicuboctahedron = Polyhedron('ABCD', 'FGA', 'GHBA', 'HIB', 'IJCB', 'JKC', 'KLDC', 'LED', 'EFAD', 'PQHG', 'QRIH', 'RSJI', 'STKJ', 'TMLK', 'MNEL', 'NOFE', 'OPGF', 'WPO', 'WXQP', 'XRQ', 'XUSR', 'UTS', 'UVMT', 'VNM', 'VWON', 'XWVU') great_rhombicuboctahedron = Polyhedron('ABCDEFGH', 'lmnYJIXW', 'ZabcNMLK', 'defgRQPO', 'hijkVUTS', 'vutsrqpo', 'AIJKLB', 'CMNOPD', 'EQRSTF', 'GUVWXH', 'YnopaZ', 'cbqred', 'gfstih', 'kjuvml', 'HXIA', 'BLMC', 'DPQE', 'FTUG', 'JYZK', 'NcdO', 'RghS', 'VklW', 'mvon', 'apqb', 'ersf', 'ituj') # Doesn't matter which "hand" pops up! snub_cube = Polyhedron('ABCD', 'GPQH', 'IRSJ', 'KTML', 'ENOF', 'XWVU', 'BHI', 'CJK', 'DLE', 'AFG', 'SUT', 'MVN', 'OWP', 'QXR', 'AGB', 'HBG', 'BIC', 'JCI', 'CKD', 'LDK', 'DEA', 'FAE', 'GFP', 'OPF', 'IHR', 'QRH', 'KJT', 'STJ', 'ELN', 'MNL', 'XQW', 'PWQ', 'USX', 'RXS', 'VMU', 'TUM', 'WOV', 'NVO') # Archimedean Solids Icosidodeca Group icosidodecahedron = Polyhedron('ABC', 'OHG', 'HPI', 'QKJ', 'KRL', 'MED', 'ENF', 'GAF', 'JBI', 'DCL', 'NUV', 'OVW', 'PXY', 'QYZ', 'RaS', 'MST', 'dXW', 'baZ', 'cUT', 'dcb', 'GHIBA', 'JKLCB', 'DEFAC', 'VOGFN', 'YQJIP', 'SMDLR', 'WXPHO', 'ZaRKQ', 'TUNEM', 'dWVUc', 'cTSab', 'bZYXd') small_rhombicosidodecahedron = Polyhedron('ABCDE', 'dPFOc', 'RSHGQ', 'UVJIT', 'XYLKW', 'abNMZ', 'uvgfe', 'wxjih', 'yzmlk', '12pon', '3tsrq', '87654', 'OFAE', 'GHBA', 'IJCB', 'KLDC', 'MNED', 'PQGF', 'STIH', 'VWKJ', 'YZML', 'bcON', 'efPd', 'fgRQ', 'hiSR', 'ijUT', 'klVU', 'lmXW', 'noYX', 'opaZ', 'qrba', 'rsdc', 'tues', 'vwhg', 'xykj', 'z1nm', '23qp', '45t3', '56vu', '67xw', '78zy', '8421', 'FGA', 'HIB', 'JKC', 'LMD', 'NOE', 'fQP', 'ghR', 'iTS', 'jkU', 'lWV', 'mnX', 'oZY', 'pqa', 'rcb', 'sed', '432', '5ut', '6wv', '7yx', '81z') # This solid has 120 vertices. We need to use arguments to give each one a different number. great_rhombicosidodecahedron = Polyhedron(RegularPolygon((1,2,3,4,5,6,7,8,9,10)), RegularPolygon((11,22,36,37,38,39,40,41,23,12)), RegularPolygon((13,24,42,43,44,45,46,47,25,14)), RegularPolygon((15,26,48,49,50,51,52,53,27,16)), RegularPolygon((17,28,54,55,56,57,58,59,29,18)), RegularPolygon((19,30,60,31,32,33,34,35,21,20)), RegularPolygon((108,109,89,78,77,76,75,88,106,107)), RegularPolygon((104,105,87,74,73,72,71,86,102,103)), RegularPolygon((100,101,85,70,69,68,67,84,98,99)), RegularPolygon((96,97,83,66,65,64,63,82,94,95)), RegularPolygon((92,93,81,62,61,80,79,90,110,91)), RegularPolygon((120,119,118,117,116,115,114,113,112,111)), RegularPolygon((1,12,23,24,13,2)),RegularPolygon((3,14,25,26,15,4)), RegularPolygon((5,16,27,28,17,6)),RegularPolygon((7,18,29,30,19,8)), RegularPolygon((9,20,21,22,11,10)),RegularPolygon((32,62,81,82,63,33)), RegularPolygon((35,34,64,65,37,36)),RegularPolygon((38,66,83,84,67,39)), RegularPolygon((41,40,68,69,43,42)),RegularPolygon((44,70,85,86,71,45)), RegularPolygon((47,46,72,73,49,48)),RegularPolygon((50,74,87,88,75,51)), RegularPolygon((53,52,76,77,55,54)),RegularPolygon((56,78,89,90,79,57)), RegularPolygon((59,58,80,61,31,60)),RegularPolygon((111,91,110,109,108,120)), RegularPolygon((119,107,106,105,104,118)),RegularPolygon((117,103,102,101,100,116)), RegularPolygon((115,99,98,97,96,114)),RegularPolygon((113,95,94,93,92,112)), RegularPolygon((10,11,12,1)),RegularPolygon((2,13,14,3)),RegularPolygon((4,15,16,5)), RegularPolygon((6,17,18,7)),RegularPolygon((8,19,20,9)),RegularPolygon((21,35,36,22)), RegularPolygon((23,41,42,24)),RegularPolygon((25,47,48,26)),RegularPolygon((27,53,54,28)), RegularPolygon((29,59,60,30)),RegularPolygon((31,61,62,32)),RegularPolygon((33,63,64,34)), RegularPolygon((37,65,66,38)),RegularPolygon((39,67,68,40)),RegularPolygon((43,69,70,44)), RegularPolygon((45,71,72,46)),RegularPolygon((49,73,74,50)),RegularPolygon((51,75,76,52)), RegularPolygon((55,77,78,56)),RegularPolygon((57,79,80,58)),RegularPolygon((81,93,94,82)), RegularPolygon((83,97,98,84)),RegularPolygon((85,101,102,86)),RegularPolygon((87,105,106,88)), RegularPolygon((89,109,110,90)),RegularPolygon((120,108,107,119)),RegularPolygon((118,104,103,117)), RegularPolygon((116,100,99,115)),RegularPolygon((114,96,95,113)),RegularPolygon((112,92,91,111))) # Eighty triangles! This is like a triangular celebration. snub_dodecahedron = Polyhedron('ABCDE', 'GRSTH', 'IUVWJ', 'KXYZL', 'MabcN', 'OdPQF', 'vhgfu', 'tesr3', '2qpo1', 'znmly', 'xkjiw', '87654', 'GAF', 'OEN', 'MDL', 'KCJ', 'IBH', 'QgR', 'iSh', 'TjU', 'lVk', 'WmX', 'oYn', 'Zpa', 'rbq', 'csd', 'fPe', 't4u', 'v5w', 'x6y', 'z71', '283', 'OFE', 'AEF', 'GHA', 'BAH', 'IJB', 'CBJ', 'KLC', 'DCL', 'MND', 'EDN', 'RGQ', 'FQG', 'UIT', 'HTI', 'XKW', 'JWK', 'aMZ', 'LZM', 'dOc', 'NcO', 'fgP', 'QPg', 'ghR', 'SRh', 'ijS', 'TSj', 'jkU', 'VUk', 'lmV', 'WVm', 'mnX', 'YXn', 'opY', 'ZYp', 'pqa', 'baq', 'rsb', 'cbs', 'sed', 'Pde', 'uft', 'etf', 'wiv', 'hvi', 'ylx', 'kxl', '1oz', 'nzo', '3r2', 'q2r', '45u', 'vu5', '56w', 'xw6', '67y', 'zy7', '781', '218', '843', 't34') # If you want any one of the Archimedean Solids to show up, and you are not sure which to pick, # use this random_archimedean_solid function! import random def random_archimedean_solid(): """Python picks any one of the 13 Archimedean Solids and automatically displays it on the black screen.""" print 'Python is picking any one of the 13 Archimedean Solids...' solid = random.randrange(0, 13) if solid == 0: print 'Python picked the truncated tetrahedron (3,6,6).' truncated_tetrahedron.display(nRT=1000) elif solid == 1: print 'Python picked the truncated cube (3,8,8).' truncated_cube.display(nRT=7000) elif solid == 2: print 'Python picked the truncated octahedron (4,6,6). No space-filling' print 'memories!' truncated_octahedron.display(nRT=2000) elif solid == 3: print 'Python picked the cuboctahedron (3,4,3,4).' cuboctahedron.display(nRT=1000) elif solid == 4: print 'Python picked the small rhombicuboctahedron (3,4,4,4).' small_rhombicuboctahedron.display(nRT=1000) elif solid == 5: print 'Python picked the great rhombicuboctahedron (4,6,8).' great_rhombicuboctahedron.display(nRT=7000) elif solid == 6: print 'Python picked the snub cube (3,3,3,3,4). It is not aware' print 'of which chiral form shows up.' snub_cube.display(nRT=1000) elif solid == 7: print 'Python picked the truncated dodecahedron (3,10,10).' truncated_dodecahedron.display(nRT=10000) elif solid == 8: print 'Python picked the truncated icosahedron (5,6,6). Sorry, no soccer!' truncated_icosahedron.display(nRT=5000) elif solid == 9: print 'Python picked the icosidodecahedron (3,5,3,5).' icosidodecahedron.display(nRT=1000) elif solid == 10: print 'Python picked the small rhombicosidodecahedron (3,4,5,4).' small_rhombicosidodecahedron.display(nRT=2000) elif solid == 11: print 'Python picked the great rhombicosidodecahedron (4,6,10).' great_rhombicosidodecahedron.display(nRT=12000) elif solid == 12: print 'Python picked the snub dodecahedron (3,3,3,3,5). It is not aware' print 'of which chiral form shows up.' snub_dodecahedron.display(nRT=2000) def solid_of_the_day(): tr4 = 0 tr6 = 0 tr8 = 0 tr12 = 0 tr20 = 0 hedron608 = 0 rhhedron608 = 0 trhedron608 = 0 sn6 = 0 hedron1220 = 0 rhhedron1220 = 0 trhedron1220 = 0 sn12 = 0 total = 0 print 'We will have Python pick a random Archimedean solid 50 times, keeping track of the picks.' print 'Then we will list the number of times each solid we pick.' print 'Out of the one(s) which were picked "most often" the first is chosen the solid' print 'of the day!' for i in xrange(50): # Python picks a solid pick = random.randrange(0, 13) if pick == 0: tr4 += 1 elif pick == 1: tr6 += 1 elif pick == 2: tr8 += 1 elif pick == 3: tr12 += 1 elif pick == 4: tr20 += 1 elif pick == 5: hedron608 += 1 elif pick == 6: rhhedron608 += 1 elif pick == 7: trhedron608 += 1 elif pick == 8: sn6 += 1 elif pick == 9: hedron1220 += 1 elif pick == 10: rhhedron1220 += 1 elif pick == 11: trhedron1220 += 1 elif pick == 12: sn12 += 1 total += 1 print 'Truncated Tetrahedron: %d pick(s)' % (tr4) print 'Truncated Cube: %d pick(s)' % (tr6) print 'Truncated Octahedron: %d pick(s)' % (tr8) print 'Cuboctahedron: %d pick(s)' % (hedron608) print 'Small Rhombicuboctahedron: %d pick(s)' % (rhhedron608) print 'Great Rhombicuboctahedron: %d pick(s)' % (trhedron608) print 'Snub Cube: %d pick(s)' % (sn6) print 'Truncated Dodecahedron: %d pick(s)' % (tr12) print 'Truncated Icosahedron: %d pick(s)' % (tr20) print 'Icosidodecahedron: %d pick(s)' % (hedron1220) print 'Small Rhombicosidodecahedron: %d pick(s)' % (rhhedron1220) print 'Great Rhombicosidodecahedron: %d pick(s)' % (trhedron1220) print 'Snub Dodecahedron: %d pick(s)' % (sn12) print 'Total: %d picks' % (total) # Now let pick the solid of the day! Go through and get the most-often amount # Each time it's too small adjust it up. Then it won't be too small in any way moa = 0 # Most-Often Amount if moa < tr4: moa = tr4 if moa < tr6: moa = tr6 if moa < tr8: moa = tr8 if moa < tr12: moa = tr12 if moa < tr20: moa = tr20 if moa < hedron608: moa = hedron608 if moa < rhhedron608: moa = rhhedron608 if moa < trhedron608: moa = trhedron608 if moa < sn6: moa = sn6 if moa < hedron1220: moa = hedron1220 if moa < rhhedron1220: moa = rhhedron1220 if moa < trhedron1220: moa = trhedron1220 if moa < sn12: moa = sn12 chosen = 0 if tr4 == moa and chosen == 0: # If the truncated tetrahedron has the most-often amount and the amount isn't yet considered print 'Truncated Tetrahedron is the chosen solid of the day!' truncated_tetrahedron.display(nRT=1000) chosen = 1 if tr6 == moa and chosen == 0: print 'Truncated Cube is the chosen solid of the day!' truncated_cube.display(nRT=7000) chosen = 1 if tr8 == moa and chosen == 0: print 'Truncated Octahedron is the chosen solid of the day!' truncated_octahedron.display(nRT=2000) chosen = 1 if hedron608 == moa and chosen == 0: print 'Cuboctahedron is the chosen solid of the day!' cuboctahedron.display(nRT=1000) chosen = 1 if rhhedron608 == moa and chosen == 0: print 'Small Rhombicuboctahedron is the chosen solid of the day!' small_rhombicuboctahedron.display(nRT=1000) chosen = 1 if trhedron608 == moa and chosen == 0: print 'Great Rhombicuboctahedron is the chosen solid of the day!' great_rhombicuboctahedron.display(nRT=7000) chosen = 1 if sn6 == moa and chosen == 0: print 'Snub Cube is then chosen solid of the day!' snub_cube.display(nRT=1000) chosen = 1 if tr12 == moa and chosen == 0: print 'Truncated Dodecahedron is the chosen solid of the day!' truncated_dodecahedron.display(nRT=10000) chosen = 1 if tr20 == moa and chosen == 0: print 'Truncated Icosahedron is the chosen solid of the day!' truncated_icosahedron.display(nRT=5000) chosen = 1 if hedron1220 == moa and chosen == 0: print 'Icosidodecahedron is the chosen solid of the day!' icosidodecahedron.display(nRT=1000) chosen = 1 if rhhedron1220 == moa and chosen == 0: print 'Small Rhombicosidodecahedron is the chosen solid of the day!' small_rhombicosidodecahedron.display(nRT=2000) chosen = 1 if trhedron1220 == moa and chosen == 0: print 'Great Rhombicosidodecahedron is the chosen solid of the day!' great_rhombicosidodecahedron.display(nRT=12000) chosen = 1 if sn12 == moa and chosen == 0: print 'Snub Dodecahedron is the chosen solid of the day!' snub_dodecahedron.display(nRT=2000) chosen = 1 def random_platonic_solid(): import random solidi = random.randrange(0, 5) if solidi == 0: tetrahedron.display(nRT=1000) elif solidi == 1: cube.display(nRT=1000) elif solidi == 2: octahedron.display(nRT=1000) elif solidi == 3: dodecahedron.display(nRT=1000) elif solidi == 4: icosahedron.display(nRT=1000) def platonic_solids_elements(): print 'Air comes from octahedra.' octahedron.display(nRT=1000) print 'Water comes from icosahedra.' icosahedron.display(nRT=1000) print 'The dodecahedron is represented as the whole universe.' dodecahedron.display(nRT=1000) print 'Fire comes from tetrahedra.' tetrahedron.display(nRT=1000) print 'Earth comes from cubes.' cube.display(nRT=1000) print "How do you like Kepler's theorem?" # ------------------------------------------------------ pseudo_rhombicuboctahedron = Polyhedron('ABCD', 'FGA', 'GHBA', 'HIB', 'IJCB', 'JKC', 'KLDC', 'LED', 'EFAD', 'PQHG', 'QRIH', 'RSJI', 'STKJ', 'TMLK', 'MNEL', 'NOFE', 'OPGF', 'WQP', 'WXRQ', 'XSR', 'XUTS', 'UMT', 'UVNM', 'VON', 'VWPO', 'XWVU') triaugmented_hexagonal_prism = Polyhedron('ABCDEF', 'ONMLKJ', 'BCON', 'DEKJ', 'FAML', 'HBA', 'HAM', 'HMN', 'HNB', 'IDC', 'ICO', 'IOJ', 'IJD', 'GFE', 'GEK', 'GKL', 'GLF') if __name__ == '__main__': import doctest doctest.testmod()