Drawing Heptagons Using Integers

Would it surprise you to learn that the two figures below were completely generated by manipulating integers?

OK, to be entirely accurate, at the very end of the computation, when it is necessary to provide floating point values to the graphics library for drawing, there are exactly four irrational numbers used. Up until that time, every point being manipulated is represented by a six-tuple of integers.

Perhaps that does not surprise you too much. Consider these additional facts:

  • Every intersection point you can see in the figures can be represented in the same way, as a six-tuple of integers.
  • The seven-fold rotation used in the computation is encoded using only integers.
  • These facts would all remain true no matter many times the nested heptagons or the spiral were iterated inward (scaling down) or outward (scaling up).

Now you should be surprised, unless you already know a fair bit of college-level or even graduate-level mathematics. This article, and two successive articles, will explain how it all works, and provide Python code to let you reproduce the figures or just play. The math you need to already know is easy -- a little middle school algebra and geometry.

Along they way, you'll get a few pointers into the relevant parts of college-level mathematics, in case you want to dig deeper. I hope that, like me, you'll find some joy and beauty in the mathematics that was never really evident when you learned it in school.

Part 1: Heptagon Numbers

Peter Steinbach first described how each regular polygon generates a system of numbers, self-contained under arithmetic, based on the relative lengths of its diagonals (the lines crossing the interior of the polygon). I first learned about this in an excellent paper by Kappraff. In the case of the heptagon, a regular polygon with seven sides, if we say that the edges of the heptagon (red lines below) have length 1, then we can say that the shorter (purple) diagonals have length $\rho$, and the longer (blue) diagonals have length $\sigma$.

These diagonal-to-edge ratios are the same for all regular heptagons, of course. Let's worry about the actual values of $\rho$ and $\sigma$ later. You will find that we can accomplish quite a lot without knowing those values!

How do $\rho$ and $\sigma$ generate a "system of numbers"? Take another look at the heptagon above (and its inscribed heptagrams). Notice all the parallel lines? You should be able to find an abundance of similar triangles -- triangles that have the same angles but different sizes. These similar triangles generate a set of relationships between $\rho$ and $\sigma$. This becomes clearer if we hide most of the diagonals:

For example, see if you can convince yourself that

$$\sigma = \rho + \frac{1}{\sigma}$$

by looking at the large blue-red-purple-red trapezoid that runs through horizontally across the heptagon. The proof relies on constructing a proportion, based on the similar triangles involved:

$$\sigma : 1 = 1 : \sigma-\rho$$

Corresponding exercises can fill in the table of basic quotients:

$$\frac{1}{\sigma} = \sigma-\rho \bbox[10pt]{} \frac{1}{\rho} = 1+\rho-\sigma \bbox[10pt]{} \frac{\sigma}{\rho} = \sigma-1 \bbox[10pt]{} \frac{\rho}{\sigma} = \rho-1$$

A little bit of algebraic manipulation with those can yield the basic products, as well:

$$\rho\sigma = \rho+\sigma \bbox[10pt]{} \rho^2 = 1+\sigma \bbox[10pt]{} \sigma^2 = 1+\rho+\sigma$$

