--- jupyter: description: 'The spirograph with planex2d: hypotrochoids as parametric curves, when a pattern closes and how many petals it has from the gear teeth, its length against an elliptic integral, and the deltoid and the astroid against their exact areas and lengths.' --- # The spirograph When does a spirograph pattern close, and how many petals does it have? A wheel with $r$ teeth rolls inside a ring with $R$ teeth, and a pen in a hole of the wheel, $d$ from its center, draws a hypotrochoid. As a point in the complex plane, with $t$ the wheel's turn around the ring, $$z(t) = (R - r)\,e^{it} + d\,e^{-i\,k t}, \qquad k = \frac{R - r}{r}.$$ Both terms come back together once $t$ is a whole number of turns and $k t$ is too, after $r / \gcd(R, r)$ turns. On the way, the two terms line up $R / \gcd(R, r)$ times, once for each petal. With the pen on the wheel's rim, $d = r$, and $r$ dividing $R$, the curve is a hypocycloid with $R/r$ cusps. For $R = 3r$ it is the deltoid, and for $R = 4r$ the astroid. ![A spirograph pattern of a ring of 96 teeth and a wheel of 52, with 24 petals](../_static/gallery/spirograph.png){.result} ```python import math import planex as px from scipy.special import ellipe ``` ```python nbsphinx="hidden" import matplotlib.pyplot as plt import numpy as np from docs_figures import INK, Row ``` ## A pattern `spirograph` draws the pattern of a ring and a wheel as one parametric curve, its angles in degrees as everywhere in planex2d, for as many turns as it takes to close. The curve crosses itself, so it is drawn in a sketch rather than as a single wire. The sketch starts at the curve's first point, where the pen is farthest out, at $R - r + d$: ```python def spirograph(R, r, d, turns=None): k = (R - r) / r turns = turns or r // math.gcd(R, r) curve = (f"{R - r}*cos(t) + {d}*cos({k}*t)", f"{R - r}*sin(t) - {d}*sin({k}*t)") return px.QuickSketch((R - r + d, 0)).Curve(curve, (0, 360 * turns), id="pattern") ``` The examples are a ring of 96 teeth with wheels of 72, 80 and 84 teeth, which give 4, 6 and 8 petals: ```python nbsphinx="hide-input" fig, axes = plt.subplots(1, 3, figsize=(9, 3.2)) for ax, (r, d) in zip(axes, ((72, 40), (80, 50), (84, 60))): k, t = (96 - r) / r, np.linspace(0, 2 * math.pi * (r // math.gcd(96, r)), 20000) ax.plot((96 - r) * np.cos(t) + d * np.cos(k * t), (96 - r) * np.sin(t) - d * np.sin(k * t), color=INK, lw=1.4) ax.set_title(f"Wheel {r}, pen {d}", color=INK, fontsize=10) ax.set_aspect("equal") ax.axis("off") plt.show() ``` ## Petals and turns After $r / \gcd(R, r)$ turns, the pen is back at its start, and one turn earlier it is not: ```python for r, d in ((72, 40), (80, 50), (84, 60)): g = math.gcd(96, r) closed, short = spirograph(96, r, d), spirograph(96, r, d, turns=r // g - 1) print("wheel", r, "| petals", 96 // g, "| closes after", r // g, "turns at", closed.pen.position, "| one turn earlier at", tuple(round(c, 3) for c in short.pen.position)) ``` ## The length The length of a pattern has no elementary formula, but an elliptic integral gives it exactly. The pen moves at the speed $|z'(t)| = \sqrt{A - B\cos((1 + k)t)}$ with $A = (R - r)^2 + d^2 k^2$ and $B = 2(R - r) d k$, and each petal takes a full turn of $(1 + k)t$, so with $N$ petals $$L = \frac{4N}{1 + k}\sqrt{A + B}\; E\!\left(\frac{2B}{A + B}\right),$$ with $E$ the complete elliptic integral of the second kind, SciPy's `ellipe`. `Measure` gives the length of the sketch's curve: ```python for r, d in ((72, 40), (80, 50), (84, 60)): k, N = (96 - r) / r, 96 // math.gcd(96, r) A, B = (96 - r) ** 2 + d**2 * k**2, 2 * (96 - r) * d * k exact = 4 * N / (1 + k) * math.sqrt(A + B) * ellipe(2 * B / (A + B)) print("wheel", r, "| length", spirograph(96, r, d).Measure("pattern")["length"], "| exact", exact) ``` ## The deltoid and the astroid A hypocycloid does not cross itself, so `px.Curve` draws it as a closed wire, and its face has an area. The deltoid of a wheel of radius 1 in a ring of 3 has the area $2\pi r^2$, and the astroid in a ring of 4 the area $6\pi r^2$. Each arch between two cusps is $8r(R - r)/R$ long, so the whole curve is $8r(R - r)$ long, which the print compares (Figure 1): ```python cusped = {} for R, name, area in ((3, "deltoid", 2 * math.pi), (4, "astroid", 6 * math.pi)): curve = px.Curve((f"{R - 1}*cos(t) + cos({R - 1}*t)", f"{R - 1}*sin(t) - sin({R - 1}*t)"), (0, 360)) cusped[name] = px.Face([curve]) print(name, "| area", cusped[name].mass, "exact", area, "| length", curve.mass, "exact", 8 * (R - 1)) ``` At the cusps the pen stops and turns back, so they are the curve's hardest points. The fitted spline follows them closely, and the deltoid's length differs from 16 by $1.5 \cdot 10^{-7}$, less than $10^{-8}$ of it. ```python nbsphinx="hide-input" Row(*[(face, name.capitalize()) for name, face in cusped.items()], caption="Hypocycloids with three and four cusps") ``` ```python nbsphinx="hidden" import sys sys.path.insert(0, "..") from gallery import save_figure from docs_figures import PLANEX R, r, d = 96, 52, 40 fig, ax = plt.subplots(figsize=(5, 5)) k, t = (R - r) / r, np.linspace(0, 2 * math.pi * (r // math.gcd(R, r)), 40000) ax.plot((R - r) * np.cos(t) + d * np.cos(k * t), (R - r) * np.sin(t) - d * np.sin(k * t), color=PLANEX, lw=0.9) ax.set_aspect("equal") ax.axis("off") save_figure(fig, "spirograph", bare=True) ```