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
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 using planex2d.

[1]:
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
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:
[3]:
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’:
[4]:
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))
525.2 Hz | against | the stem's foot sideways -0.0 along -0.0053
1612.7 Hz | together | the stem's foot sideways -0.9778 along 0.0
3285.3 Hz | against | the stem's foot sideways -0.0 along 0.0303
4721.0 Hz | together | the stem's foot sideways -0.819 along 0.0
8171.4 Hz | together | the stem's foot sideways 1.8959 along -0.0
9027.1 Hz | against | the stem's foot sideways 0.0 along 0.0946
11441.4 Hz | together | the stem's foot sideways 0.0896 along -0.0
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):
[5]:
tuning = [f for f, kind, _, _ in found if kind == "against"]
print("clang / fundamental", tuning[1] / tuning[0], "| cantilever", (4.6940911 / 1.8751041) ** 2)
clang / fundamental 6.254815824253517 | cantilever 6.26689272858657
(a) The fork (rotated 90 degrees)
(b) Tuning mode, 525 Hz (rotated 90 degrees)
(c) Swaying, 1613 Hz (rotated 90 degrees)
(d) Clang, 3285 Hz (rotated 90 degrees)
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:
[7]:
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()
L 60 | tuning mode 873.9 | cantilever from the tine's start 1160.4 | from the bottom of the half ring 877.5
L 70 | tuning mode 666.6 | cantilever from the tine's start 852.6 | from the bottom of the half ring 669.4
L 80 | tuning mode 525.2 | cantilever from the tine's start 652.7 | from the bottom of the half ring 527.4
L 90 | tuning mode 424.5 | cantilever from the tine's start 515.8 | from the bottom of the half ring 426.2
L 100 | tuning mode 350.3 | cantilever from the tine's start 417.8 | from the bottom of the half ring 351.6
L 110 | tuning mode 293.9 | cantilever from the tine's start 345.3 | from the bottom of the half ring 295.0
L 120 | tuning mode 250.1 | cantilever from the tine's start 290.1 | from the bottom of the half ring 251.0
Tuned to 440 Hz
The concert A is 440 Hz. brentq finds the tine length that gives it:
[9]:
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))
tine length 88.24391452651199 | frequency 439.99980338380965
See Tuning a marimba bar for the free bar and its overtones, and netgen and NGSolve for meshing a planex2d face.