--- jupyter: description: 'Lissajous curves with planex2d: the curve x = sin(at + φ), y = sin(bt) cut at its own crossings by px.Wires, its 2ab − a − b crossings for coprime frequencies, the 2a + 2b touches that give the frequency ratio, and the open arc with (a − 1)(b − 1)/2 crossings at the special phase.' --- # Lissajous curves How often does a Lissajous curve cross itself? The curve $$x = \sin(a t + \varphi), \qquad y = \sin(b t)$$ 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](geometry-and-design.md) using planex2d. .. rst-class:: px-example-links `Open this example in the playground `__, the interactive code editor in the browser, to change the code and run it. ![A Lissajous curve with the frequencies 3 and 4, its 17 crossings marked](../_static/gallery/lissajous-curves.png){.result} ```python import math from collections import Counter import numpy as np import planex as px ``` ```python nbsphinx="hidden" from docs_figures import Row ``` ## The curves The curve is sampled at 6000 steps over one period, with $t$ from 0 to $2\pi$, and scaled to 50: ```python 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): ```python 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)) ``` ```python nbsphinx="hide-input" Row(*[(px.Compound(curves[r][1]), f"{r[0]} : {r[1]}, {len(curves[r][0])} crossings", {"dots": curves[r][0], "lw": 1.2}) for r in ((1, 2), (3, 2), (3, 4), (5, 4))], caption="Four Lissajous curves and their 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: ```python 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) ``` ## 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): ```python 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])) ``` ```python nbsphinx="hide-input" Row(*[(px.Compound(arcs[r][1]), f"{r[0]} : {r[1]}, {len(arcs[r][0])} crossings", {"dots": arcs[r][0], "lw": 1.2}) for r in ((3, 2), (3, 4), (5, 4), (7, 4))], caption="The folded curves at the special phase") ``` See [Wires and faces](../core/basics/wires-and-faces.md) for wires and `px.Wires`. ```python nbsphinx="hidden" import sys import matplotlib.pyplot as plt sys.path.insert(0, "..") from docs_figures import INK, PLANEX, RED from gallery import save_figure points = lissajous(3, 4, math.pi / 16) found = curves[3, 4][0] fig, ax = plt.subplots(figsize=(6, 4)) ax.plot(points[:, 0], points[:, 1], color=PLANEX, lw=2.2) ax.scatter(*np.array(found).T, s=30, color=RED, zorder=5) ax.plot([-50, 50, 50, -50, -50], [-50, -50, 50, 50, -50], color=INK, lw=0.8, ls=(0, (3, 2))) ax.set_xlim(-84, 84) ax.set_ylim(-56, 56) ax.set_aspect("equal") ax.axis("off") save_figure(fig, "lissajous-curves") plt.close(fig) ```