Okay, another spirograph post. I have played with using lines parallel to the y-axis. Doing half the cycle in each direction. And, tried to plot individual lines perpendicular to the curve’s tangent at each point calculated for the curve. I liked the results enough to keep this series going.

I am once again playing with the idea of a cycling line width for the simple spirograph curve. Doesn’t end up looking simple, but…

Lines Parallel to the Y-axis

This is a fairly simple change. But, because I didn’t want to, at this time, add another command line parameter/switch, I used the existing -t parameter and added another test. And, of course plenty of duplicated code. So, I copied the code for test 3 to a new if block for a test 4. Then made a few changes to turn the plotted lines 90 degrees.

...
  for i in range(t_pts):
    c_fi = splt.f(t[i])
    c_x = np.real(c_fi[-1])
    c_y = np.imag(c_fi[-1])

    c_adj = (i % lc_frq) + 1
    if c_adj >= lc_dir:
      c_adj = lc_diff - c_adj
    x_lf, x_rt = (c_x-(c_adj*lc_mult), c_x+(c_adj*lc_mult))
...

First run makes it pretty obvious the lines are now perpendicular to the y-axis. Eh?

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 4
get_radii(6) -> [1, 0.5955832707359843, 0.584260423550214j, 0.5598509012893342j, 0.3642955495084248j, 0.2298496437650825]
get_freqs(nbr_w=6, kf=4, mcg=2) -> [-2, 2, 18, 6, 6, -6]
r_keep: [3]
Test 4: an attempt at changing line thickness - y axis - for a basic curve plot
colour map: plt.cm.GnBu_r (24), max line height: 0.17853001772347474, line width: 9
attempt at plotting basic curve with a changing line thickness perpendicular to the y-axis

And, another with double the plot points.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 4 -p 1000
get_radii(8) -> [1, 0.6913216128983071j, 0.38582062122447824, 0.28093506685574704, 0.23795891575929823, 0.1536888900443798, 0.1319004446418541, 0.125j]
get_freqs(nbr_w=8, kf=3, mcg=2) -> [5, 5, -7, -4, 8, 11, 11, 11]
r_keep: [5]
Test 4: an attempt at changing line thickness - y axis - for a basic curve plot
colour map: plt.cm.viridis (50), max line height: 0.17932935529024055, line width: 1

And, with a randomly selected line width of 1, it is really obvious which way the lines are running. But, still a pleasant to look at image.

another plot of basic curve with a changing line thickness perpendicular to the y-axis

Alternate Parallel Axis