(You could also use Steinbach's elegant diagonal product formula to derive the products.)

Here you can start to see some of the special properties of $\rho$ and $\sigma$. For example, using the division and multiplication rules above, you can reduce any expression like $\rho^m\sigma^n$, for any integer powers $m$ and $n$ (including negative powers) to a simple expression of the form:

$$a+b\rho+c\sigma$$

for some integers $a$, $b$, and $c$. Let us try an example. The strategy is to substitute for $\rho\sigma$, $\sigma^2$, and $\rho^2$ at every opportunity, then distribute and multiply, and repeat, until we can collect all the terms.

$$\rho^3\sigma^2 = \rho^2\rho\sigma^2$$$$ = (1+\sigma)\rho(1+\rho+\sigma)$$$$ = (\rho+\rho\sigma)(1+\rho+\sigma)$$$$= (\rho+\rho+\sigma)(1+\rho+\sigma)$$$$ = (2\rho+\sigma)(1+\rho+\sigma)$$$$= (2\rho+\sigma) + (2\rho^2+\rho\sigma) + (2\rho\sigma+\sigma^2)$$$$= 2\rho+\sigma + 2+2\sigma + \rho+\sigma + 2\rho+2\sigma + 1+\rho+\sigma$$$$= 3 + 6\rho + 7\sigma$$

Thus $a+b\rho+c\sigma$ is the general form of any number in our new system. We will call this a heptagon number, in the same way we call $x+yi$ a complex number, or $u/v$ a rational number. In all such cases we need not be bothered by the fact that a number consists of more than just numerals. We care more about the fact that these numbers can be added, subtracted, multiplied, and divided, and still produce more numbers of the same form. Mathematicians use the term field for such a system of numbers.

Given two heptagon numbers $a+b\rho+c\sigma$ and $d+e\rho+f\sigma$, it is trivial to see that addition and subtraction produce more heptagon numbers:

$$(a+b\rho+c\sigma) + (d+e\rho+f\sigma) = (a+d)+(b+e)\rho+(c+f)\sigma$$$$(a+b\rho+c\sigma) - (d+e\rho+f\sigma) = (a-d)+(b-e)\rho+(c-f)\sigma$$

In math-speak, the heptagon numbers are closed under addition and subtraction because the integers are closed under those operations: $a+d$, $b-e$, etc. are all integers.

Demonstrating that the heptagon numbers are closed under multiplication is much more interesting:

$$(a+b\rho+c\sigma) * (d+e\rho+f\sigma) =$$$$(ad + ae\rho + af\sigma) + (bd\rho + be\rho^2 + bf\rho\sigma) + (cd\sigma + ce\rho\sigma + cf\sigma^2) =$$$$ad + ae\rho + af\sigma + bd\rho + (be\sigma+be) + (bf\rho+bf\sigma) + cd\sigma + (ce\rho+ce\sigma) + (cf+cf\rho+cf\sigma) =$$$$(ad+be+cf) + (ae+bd+bf+ce+cf)\rho + (af+be+bf+cd+ce+cf)\sigma$$

Notice that we have derived the formulas for addition, subtraction, and multiplication while performing those demonstrations. Deriving the formula for division of heptagon numbers is considerably more difficult, requiring some linear algebra. Also, to perform division, we would require rational coefficients $a$, $b$, and $c$ in every heptagon number, since the integers are not closed under division. Fortunately, we don't need to do any division for our purposes here, so we can be satisfied with the comfortable ease of doing integer arithmetic.

Implementing Heptagon Numbers in Python

The Python code below is a simple implementation of the arithmetic of heptagon numbers (except for division). The code is written in a very basic fashion, because I want you to be able to understand the code.

After a glance at the code, you can see that

(a,b,c)

is the Python representation for the heptagon number $a+b\rho+c\sigma$. The only Python "trick" I employ is the tuple unpacking assignment, which improves the readability of the code:

a, b, c = h
In [1]:
zero = (0,0,0)
one = (1,0,0)
rho = (0,1,0)
sigma = (0,0,1)
rho_inv = (1,1,-1)
sigma_inv = (0,-1,1)

def neg( h ) :
    a, b, c = h  # this assigment syntax "unpacks" the tuple
    return ( -a, -b, -c )

def plus( h1, h2 ) :
    a, b, c = h1
    d, e, f = h2
    return ( a+d, b+e, c+f )

def minus( h1, h2 ) :
    a, b, c = h1
    d, e, f = h2
    return ( a-d, b-e, c-f )

def times( h1, h2 ) :
    a, b, c = h1
    d, e, f = h2
    return ( a*d + b*e + c*f,
             a*e + b*d + b*f + c*e + c*f,
             a*f + b*e + b*f + c*d + c*e + c*f )

rho_sigma = times( rho, sigma )
rho_2 = times( rho, rho )
sigma_2 = times( sigma, sigma )

# some quick-and-dirty unit testing

print( rho_sigma )  # should be (0, 1, 1)
print( rho_2 )      # should be (1, 0, 1)
print( sigma_2 )    # should be (1, 1, 1)
print( times( sigma, sigma_inv ) )    # should be (1, 0, 0)
print( times( (1,-1,-1), (0,-1,1) ) ) # should be (0, -2, 1)
(0, 1, 1)
(1, 0, 1)
(1, 1, 1)
(1, 0, 0)
(0, -2, 1)

Improving Code Readability

While the implementation above is simple and perfectly functional, it is a bit awkward to code arithmetic expressions using function notation. Python does have the notion of operator overloading using special function names, so we can provide an implementation that is much more natural when used, and only a little more complicated than the code above.

In addition to supporting operator overloading, we can easily support mixed expressions using Python's "duck typing", as long as we're willing to write $\rho+1$ rather than $1+\rho$, for example.

Finally, this implementation supports automatic conversion of heptagon numbers to strings, so output is more readable, as well.

In [4]:
import math

class HeptagonNumber(object):

    def __init__( self, ones=0, rhos=0, sigmas=0 ):
        self.ones = ones
        self.rhos = rhos
        self.sigmas = sigmas

    def __add__( self, rhs ):  #self and rhs are HeptagonNumber objects
        a,b,c = self.ones, self.rhos, self.sigmas
        if isinstance( rhs, self.__class__ ):
            d,e,f = rhs.ones, rhs.rhos, rhs.sigmas
            return HeptagonNumber( a+d, b+e, c+f )
        elif isinstance( rhs, int ):
            return HeptagonNumber( a+rhs, b, c )
        else:
            raise TypeError("unsupported operand type(s) for +: '{}' and '{}'").format(self.__class__, type(other))

    def __sub__( self, rhs ):  #self and rhs are HeptagonNumber objects
        a,b,c = self.ones, self.rhos, self.sigmas
        if isinstance( rhs, self.__class__ ):
            d,e,f = rhs.ones, rhs.rhos, rhs.sigmas
            return HeptagonNumber( a-d, b-e, c-f )
        elif isinstance( rhs, int ):
            return HeptagonNumber( a-rhs, b, c )
        else:
            raise TypeError("unsupported operand type(s) for +: '{}' and '{}'").format(self.__class__, type(other))

    def __neg__( self ) :
        return HeptagonNumber( -self.ones, -self.rhos, -self.sigmas )

    def __mul__( self, rhs ) :
        a,b,c = self.ones, self.rhos, self.sigmas
        if isinstance( rhs, self.__class__ ):
            d,e,f = rhs.ones, rhs.rhos, rhs.sigmas
            return HeptagonNumber( a*d + b*e + c*f,
             a*e + b*d + b*f + c*e + c*f,
             a*f + b*e + b*f + c*d + c*e + c*f )
        elif isinstance( rhs, int ):
            return HeptagonNumber( a*rhs, b*rhs, c*rhs )
        else:
            raise TypeError("unsupported operand type(s) for +: '{}' and '{}'").format(self.__class__, type(other))

    def __str__( self ):
        s = u""
        if self.ones != 0 :
            s = s + u"%d" % ( self.ones )
        if self.rhos != 0 :
            if len(s) > 0 :
                s = s + u"+"
            if self.rhos != 1 :
                s = s + u"%d" % ( self.rhos )
            s = s + u"\u03C1"
        if self.sigmas != 0 :
            if len(s) > 0 :
                s = s + u"+"
            if self.sigmas != 1 :
                s = s + u"%d" % ( self.sigmas )
            s = s + u"\u03C3"
        return s.encode('utf-8') 

    # These values will be derived in Part 2
    
    sigma_real = 1 / (2* math.sin(math.pi/14))

    rho_real = 2*math.sin(5*math.pi/14)

    def __float__( self ) :
        return self.ones + self.rho_real * self.rhos + self.sigma_real * self.sigmas

zero = HeptagonNumber()
one = HeptagonNumber(1)
rho = HeptagonNumber(0,1)
sigma = HeptagonNumber(0,0,1)

# Now we can use natural operators for arithmetic,
#  as long as we put integers on the right.
sigma_inv = sigma - rho
rho_inv = rho - sigma + 1
rho_over_sigma = rho - 1
sigma_over_rho = sigma - 1

def printProd( h1, h2 ) :
    print str(h1) + str(h2) + " = " + str(h1*h2)
    
def printProd2( h1, h2 ) :
    print "(" + str(h1) + ")(" + str(h2) + ") = " + str(h1*h2)
    
printProd( rho, rho )
printProd( rho, sigma )
printProd( sigma, sigma )
printProd2( sigma, sigma_inv )
printProd2( rho, rho_inv )
printProd2( rho_over_sigma, sigma_over_rho )
ρρ = 1+σ
ρσ = ρ+σ
σσ = 1+ρ+σ
(σ)(-1ρ+σ) = 1
(ρ)(1+ρ+-1σ) = 1
(-1+ρ)(-1+σ) = 1

Summary, and a Look Ahead

In Part 2 of this story, I will show you how to use heptagon numbers to draw regular heptagons and heptagrams. In Part 3, I will show how we can implement a rotation operator without leaving the heptagon numbers.

For now, there are many interesting explorations you can make with heptagon numbers. For example, it is possible to implement functions for "scale by $\rho^n$" and "scale by $\sigma^n$" using only addition (and subtraction, for negative $n$), and looping, without any multiplication. Give it a try!