<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Python for Everyone!<br/>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.
Step3:  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.
<ASSISTANT_TASK:>
Python Code:
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()
Explanation: Python for Everyone!<br/>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.
End of explanation
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()
Explanation: 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.
End of explanation
<END_TASK>