As mentioned last time I am going to modify the code from the last post to allow producing animations using from 3 to 8 wheels. I also want to see if we can use the fargs parameter to send radii and frequency values to the animation update function.

New Module and init() Function

I am going to start a new module for this one. But I am going to start by copying the code from the module coded in the previous posts. I am also going to move some of the initialization code into a separate function that will be passed to funcAnimation(). I made the following changes, then tested to make sure the animation worked when passed the init() function.

""" r:\learn\py_play\spirograph\spiro_2.py:
  Use matplotlib to generate a plot of continuous curves using the parametric technique covered in :

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

  This module will allow for the animation/drawing of curves based on 3 to 8 wheels.
  Radii and rotational frequencies need to be specified and passed to functions.

  Version 0.1.0, 2022.02.02: start with copy of spiro_1.py and modify
"""

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation, PillowWriter, FFMpegWriter

# Misc vars
# value of alpha to use for circles and radial lines
c_alpha = 0.3
sp_rds = [1, 1/2, 1j/3]
sp_frq = [1, 7, -17]
# number of circles initialized one time, rather than in multiple functions
nbr_c = len(sp_rds)
# a few colur to use for drawing circles and radial lines
clrs = ['b', 'C1', 'g', 'r', 'c']
r_rad = [max(np.real(rd), np.imag(rd)) for rd in sp_rds]

# Set up plot/figure
fig, ax = plt.subplots(figsize=(8,8))  
# 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)
# add a line for the radial portions
ln_rs, = plt.plot([], [], color='k', alpha=c_alpha)
# 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 init():
  # with multiple circles, need to calculate axis limits
  t_lim = sum(r_rad) + 0.15
  ax.set_xlim(-t_lim, t_lim)  
  ax.set_ylim(-t_lim, t_lim)
  # # get rid of axes and leftover whitespace
  plt.axis('off')
  ax.set_position([0, 0, 1, 1], which='both')
  # set the aspect ratio to square
  ax.set_aspect('equal')
  # title

  circs = create_circles()
  for cir in circs:
    ax.add_patch(cir)
  
  ln_rs.set_data([], [])
  ln_sp.set_data([], [])
  # l1 = ln_rs,
  # l2 = ln_sp,
  # return l1, l2
  # return ln_sp
  return ln_rs, ln_sp, *circs

For the test I modified the call to funcAnimation() to be:

ani = FuncAnimation(fig, update, t, init_func=init, interval=.5, repeat=False, blit=True)

Passing Circle Data to FuncAnimation

I did mess around with passing the circle radii and frequencies to the animation function using fargs=. But, because there was no way to pass the circle variables to the init() function, I decided to, for now, skip this idea and just use globally accessible variables for the lists of radii and frequencies.

Modify Functions to Use Circle Data

Okay, to make this whole thing more flexible, I am going to have to remove hard coding of the circle parameters from the various functions. Instead I will get the circle parameters from the global variables: sp_rds and sp_frq. Well, and r_rad for the circle creation function.

f(t)

Let’s start with that parametric function which defines our curve. Since the circle creation function depends on the values returned by this function, let’s get rid of those hard coded values. I am going to take advantage of Python’s built-in complex() function to maybe simplify things a little. Also using the global variables sp_rds and sp_frq to get the necessary parameters for the equation.

def f(t):
  # sp_rds[] and sp_frq[] expected to exist in global scope with suitable values
  c_frq = [complex(0, sp) for sp in sp_frq]
  c_pts = [np.exp(c_frq[0]*t)*sp_rds[0]]
  for i in range(1, nbr_c):
    c_pts.append(c_pts[i-1] + np.exp(c_frq[i]*t)*sp_rds[i])
  return tuple(c_pts)

When testing this change, the radial line for the big circle did not show on the animation. The other two did. When I checked the lists of x and y coordinates for the radial line(s) created in the update function, they did not have 0 as the first element. The following lines in the update function did not seem to work, even though they did previously.

rs_x = [0] + np.real(r_pts)
rs_y = [0] + np.imag(r_pts)

