Well, my taking my mind off trying to sort out feature engineering and feature selection is going to be a much longer journey than I expected.

I was sent a link to a web site: Rotae by Nadieh Bremer. Given my age you will realize that the spirograph was a part of my youth. But this project took it to another level and absolutely fascinated me. I decided I would like to see what I could do — in a similar vien.

I decided I would start by trying to generate a plot of the first of the three “continuous curves” covered in the article that inspired Rotae. Specifically:

FARRIS, Frank A. “Wheels on Wheels on Wheels-Surprising Symmetry."
Mathematics Magazine 69, No. 3 (1996): 185-189.

I also want to do some animation of the plots. Perhaps also showing all the wheels rolling about generating the curve. I have neither the artistic skills nor, in all probability, the sofware to produce anything like Rotae. But, I think this will be fun. And, you know this “blog” really is about learning to code and having fun while doing so. This looks like a perfect opportunity to do just that.

I have not to-date used a Jupyter notebook to run the code. Instead I have just been running Python modules in an Anaconda Powershell window. Using the same conda environment I used for the previous Titanic related posts. That may change, but for now it works for me.

Parametric Equations

As discussed in the article mentioned above, a common way to generate the curve would be to use the sine and cosine to get the x and y coordinates for “each point” on the curve. Something like this vector equation:

$$(x, y) = (cos(t), sin(t)) + \frac{1}{2}\ (cos(7t), sin(7t)) + \frac{1}{3}\ (sin(17t), cos(17t))$$

where, typically, t = [0, ..., 2*pi]

But as pointed out in the article, if we use complex notation we can convert this to a parametric equation for a terminating Fourier series. This approach struck me as better than the vector approach utilizing the sine and cosine. We are then looking at something like:

$$f(t) = x(t) + i*y(t) = e^{it} + \frac{1}{2}\ e^{7it} + \frac{i}{3}\ e^{-17it}$$

Then our x values are the real part(s) of f(t) and the y values the imaginary component(s). This just appealed to me more than the sine/cosine version.

Plot Curve

Okay, let’s plot that parametric equation above. We could use a variety of packages, but I have opted to use matplotlib. Let’s sort some imports and define a function for that equation. Fortunately, Numpy is happy with complex/imaginary numbers. Python uses j (engineering) not i (mathematics) for the imaginary unit. If interested in why see the resources section.

import numpy as np
import matplotlib.pyplot as plt

def f(t):
    return np.exp(1j*t) - np.exp(6j*t)/2 + 1j*np.exp(-17j*t)/3

And, now let’s define a discrete set of values for t between 0 and 2 * pi. We’ll use a fair number of values to ensure a nice smooth curve.

t = np.linspace(0, 2*np.pi, 500)

Now, we’ll plot our curve. I am also setting the plot aspect ratio to be square rather than rectangular.

plt.plot(np.real(f(t)), np.imag(f(t)))

# These two lines make the aspect ratio square
fig = plt.gcf()
fig.gca().set_aspect('equal')

plt.show()

Now if you run the module at a suitable command prompt, you will see something like this:

First attempt at spiro-like curve

Tidy the Display

I really didn’t like the x and y axes being included in the plot, so let’s fix that. Will need to get an axis object.

fig.gca().set_aspect('equal')

# get rid of axes and leftover whitespace
ax = fig.gca()
plt.axis('off')
ax.set_position([0, 0, 1, 1], which='both')

plt.show()

Now when you run the module, you should see something like this:

First spiro-like curve with axes removed from plot

Initial Attempt at Animation

Okay, before calling it a day, let’s see if we can animate the drawing of the curve. This is going to take a bit of work and some refactoring of the current code. This attempt will be fairly “quick and dirty”. If I continue working on this, I will consider tidying things up at a later date.

So, let’s start from the top.

I am going to use [matplotlib.animation.FuncAnimation]() to generate the animation. So will need another import, and a new plotting function that FuncAnimation will call for each frame in the animation.

Since we accessed the figure and axis objects, let’s assign those to variables near the top of our module. Let’s also make the plot a touch bigger. And, set the limits for the plot axes, set the aspect ratio and turn off display of the axes. I have hardcoded the limits based on a little experimentation.

We will also have to create a couple of arrays for storing the curve’s plot values for each frame. Since we want the curve to “grow” on the plot, we need to have all previous points available to the animation function. These need to be initialized outside the function. (Not sure need is correct, but definitely easier this way.)

For, now I am also going to leave the circle radii and frequencies hard coded in the parametric function definition.

""" r:\learn\py_play\spirograph\spiro_1.py:
  Use matplotlib to generate a plot of the first continuous curve discussed in the paper:

  FARRIS, Frank A. "Wheels on Wheels on Wheels-Surprising Symmetry.
  Mathematics Magazine 69, No. 3 (1996): 185-189.

  Version 0.2.0, 2022.02.01: refctor to animate drawing of curve
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation

# Set up plot/figure
fig, ax = plt.subplots(figsize=(8,8))  
# make the aspect ratio square
fig.gca().set_aspect('equal')
# get rid of axes and leftover whitespace
plt.axis('off')
ax.set_position([0, 0, 1, 1], which='both')
# need to set limits for the plot axes
ax.set_xlim(-2.05, 2.05)  
ax.set_ylim(-2.05, 2.05)

# lists to store the points for plotting the curve
x_sp = []
y_sp = []
# we don't want to create a new line object each time we run update,
# so instantiate line object for the curve available globally
ln_sp, = plt.plot(x_sp, y_sp)
# an array of the t values to use to plot the curve
# 500 should keep the curve smooth enough at this plot size
# and this will also determine the number of frames in the animation
t = np.linspace(0, 2*np.pi, 500)


def f(t):
    return np.exp(1j*t) + np.exp(7j*t)/2 + 1j*np.exp(-17j*t)/3

I ran a quick plot to make sure the above code still works.

plt.plot(np.real(f(t)), np.imag(f(t)))
plt.show()

Now for that plotting update function, update(), that will generate the curve data for each frame. It will use our parametric function to get the new x and y coordinates for the current frame. Update the list of x and y coordinates. Then update the line object associated with the curve and return that to FuncAnimation. It will take as an input (argument/parameter) the radian value associated with the current frame (from the t array initialized in our setup code).

def update(i):
  r1 = f(i)
  x_sp.append(np.real(r1))  
  y_sp.append(np.imag(r1))
  ln_sp.set_data(x_sp, y_sp)
  return ln_sp

Now let’s create our animation and display it.

ani = FuncAnimation(fig, update, t, interval=.5, repeat=False)
plt.show()

I have saved the animation to an animated gif so you can see it at work. An mp4 would be much smaller, but I didn’t want to mess with adding a video to this post. That may change with future posts (if any).

I can’t seem to stop the gif from repeating. Probably due to the version of Pillow I have in my conda environment. So, I decided to add a light plot of the curve to the gif version of the animation.

First attempt at animating spiro-like curve

Done

That’s it for this one. You have no idea how much I enjoyed playing with this. Wrote lots of sloppy code. And, so I think I will continue blogging on this subject for at least one more post. Though I expect there will likely be more than one.

Like I mentioned earlier, no notebook for this one. Though that may change before the post is published. I am also thinking of stuffing this post into the blog at a much earlier date than it is currently scheduled to be published — have managed to draft a few posts for about a month or two into the future.

And, if you would like to see that first gif feel free. Or the animation mp4. Rather big difference in size (3 MB for the second gif versus 166 KB for the mp4).

Looking forward to the next post. I am going to try adding the circles and radius lines to the animation.

Resources