--- jupyter: description: A Geneva drive stepping its wheel a quarter turn per revolution; the driver's locking disc is shaped with Boolean operations from everything the wheel sweeps through. --- # Geneva drive and its locking disc The example is a Geneva drive, the Maltese cross of film projectors and watch movements. A driver turning steadily makes the wheel step a quarter turn and then holds it locked until the next step. The pin enters a slot along its axis, so the step starts and ends without a jolt, and between steps, a locking disc on the driver sits in a concave arc of the wheel. The driver's locking disc is shaped with Boolean operations: it loses whatever the wheel sweeps through while it turns. ![A Geneva drive stepping its wheel a quarter turn per turn of the driver](../_static/gallery/geneva-drive.gif){.result} ```python import math import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation from IPython.display import HTML import planex as px ``` ```python nbsphinx="hidden" from docs_figures import GUIDE, LIGHT, Chart, Row ``` ## The dimensions The wheel has `n` = 4 slots. The driver's pin, of radius `pin` = 4, sits on a crank of `a` = 30, and the parts keep a clearance `gap` of 0.2. The rest follows: - the pin enters a slot along its axis, so the crank is at right angles to the slot there, and the center distance is c = a / sin(π / n); - the wheel reaches out to the pin's sides there, so its radius is the hypotenuse of c cos(π / n) and the pin's radius; - the pin is in a slot for 2 (90° − 180° / n) of each driver turn; - the locking disc has a radius of 0.72 a. ```python n = 4 a = 30.0 pin = 4.0 gap = 0.2 c = a / math.sin(math.pi / n) rim = math.hypot(c * math.cos(math.pi / n), pin) span = 90 - 180 / n lock = 0.72 * a print("center distance", c, "wheel radius", rim, "pin in a slot for", 2 * span, "degrees") ``` ## The wheel The wheel is a disc with n slots pointing at its center. Between the slots are concave arcs of the locking disc's radius plus the clearance, centered where the driver's axle is when that arc faces it. The slots are as wide as the pin plus the clearance and reach 1 deeper than the pin goes, and the wheel has a bore of 12 (Figure 1). ```python def disc(x, y, r): return px.QuickSketch().Circle(x, y, r).Face() def slot(q): w = pin + gap / 2 depth = c - a - pin - 1.0 s = px.QuickSketch((depth, -w)).Rectangle(2 * rim - depth, 2 * w).Face() return s.Rotated((0, 0), 180 + 360 * (q + 0.5) / n).Translated((c, 0)) def arc_cut(q): t = math.radians(180 + 360 * q / n) return disc(c + c * math.cos(t), c * math.sin(t), lock + gap) wheel = px.Subtract([disc(c, 0, rim), *[slot(q) for q in range(n)], *[arc_cut(q) for q in range(n)], disc(c, 0, 6)]) ``` ```python nbsphinx="hide-input" Row((wheel, "The wheel"), caption="The Geneva wheel with four slots") ``` ## The driver The driver has the pin on a crank arm, which runs in a layer behind the wheel, and the locking disc in the wheel's layer. While the wheel steps, the tips between its slots swing past the driver. The locking disc loses everything the wheel sweeps through: the wheel, grown by half the clearance, is placed at every half degree of the step, as the driver sees it, and the union of these positions is subtracted. `wheel_angle` gives the wheel's angle for every angle of the driver; see Motion below. The driver has a bore of 12 too, and its arm is 8 wide (Figure 2). ```python def wheel_angle(theta): turns, rest = divmod(theta + 180, 360) rest -= 180 if -span <= rest <= span: t = math.radians(rest) psi = math.degrees(math.atan2(a * math.sin(t), a * math.cos(t) - c)) % 360 return -turns * 360 / n + psi - (180 + span) return -(turns + (rest > span)) * 360 / n grown = wheel.Offset(gap / 2, inplace=False) steps = [q / 2 for q in range(-2 * round(span), 2 * round(span) + 1)] sweep = px.Unify([grown.Rotated((c, 0), wheel_angle(t)).Rotated((0, 0), -t) for t in steps]) locking = disc(0, 0, lock) - sweep - disc(0, 0, 6) pin_face = disc(a, 0, pin) arm = px.QuickSketch((0, -pin)).Rectangle(a, 2 * pin).Face() ``` ```python nbsphinx="hide-input" Row((wheel, "The wheel, the driver's arm, locking disc and pin", {"fills": [(arm - locking, LIGHT), (locking, GUIDE), (pin_face, GUIDE)]}), caption="The Geneva drive in the position of the drawing") ``` ## Motion While the pin is in a slot, the slot points at the pin, and otherwise the wheel stands still, as `wheel_angle` above computes it (Figure 3). ```python nbsphinx="hide-input" thetas = [q / 2 for q in range(-360, 360)] Chart("driver angle [°]", "wheel angle [°]", curves={"wheel": (thetas, [wheel_angle(t) for t in thetas])}, title="A quarter turn of the wheel per turn of the driver") ``` **Checks** over a full turn of the driver, in steps of half a degree: - the wheel steps exactly 360° / n per turn and stands still for the rest of it; - the parts in the wheel's layer never overlap, and they come no closer than the clearance. ```python def apart(t): placed = wheel.Rotated((c, 0), wheel_angle(t)) return min(locking.Rotated((0, 0), t).Distance(placed), pin_face.Rotated((0, 0), t).Distance(placed)) def overlap(t): return ((locking.Rotated((0, 0), t) + pin_face.Rotated((0, 0), t)) * wheel.Rotated((c, 0), wheel_angle(t))).mass standing = sum(1 for t in thetas if abs(wheel_angle(t + 0.5) - wheel_angle(t)) < 1e-12) / len(thetas) print("step per driver turn", wheel_angle(-180) - wheel_angle(179.999), "degrees") print("standing for", standing, "of the turn, exact", 1 - 2 * span / 360) print("largest overlap", max(overlap(t) for t in thetas), "closest approach", round(min(apart(t) for t in thetas), 3), "clearance", gap / 2) ``` ## In motion The drive through a full turn of the driver: ```python nbsphinx="hide-input" fig, ax = plt.subplots(figsize=(7, 5)) def frame(q): theta = -180 + 5 * q ax.clear() arm.Rotated((0, 0), theta).Plot(ax=ax, face_col="lightgray", legend=False) wheel.Rotated((c, 0), wheel_angle(theta)).Plot(ax=ax, face_col="tab:blue", legend=False) locking.Rotated((0, 0), theta).Plot(ax=ax, face_col="tab:orange", legend=False) pin_face.Rotated((0, 0), theta).Plot(ax=ax, face_col="tab:orange", legend=False) ax.set_aspect("equal", adjustable="box") ax.set_xlim(-35, c + rim + 3) ax.set_ylim(-35, 35) ax.axis("off") animation = FuncAnimation(fig, frame, frames=72, interval=60) plt.close(fig) HTML(animation.to_jshtml()) ``` ```python nbsphinx="hidden" import sys sys.path.insert(0, "..") from gallery import save_scene, save_figure, save_animation, HIDDEN save_animation(animation, "geneva-drive") ```