Lissajous curves
How often does a Lissajous curve cross itself? The curve
is what an oscilloscope draws when two sine signals drive its two axes. With whole, coprime frequencies \(a\) and \(b\) and a phase \(\varphi\) that is not special, it is a closed curve that crosses itself exactly \(2ab - a - b\) times. It touches the box around it \(2a + 2b\) times, \(b\) times at the top and at the bottom and \(a\) times at each side, so the frequency ratio can be read off the screen. At the special phases the curve folds into an open arc that it runs through back and forth, with \((a - 1)(b - 1)/2\) crossings.
The example draws the curves as polylines, lets px.Wires cut them where they cross and counts the crossings for six frequency ratios, then reads the ratio from the touches and checks the folded arcs.
This page is part of a collection of geometry and design examples using planex2d.
Open this example in the playground, the interactive code editor in the browser, to change the code and run it.

[1]:
import math
from collections import Counter
import numpy as np
import planex as px
The curves
The curve is sampled at 6000 steps over one period, with \(t\) from 0 to \(2\pi\), and scaled to 50:
[3]:
def lissajous(a, b, phase, samples=6000):
"""the points of x = sin(a t + phase), y = sin(b t) over one period, scaled to 50"""
t = np.linspace(0, 2 * math.pi, samples + 1)
return np.column_stack([np.sin(a * t + phase), np.sin(b * t)]) * 50
The crossings
A polyline that crosses itself is no wire, but px.Wires takes its segments as loose edges, cuts them where they cross and chains the pieces into clean wires. Every crossing becomes a point where four edge ends meet. The phase \(\pi / 4b\) is not one of the special ones (Figure 1):
[4]:
def crossings(points):
"""the points where the polyline crosses itself, and its pieces as clean wires"""
pieces = px.Wires([px.Line(tuple(p), tuple(q)) for p, q in zip(points[:-1], points[1:])])
ends = Counter((round(v[0], 7), round(v[1], 7)) for w in pieces for e in w.edges for v in (e.start, e.end))
return [p for p, count in ends.items() if count == 4], pieces
curves = {}
for a, b in ((1, 2), (3, 2), (3, 4), (5, 4), (5, 6), (7, 4)):
found, pieces = crossings(lissajous(a, b, math.pi / (4 * b)))
curves[a, b] = (found, pieces)
print("a", a, "b", b, "| crossings", len(found), "| 2ab - a - b", 2 * a * b - a - b)
# px.Plot(px.Compound(pieces))
a 1 b 2 | crossings 1 | 2ab - a - b 1
a 3 b 2 | crossings 7 | 2ab - a - b 7
a 3 b 4 | crossings 17 | 2ab - a - b 17
a 5 b 4 | crossings 31 | 2ab - a - b 31
a 5 b 6 | crossings 49 | 2ab - a - b 49
a 7 b 4 | crossings 45 | 2ab - a - b 45

(a) 1 : 2, 1 crossings

(b) 3 : 2, 7 crossings

(c) 3 : 4, 17 crossings

(d) 5 : 4, 31 crossings
The frequency ratio
On an oscilloscope, the ratio of the two frequencies is read off the curve, which touches the top \(b\) times and each side \(a\) times. A touch is a highest point of \(y\) or of \(x\) along the curve. It may fall on two equal samples, so the test allows equality on one side:
[6]:
for a, b in ((3, 2), (5, 4), (7, 4)):
x, y = lissajous(a, b, math.pi / (4 * b))[:-1].T
top = np.sum((y >= np.roll(y, 1)) & (y > np.roll(y, -1)))
side = np.sum((x >= np.roll(x, 1)) & (x > np.roll(x, -1)))
print("a", a, "b", b, "| touches at the top", top, "| at the right side", side)
a 3 b 2 | touches at the top 2 | at the right side 3
a 5 b 4 | touches at the top 4 | at the right side 5
a 7 b 4 | touches at the top 4 | at the right side 7
The folded curves
At a special phase the curve runs along an open arc and back along the same path. With \(x = \cos a t\), \(y = \cos b t\) the arc is run once for \(t\) from 0 to \(\pi\). It has fewer crossings, \((a - 1)(b - 1)/2\) (Figure 2):
[7]:
arcs = {}
for a, b in ((3, 2), (3, 4), (5, 4), (5, 6), (7, 4)):
t = np.linspace(0, math.pi, 6001)
arcs[a, b] = crossings(np.column_stack([np.cos(a * t), np.cos(b * t)]) * 50)
print("a", a, "b", b, "| crossings", len(arcs[a, b][0]), "| (a - 1)(b - 1)/2", (a - 1) * (b - 1) // 2)
# px.Plot(px.Compound(arcs[5, 4][1]))
a 3 b 2 | crossings 1 | (a - 1)(b - 1)/2 1
a 3 b 4 | crossings 3 | (a - 1)(b - 1)/2 3
a 5 b 4 | crossings 6 | (a - 1)(b - 1)/2 6
a 5 b 6 | crossings 10 | (a - 1)(b - 1)/2 10
a 7 b 4 | crossings 9 | (a - 1)(b - 1)/2 9

(a) 3 : 2, 1 crossings

(b) 3 : 4, 3 crossings

(c) 5 : 4, 6 crossings

(d) 7 : 4, 9 crossings
See Wires and faces for wires and px.Wires.