[0] is of type <class 'list'>. The portion on the right of the plus sign is <class 'numpy.ndarray'>. That may be causing problems. So, I decide to change the numpy.ndarray to a list. That seemed to fix the problem. But really not sure why the problem didn’t show up previously.

rs_x = [0] + np.real(r_pts).tolist()
rs_y = [0] + np.imag(r_pts).tolist()

create_circles()

The circle creation function already uses the sp_rds list to generate its list of circles. Well more specifically, the r_rad list. I.E. the sp_rds list with any imaginary radii converted to a real number.

But, I only have five colours in the list clrs = ['b', 'C1', 'g', 'r', 'c']. So, let’s try something completely different. I am going to use one of matplotlib’s many colour maps and assign a suitable number of colours to the colour cycler used by the plotting functions. For now I am going to hard code a colour map, but I may eventually create a function accepting a map name to set up the cycler. The cycler works well with line plots, but for circles, I will need to specify the colour for each one individually.

# Set up plot/figure
fig, ax = plt.subplots(figsize=(8,8))  
# set up colour cycler for circle colours
ax.set_prop_cycle('color',[plt.cm.GnBu_r(i) for i in np.linspace(0, 1, nbr_c * 2)])
cycle = plt.rcParams['axes.prop_cycle'].by_key()['color']

. . .

def create_circles():
  # r_rad[] and cycle[] expected to exist in global scope with suitable values
  rs = f(0)
  circles = []
  circles.append(plt.Circle((0, 0), r_rad[0], color=cycle[0], fill=False, alpha=c_alpha))
  for i in range(1, nbr_c):
    circles.append(plt.Circle((np.real(rs[i-1]), np.imag(rs[i-1])), r_rad[i], color=cycle[i], fill=False, alpha=c_alpha))
  return circles

Seems to work with three wheels, will see what happens when we get around to increasing that number.

Test With More Wheels

I am going to test with configurations with more wheels.

4 Wheels

For the first I have modified the cicle parameter lists to use the 4 wheel values from the Wheels on Wheels on Wheels-Surprising Symmetry article.

# 4 wheels
sp_rds = [1, 1j/2, 1/5, 1j/5]
sp_frq = [2, -16, -7, 29]

Though the animation is a little fast, that did work. The gif is once again quite large. So, I will only include the final curve in the post. But you can download the animated gif or the mp4 video if you wish.

curve generated using 4 wheels

5 Wheels

I will eventually get into where the following numbers came from.

sp_rds = [1, 0.7079204688847565j, 0.6727215820151806, 0.5612608810815637, 0.35936127484837616j]
sp_frq = [-3, -15, 1, 1, 9]

You can download the animated gif or the mp4 video if you wish.

curve generated using 5 wheels

6 Wheels

# 6 wheels
sp_rds = [1, 0.7375812620375515, 0.6307032530245636, 0.5957789521397886, 0.5349597341858304, 0.44049737847391157]
sp_frq = [-3, 21, 9, 27, -9, -15]

For this one the curve line was not particularly smooth. So I changed the 500 to 1000 in my t variable declaration. Perhaps more interestingly, the animation repeated the sequence 2 or 3 times before terminating. This may be related to the situation mentioned in the article, where the frequencies are congruent to 0 mod k.

You can download the animated gif or the mp4 video if you wish.

curve generated using 6 wheels

7 Wheels

# 7 Wheels
sp_rds = [1, 0.7041851572301108j, 0.5452747785710877, 0.46123389148019145, 0.38528191563186875j, 0.34589058106324105j, 0.24707662091033855]
sp_frq = [-4, 6, 6, -14, 16, -4, 1]

You can download the animated gif or the mp4 video if you wish.

curve generated using 7 wheels

Enough Is Enough

Well, I think you get the idea. So, that’s it for this one. But, there will definitely be more to come. I want to look at how various frequencies affect the curves for a fixed set of wheels. For that I am going to write some code to produce the desired numbers for the wheel parameters. I also want to play with that multiline plot situation that popped up in an earlier post. And, I would like to add command line parameters to the module(s) to allow for the generation of random images based on the number of wheels and a specific k-fold symmetry. Lot’s more fun on the horizon.

Until next time, be safe, be happy, have fun.

Resources