Shapes with Vpython


Python for Everyone!
Oregon Curriculum Network

VPython inside Jupyter Notebooks

The Vector, Edge and Polyhedron types

The Vector class below is but a thin wrapper around VPython's built-in vector type. One might wonder, why bother? Why not just use vpython.vector and be done with it? Also, if wanting to reimplement, why not just subclass instead of wrap? All good questions.

A primary motivation is to keep the Vector and Edge types somewhat aloof from vpython's vector and more welded to vpython's cylinder instead. We want vectors and edges to materialize as cylinders quite easily.

So whether we subclass, or wrap, we want our vectors to have the ability to self-draw.

The three basis vectors must be negated to give all six spokes of the XYZ apparatus. Here's an opportunity to test our __neg__ operator then.

The overall plan is to have an XYZ "jack" floating in space, around which two tetrahedrons will be drawn, with a common center, as twins.

Their edges will intersect as at the respective face centers of the six-faced, twelve-edged hexahedron, our "duo-tet" cube (implied, but could be hard-wired as a next Polyhedron instance, just give it the six faces).

A lot of this wrapper code is about turning vpython.vectors into lists for feeding to Vector, which expects three separate arguments. A star in front of an iterable accomplishes the feat of exploding it into the separate arguments required.

Note that vector operations, including negation, always return fresh vectors. Even color has not been made a mutable property, but maybe could be.


In [1]:
from vpython import *

class Vector:
    
    def __init__(self, x, y, z):
        self.v = vector(x, y, z)
        
    def __add__(self, other):
        v_sum = self.v + other.v
        return Vector(*v_sum.value)
    
    def __neg__(self):
        return Vector(*((-self.v).value))
    
    def __sub__(self, other):
        V = (self + (-other))
        return Vector(*V.v.value)
                      
    def __mul__(self, scalar):
        V = scalar * self.v
        return Vector(*V.value)
    
    def norm(self):
        v = norm(self.v)
        return Vector(*v.value)
    
    def length(self):
        return mag(self.v)
    
    def draw(self):
        self.the_cyl = cylinder(pos=vector(0,0,0), axis=self.v, radius=0.1)
        self.the_cyl.color = color.cyan
        
XBASIS = Vector(1,0,0)
YBASIS = Vector(0,1,0)
ZBASIS = Vector(0,0,1)
XNEG = -XBASIS
YNEG = -YBASIS
ZNEG = -ZBASIS
XYZ = [XBASIS, XNEG, YBASIS, YNEG, ZBASIS, ZNEG]

sphere(pos=vector(0,0,0), color = color.orange, radius=0.2)
for radial in XYZ:
    radial.draw()


Even though the top code cell contains no instructions to draw, Vpython's way of integrating into Jupyter Notebook seems to be by adding a scene right after the first code cell. Look below for the code that made all of the above happen. Yes, that's a bit strange.


In [2]:
class Edge:
    
    def __init__(self, v0, v1):
        self.v0 = v0
        self.v1 = v1
        
    def draw(self):
        """cylinder wants a starting point, and a direction vector"""
        pointer = (self.v1 - self.v0)
        direction_v = norm(pointer) * pointer.length() # normalize then stretch
        self.the_cyl = cylinder(pos = self.v0.v, axis=direction_v.v, radius=0.1)
        self.the_cyl.color = color.green
        
class Polyhedron:
    
    def __init__(self, faces, corners):
        self.faces = faces
        self.corners = corners
        self.edges = self._get_edges()
        
    def _get_edges(self):
        """
        take a list of face-tuples and distill
        all the unique edges,
        e.g. ((1,2,3)) => ((1,2),(2,3),(1,3))
        e.g. icosahedron has 20 faces and 30 unique edges
        ( = cubocta 24 + tetra's 6 edges to squares per
        jitterbug)
        """
        uniqueset = set()
        for f in self.faces:
            edgetries = zip(f, f[1:]+ (f[0],))
            for e in edgetries:
                e = tuple(sorted(e)) # keeps out dupes
                uniqueset.add(e)                
        return tuple(uniqueset)
    
    def draw(self):
        for edge in self.edges:
            the_edge = Edge(Vector(*self.corners[edge[0]]), 
                            Vector(*self.corners[edge[1]]))
            the_edge.draw()

the_verts = \
{ 'A': (0.35355339059327373, 0.35355339059327373, 0.35355339059327373),
  'B': (-0.35355339059327373, -0.35355339059327373, 0.35355339059327373),
  'C': (-0.35355339059327373, 0.35355339059327373, -0.35355339059327373),
  'D': (0.35355339059327373, -0.35355339059327373, -0.35355339059327373),
  'E': (-0.35355339059327373, -0.35355339059327373, -0.35355339059327373),
  'F': (0.35355339059327373, 0.35355339059327373, -0.35355339059327373),
  'G': (0.35355339059327373, -0.35355339059327373, 0.35355339059327373),
  'H': (-0.35355339059327373, 0.35355339059327373, 0.35355339059327373)} 

the_faces = (('A','B','C'),('A','C','D'),('A','D','B'),('B','C','D'))

other_faces = (('E','F','G'), ('E','G','H'),('E','H','F'),('F','G','H'))
tetrahedron = Polyhedron(the_faces, the_verts)
inv_tetrahedron = Polyhedron(other_faces, the_verts)

print(tetrahedron._get_edges())
print(inv_tetrahedron._get_edges())

tetrahedron.draw()
inv_tetrahedron.draw()


(('B', 'C'), ('A', 'D'), ('A', 'B'), ('A', 'C'), ('B', 'D'), ('C', 'D'))
(('F', 'G'), ('G', 'H'), ('E', 'H'), ('F', 'H'), ('E', 'G'), ('E', 'F'))

The code above shows how we might capture an Edge as the endpoints of two Vectors, setting the stage for a Polyhedron as a set of such edges. These edges are derived from faces, which are simply clockwise or counterclockwise circuits of named vertices.

Pass in a dict of vertices or corners you'll need, named by letter, along with the tuple of faces, and you're set. The Polyhedron will distill the edges for you, and render them as vpython.cylinder objects.

Remember to scroll up, to the scene right after the first code cell, to find the actual output of the preceding code cell.

At Oregon Curriculum Network (OCN) you will find material on Quadrays, oft used to generate 26 points of interest A-Z, the A-H above the beginning of the sequence. From the duo-tet cube we move to its dual, the octahedron, and then the 12 vertices of the cuboctahedron. 8 + 6 + 12 = 26.

When studying Synergetics (a namespace) you will encounter canonical volume numbers for these as well: (Tetrahedron: 1, Cube: 3, Octahedron: 4, Rhombic Dodecahedron 6, Cuboctahedron 20).

For Further Reading:

Polyhedrons 101
STEM Mathematics -- with nbviewer