--- jupyter: description: 'The tuning fork with planex2d and NGSolve: a steel fork in plane stress, its modes sorted into tines swinging against each other and swaying together, the stem at rest in the tuning mode, the clang mode at 6.27 times the fundamental, the tines as cantilevers from the bottom of the bend, and the tine length for 440 Hz.' --- # The tuning fork Why does a tuning fork have two tines? In its tuning mode, the two tines swing against each other as mirror images. The forces they put on the root cancel. The stem hardly moves, the hand that holds it takes almost no energy out, and the fork rings for a long time at one clean pitch. Each tine bends like a cantilever, with the frequency $$f = \frac{1.875^2}{2 \pi} \, \frac{w}{L^2} \sqrt{\frac{E}{12 \rho}}$$ for a tine $L$ long and $w$ wide in the direction it swings. The next mode of the same kind, the clang that is heard for a moment after a hard strike, lies at about 6.27 times the fundamental. The example is a steel fork in plane stress. NGSolve and SciPy find its modes, which are sorted by how the tips of the tines move. The tuning mode is checked against the cantilever formula, along with the clang ratio and the motion of the stem. Then SciPy's `brentq` finds the tine length for 440 Hz, the concert A. This page is part of a collection of [physics examples](physics.md) using planex2d. ![A tuning fork in its tuning mode, the tines swinging against each other, the motion magnified](../_static/gallery/tuning-fork.gif){.result} ```python import math import matplotlib.pyplot as plt import ngsolve as ngs import numpy as np import planex as px import scipy.sparse as sp from scipy.optimize import brentq from scipy.sparse.linalg import eigsh ``` ```python nbsphinx="hidden" from docs_figures import Chart, Row ``` ## The fork The tines are 5 wide and 8 apart, in millimeters. A half ring joins them at the root, and a stem 8 wide and 40 long hangs below it. The fork is steel, with $E$ = 210000 N/mm², Poisson's ratio 0.3 and the density 7.85e-9 t/mm³, so the frequencies come out in Hz: ```python E, nu, rho = 210e3, 0.3, 7.85e-9 w, gap, stem = 5, 8, 40 inner, outer = gap / 2, gap / 2 + w def fork(L): """the fork with tines L long above the center of the half ring""" ring = px.Circle((0, 0), outer).Face() - px.Circle((0, 0), inner).Face() lower = px.QuickSketch().Move((-outer - 1, -outer - 1)).Rectangle(2 * outer + 2, outer + 1).Face() tines = [px.QuickSketch().Move((x, 0)).Rectangle(w, L).Face() for x in (-outer, inner)] handle = px.QuickSketch().Move((-inner, -outer - stem)).Rectangle(gap, stem + 1).Face() return px.Unify([ring * lower] + tines + [handle]) ``` ## Its modes The fork is free. Its modes come from its stiffness and mass as on the page on the marimba bar, and the first three, at zero frequency, are rigid motions. A mode is sorted by the sideways motion of the two tips, which move in opposite directions when the tines swing against each other and in the same direction when the fork sways as a whole. The motion of the stem's foot is given as a share of the tips': ```python mu, lam = E / (2 * (1 + nu)), E * nu / (1 - nu**2) def modes(L, k=10): """the frequencies in Hz, how the tines move and how much the stem's foot moves, for each mode""" mesh = ngs.Mesh(px.OCCGeometry(fork(L)).GenerateMesh(maxh=w / 2)) mesh.Curve(3) V = ngs.VectorH1(mesh, order=3) u, v = V.TnT() def stress(q): strain = ngs.Sym(ngs.Grad(q)) return 2 * mu * strain + lam * ngs.Trace(strain) * ngs.Id(2) a = ngs.BilinearForm(ngs.InnerProduct(stress(u), ngs.Sym(ngs.Grad(v))) * ngs.dx).Assemble() m = ngs.BilinearForm(rho * u * v * ngs.dx).Assemble() A = sp.csr_matrix(a.mat.CSR(), shape=(V.ndof, V.ndof)) M = sp.csr_matrix(m.mat.CSR(), shape=(V.ndof, V.ndof)) w2, vectors = eigsh(A, k=k, M=M, sigma=-1e5, which="LM") found = [] for j in np.argsort(w2)[3:]: mode = ngs.GridFunction(V) mode.vec.FV().NumPy()[:] = vectors[:, j] left, right = mode(mesh(-outer + w / 2, L - 0.5)), mode(mesh(inner + w / 2, L - 0.5)) foot = np.array(mode(mesh(0, -outer - stem + 0.5))) / max(abs(left[0]), abs(right[0])) kind = "against" if left[0] * right[0] < 0 else "together" found.append((math.sqrt(w2[j]) / (2 * math.pi), kind, foot, mode)) return found, mesh found, mesh = modes(80) for f, kind, foot, _ in found: print(round(f, 1), "Hz |", kind, "| the stem's foot sideways", round(foot[0], 4), "along", round(foot[1], 4)) ``` The lowest mode is the tuning mode. In it, the stem's foot moves sideways by a ten-thousandth of the tips' motion and along the stem by half a percent, while in the swaying modes it moves as much as the tips. The small motion along the stem is what a table picks up when the stem is pressed on it. The clang, the next mode with the tines against each other, lies close to the cantilever's ratio (Figure 1): ```python tuning = [f for f, kind, _, _ in found if kind == "against"] print("clang / fundamental", tuning[1] / tuning[0], "| cantilever", (4.6940911 / 1.8751041) ** 2) ``` ```python nbsphinx="hide-input" def dense(points, step=1.0): """a closed polyline with a point at least every step along each edge""" ring = np.vstack([points, points[:1]]) out = [] for p, q in zip(ring[:-1], ring[1:]): k = max(1, int(np.ceil(np.hypot(*(q - p)) / step))) out += [p + (q - p) * i / k for i in range(k)] return np.array(out) def at(mode, p): """the mode at p, or at a point 0.1 beside it where p lies just off the mesh (on the inside of the bend)""" for dx, dy in ((0, 0), (0.1, 0), (-0.1, 0), (0, 0.1), (0, -0.1)): try: return mode(mesh(p[0] + dx, p[1] + dy)) except Exception: pass return (0.0, 0.0) def moved(n, scale=12): """the fork's outline moved by mode n of the fork with tines 80 long""" outline = np.array(fork(80).wires[0].Polylines(1e-4)[0]) if np.allclose(outline[0], outline[-1]): outline = outline[:-1] points = dense(outline) shift = np.array([at(found[n][3], p) for p in points]) # the tines drawn moving apart, not through each other shift *= scale / np.abs(shift).max() * (-1 if at(found[n][3], (-outer + w / 2, 79.5))[0] > 0 else 1) return px.FromPolyline(points + shift, closed=True).Face() swaying = next(n for n, m in enumerate(found) if m[1] == "together") clang = [n for n, m in enumerate(found) if m[1] == "against"][1] # the clang bends each tine into an S: drawn smaller, so that the tines do not meet across the gap of 8 Row((fork(80), "The fork", {"rotate": 90}), (moved(0, 10), f"Tuning mode, {found[0][0]:.0f} Hz", {"rotate": 90}), (moved(swaying, 10), f"Swaying, {found[swaying][0]:.0f} Hz", {"rotate": 90}), (moved(clang, 3.5), f"Clang, {found[clang][0]:.0f} Hz", {"rotate": 90}), caption="The fork with tines 80 long and three of its modes, the motion magnified") ``` ## The tines as cantilevers Each tine is a cantilever, but not one held at the point where it starts, because the half ring bends with it. Measured from the bottom of the half ring, which lies `outer` = 9 below the tine's start, the tine's length fits the cantilever formula to within half a percent for every length. Figure 2 shows the frequency for tines from 60 to 120 long: ```python lengths = [60, 70, 80, 90, 100, 110, 120] cantilever = lambda L: 1.8751041**2 / (2 * math.pi) * w / L**2 * math.sqrt(E / (12 * rho)) frequencies = [] for L in lengths: frequencies.append(modes(L)[0][0][0]) print("L", L, "| tuning mode", round(frequencies[-1], 1), "| cantilever from the tine's start", round(cantilever(L), 1), "| from the bottom of the half ring", round(cantilever(L + outer), 1)) # plt.plot(lengths, frequencies); plt.show() ``` ```python nbsphinx="hide-input" fine = np.linspace(60, 120, 200) Chart("Tine length", "Frequency in Hz", dots={"Tuning mode (NGSolve)": (lengths, frequencies)}, lines={"Cantilever, L": (fine, cantilever(fine)), "Cantilever, L + 9": (fine, cantilever(fine + outer))}, title="The tuning mode against the cantilever formula") ``` ## Tuned to 440 Hz The concert A is 440 Hz. `brentq` finds the tine length that gives it: ```python L440 = brentq(lambda L: modes(L)[0][0][0] - 440, 60, 140, xtol=1e-4) print("tine length", L440, "| frequency", modes(L440)[0][0][0]) # px.Plot(fork(L440)) ``` See [Tuning a marimba bar](marimba-bar-tuning.md) for the free bar and its overtones, and [netgen and NGSolve](../integrations/netgen/index.md) for meshing a planex2d face. ```python nbsphinx="hidden" import sys from matplotlib.animation import FuncAnimation sys.path.insert(0, "..") from docs_figures import FACE, INK, PLANEX, SOFT from gallery import save_animation # the tuning mode over one period, the tines 3.6 apart at most inward, so that they never meet across the gap of 8 outline = np.array(fork(80).wires[0].Polylines(1e-4)[0]) if np.allclose(outline[0], outline[-1]): outline = outline[:-1] points = dense(outline) shift = np.array([at(found[0][3], p) for p in points]) shift *= 3.6 / np.abs(shift).max() * (-1 if at(found[0][3], (-outer + w / 2, 79.5))[0] > 0 else 1) fig, ax = plt.subplots(figsize=(6, 4)) def frame(k): ax.clear() fork(80).Rotated((0, 0), -90).Plot(ax=ax, face_col=None, line_col=SOFT, legend=False) swing = points + math.cos(2 * math.pi * k / 24) * shift px.FromPolyline(swing, closed=True).Face().Rotated((0, 0), -90).Plot(ax=ax, face_col=PLANEX, line_col=INK, legend=False) for artist in ax.lines + ax.collections + ax.patches: artist.set_linewidth(1.0) ax.set_aspect("equal", adjustable="box") ax.set_xlim(-57, 87) ax.set_ylim(-48, 48) ax.axis("off") animation = FuncAnimation(fig, frame, frames=24, interval=42) save_animation(animation, "tuning-fork", fps=24, bare=True) plt.close(fig) ```