Next I thought I’d look at changing the parallel axis half way through a cycle. So added another test (#5) and duplicate code. I made a few changes to the way I generate and get the points on the curve. Instead of calling splt.f() every loop, I call it once outside the loop, then reference the returned values in the loop. Should be a touch faster that way. Will eventually modify the other tests to do the same, if possible. Don’t know if that will work with the animations.

As for this test, the crucical code is:

    if c_tmp >= lc_dir:
      y_up, y_dn = (c_y+(c_adj*lc_mult), c_y-(c_adj*lc_mult))
      ax.plot([c_x, c_x], [y_up, y_dn], lw=ln_w)
    else:
      x_lf, x_rt = (c_x-(c_adj*lc_mult), c_x+(c_adj*lc_mult))
      ax.plot([x_lf, x_rt], [c_y, c_y], lw=ln_w)

Ran some test plots. Didn’t like most of them, hard transition when changing parallel axis. But, did get one I thought worth including in the post.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 5 -p 1000
get_radii(4) -> [1, 0.6300811883281905, 0.4440238156211833j, 0.3527866493447705]
get_freqs(nbr_w=4, kf=3, mcg=1) -> [1, -5, 13, 1]
r_keep: [2]
Test 5: an attempt at changing line thickness - changing axis - for a basic curve plot
colour map: plt.cm.GnBu_r (50), max line height: 0.19083643005780496, line width: 1
plot of basic curve with a changing line thickness and every half cycle alternating the axis to which the lines are perpendicular

Then I thought, what if I alternate the axis every second plot point. Relatively simple change.

    if i % 2 == 0:
      y_up, y_dn = (c_y+(c_adj*lc_mult), c_y-(c_adj*lc_mult))
      ax.plot([c_x, c_x], [y_up, y_dn], lw=ln_w)
    else:
      x_lf, x_rt = (c_x-(c_adj*lc_mult), c_x+(c_adj*lc_mult))
      ax.plot([x_lf, x_rt], [c_y, c_y], lw=ln_w)

And, one of my trial runs looked like this.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 5 -p 1000
get_radii(4) -> [1, 0.5413278764739945j, 0.4513030265687153j, 0.25760781298218155]
get_freqs(nbr_w=4, kf=2, mcg=1) -> [-1, -7, 1, -3]
r_keep: [2]
Test 5: an attempt at changing line thickness - changing axis - for a basic curve plot
colour map: plt.cm.PuBu_r (50), max line height: 0.18275416243634665, line width: 8
plot of basic curve with a changing line thickness and every 2nd plot point alternating the axis to which the lines are perpendicular

While considering the option, didn’t think hard enough to realize I’d be drawing a lot of crosses. But, the plots didn’t look all that bad.

Lines Perpendicular to Curve Tangent

Okay, I don’t want to mess with differentials. But it occurred to me that if I calculated the slope of the plot points behind and ahead of the current point, I would get a reasonable estimate of the curve’s tangent at the current plot point. Well assuming I am using a decent number of points. So, that’s my next test (#6). Sorry, more duplicating of code.

Getting Perpendicular Line

You may recall that the slope of the line perpendicular to the tangent, \(S_p\), at any given point is minus the reciprocal of the slope of the tangent, \(S_t\), at that point.

$$S_p = - \frac{1}{S_t}$$

And, a line has the rather simple formula, \(y = ax + b\). Where \(a\) is the slope and \(b\) is the intercept. Now, we know \(y\), \(a\) and \(x\), so should be easy enough to figure out \(b\). Then we create a simple lambda function for our perpendicular line. Saving whatever data we need for the next iteration of our plotting loop. Oh yes, and let’s not divide by zero. Something like:

  r_pts = splt.f(t)
  c_fi = r_pts[-1]
  prv_pt = (np.real(c_fi[-1]), np.imag(c_fi[-1]))
  nxt_pt = (np.real(c_fi[1]), np.imag(c_fi[1]))
  prv_pln = lambda x: 0

  for i in range(t_pts):
  # for i in range(0, 10, 2):
    c_x = np.real(c_fi[i])
    c_y = np.imag(c_fi[i])

    # is_0_div = (prv_pt[0] - c_x) == 0
    is_0_div = (nxt_pt[0] - prv_pt[0]) == 0
    if not is_0_div:
      # est_tngt = (prv_pt[1] - c_y) / (prv_pt[0] - c_x)
      est_tngt = (nxt_pt[1] - prv_pt[1]) / (nxt_pt[0] - prv_pt[0])
      m1 = -1 / est_tngt
      b1 = c_y - (m1 * c_x)
      pln = lambda x: m1*x + b1
      prv_pt = (c_x, c_y)
      # make sure don't exceed array boundary
      nxt_ndx = (i + 2) % t_pts
      # nxt_pt = (np.real(r_pts[0][nxt_ndx]), np.imag(r_pts[0][nxt_ndx]))
      nxt_pt = (np.real(c_fi[nxt_ndx]), np.imag(c_fi[nxt_ndx]))

Plotting Line

We will use pln() to calculate the end points for our line by shifting \(x\) slightly in each direction. Concept is similar to what we did to get the end points for the lines parallel to the axes. Something like:

    c_tmp = (i % lc_frq) + 1
    if c_tmp >= lc_dir:
      c_adj = lc_diff - c_adj
    else:
      c_adj = c_tmp

    if not is_0_div:
      x_lf, x_rt = (c_x - c_adj*lc_mult, c_x + c_adj*lc_mult)
      y_lf, y_rt = (pln(x_lf), pln(x_rt))
      ax.plot([x_lf, x_rt], [y_lf, y_rt], lw=ln_w)

That’s indeed simpler than I expected it to be.

And a test plot.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(8) -> [1, 0.5821705649162981, 0.5387651072891364, 0.2915857487395326, 0.19876211782206524, 0.125j, 0.125, 0.125]
get_freqs(nbr_w=8, kf=7, mcg=1) -> [-6, 29, -13, 22, -20, 29, 8, 29]
r_keep: [3]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.GnBu_r (24), max line height: 0.1930135865119747, line width: 5, lc_mult: 0.015441086920957976
attempt to plot lines perpendicular to tangent of underlying spirograph curve

Certainly not what I expected. But, I also expect you already realized what was going to happen. Took me a bit, but I did eventually figure it out.

The problem is as the slope of the tanget approaches zero, the length of the perpendicular line would approach infinity. As matplotlib adjusted the boundaries of the plot to account for those really long lines, the lines I really wanted to see became proportionately smaller. In some cases so small they were not visible at all in some of the trial plots. Picked the one above because it actually illustrated the problem.

Don’t Plot Long Lines

The first approach I took was to not plot lines over a certain length. I ended up choosing a boundary of 3 units. And, for this I also needed a function to calculate the length of each line. So, I added the following function definition near the top of the test’s code block (if block).

  def len_ln(x1, y1, x2, y2):
    return np.sqrt((x2 - x1)**2 + (y2 - y1)**2)

And, I calculate the line length for loop iteration, and just don’t plot any longer than 3 units. Something like this:

    if not is_0_div:
      x_lf, x_rt = (c_x - c_adj*lc_mult, c_x + c_adj*lc_mult)
      y_lf, y_rt = (pln(x_lf), pln(x_rt))
      ln_len = len_ln(x_lf, y_lf, x_rt, y_rt)
      if ln_len > 2:
        continue
      ax.plot([x_lf, x_rt], [y_lf, y_rt], lw=ln_w)

Giving it a try.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(3) -> [1, 0.6553528588197892, 0.6445425031059817j]
get_freqs(nbr_w=3, kf=3, mcg=2) -> [2, 8, 8]
r_keep: [2]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.hot (24), max line height: 0.1956124421322238, line width: 2, lc_mult: 0.015648995370577904
plot lines perpendicular to tangent of underlying spirograph curve dropping any lines that are too long

Not what I was expecting. But certainly has an artistic flair. I did not expect those hard changes in line length. So, I added a line of code to plot the underlying curve over the perpendicular line plot to try and get a feel for what was happening.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(4) -> [1, 0.5891920594044432, 0.5209404083148098, 0.44397993508805533]
get_freqs(nbr_w=4, kf=4, mcg=1) -> [-3, -11, 5, 5]
r_keep: [2]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.viridis (24), max line height: 0.19883148258829014, line width: 6, lc_mult: 0.01590651860706321
plot lines perpendicular to tangent of underlying spirograph curve dropping any lines that are too long, overlaying a plot of the underlying curve

Well, you can see where the lines are dropped as the tangent’s slope approaches zero. But, I do not understand why on the outer loops that hard change in line length is being produced. I would expect adjacent lines to be of a similar length. Then it occurred to me that maybe the cycle length was affecting things. So, I added code to plot a red dot at the point where the cycle restarted. And, bingo.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(8) -> [1, 0.5468203914115016j, 0.4609911307985759j, 0.32397878590137985j, 0.28147505453221694, 0.22092059802684264j, 0.16840340818420318, 0.125]
get_freqs(nbr_w=8, kf=3, mcg=2) -> [2, -1, 11, 11, -10, 14, -4, 2]
r_keep: [4]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.PuBu_r (24), max line height: 0.1789020273771002 (0.014312162190168017), line width: 2, lc_mult: 0.014312162190168017
plot lines perpendicular to tangent of underlying spirograph curve dropping any lines that are too long, overlaying a plot of the underlying curve and cycle end points

So, let’s try a fixed value for getting the x-coord for the line’s endpoints.

      # x_lf, x_rt = (c_x - c_adj*lc_mult, c_x + c_adj*lc_mult)
      x_lf, x_rt = (c_x - lc_mult*10, c_x + lc_mult*10)
(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(4) -> [1, 0.6303120117756168j, 0.5561295787518112j, 0.3220830717538938]
get_freqs(nbr_w=4, kf=4, mcg=2) -> [2, -10, -14, -14]
r_keep: [2]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.hot (24), max line height: 0.19577210375890688 (0.01566176830071255), line width: 8, lc_mult: 0.01566176830071255
plot lines perpendicular to tangent of underlying spirograph curve using a fixed x-axis difference for the line endpoints

Other than the dropped lines, that’s looking more like what is desired. Though I may mess with that 10 times multiple. Using just lc_mult produced lines that were much to short to be very useful.

Don’t Drop Long Lines

But what to do if we don’t drop. I am going to look at reducing their length by reducing the y-values until the line is under a given length. Again, relatively simple code, but another loop needed. I’ve also reduced the length at which I take action from two (2) to three (3). I also wanted to make sure the lines didn’t get too small or ended up at zero length. At least as much as possible. Took a bit of experimentation to find reasonably good values.

      ln_len = len_ln(x_lf, y_lf, x_rt, y_rt)
      if ln_len > 2:
        while not ln_len < 1.99 and ln_len >= 3: 
          y_lf *= .95
          y_rt *= .95
          ln_len = len_ln(x_lf, y_lf, x_rt, y_rt)
        # continue

Really not what I was aiming for, but still a touch of visual interest.

(ds-3.9) PS R:\learn\py_play\spirograph> python spiro_play.tst.py -t 6
get_radii(6) -> [1, 0.5409332228530115, 0.49166983681299503, 0.4173137196226338, 0.2597773345307932j, 0.256408083690708j]
get_freqs(nbr_w=6, kf=3, mcg=1) -> [-2, -2, 7, 10, 7, -5]
r_keep: [3]
Test 6: an attempt at changing line thickness - perpendicular to curve - for a basic curve plot
colour map: plt.cm.PuBu_r (24), max line height: 0.18125609240215632 (0.014500487392172506), line width: 5, lc_mult: 0.014500487392172506
plot lines perpendicular to tangent of underlying spirograph curve shrinking overly long lines

Looks like a line or two still getting dropped.

I think I really need to figure out how to somehow limit how much is getting plotted. Might be better to look at plotting against a curve generated by one of the slower wheels rather than the ultimate spirograph curve?

Done

I think that’s it for this one.

I want to refactor all the code, getting rid of as much duplication as possible. Adding all the various tests into one module using command line parameters to select the desired plot and to set any options of interest (line styles, over plots, etc.).

So there will likely be one more post in this series sharing the refactored packages and that final, do-all module. I find it quite therapeutic repeatedly running these scripts to generate the various plots. It’s quick. The plots change like crazy. Many, if not most, are rather visually entertaining. And, I know I wrote the code — that’s a real bonus.

Until then, have fun and be safe.

Done, Done, Done

Since drafting the above post, I have managed to write something that might be able to fake it as that do-all, end-all module. But, have decided it didn’t need a post of its own. So, I have added a page (unlisted post) with all the code for the current state of the module and related packages. Should you be interested take a look at, Spirograph IX: Package Code III.

So, at least for the forseeable future, that is it for wheels on wheels on wheels on… posts. It has truly been a lot of fun. I don’t believe how much pleasure and satisfaction I got out of the effort and the experimentation. Really need to find something else like this to tackle.

I sincerely hope you find something as satisfying to tackle. Whether pure fun or something more useful to the world at large.