Machining Sounds - Part A: Vibrations¶
Part A: Vibrations¶
A FAB26 workshop lesson, companion to Machining Dynamics: Jupyter Notebook Edition (DOI: forthcoming)
Authors: Tony L. Schmitz and Michael F. Gomez
"The sensation of sound is a thing sui generis, not comparable with any of our other sensations." Lord Rayleigh, The Theory of Sound (1877)
Machines make sound and that sound carries information. Every sound a milling machine makes begins as a vibration, so vibration is where this lesson series begins. The series has three Parts and one destination: by the end of Part C, you will listen to a simulated cut, identify its chatter frequency, and choose a better spindle speed, with a time domain simulation supporting every step. Part A contributes the vocabulary that Parts B and C require: the natural frequency, stiffness, and damping of a structure, the frequency response function that contains them, and the connection between a vibrating structure and the sound it radiates.
A note on the notebook mechanics, stated once for the whole series. A few short parameter cells are left visible and are meant to be edited and rerun. Every figure and analysis cell is collapsed beneath the prose that describes it and can be expanded whenever the implementation is wanted. Each Part stands alone and emphasizes a live demonstration.
The purposes of Part A are to:
- Review key concepts in the study of sound and vibration and the origin of the chatter problem in milling.
- Distinguish between the three kinds of vibration: free, forced, and self-excited.
- Describe the simplest vibrating system and its natural frequency.
- Explain damping, and why a struck object rings down rather than ringing forever.
- Describe forced vibration, resonance, and the frequency response function (FRF).
- Introduce the tap test, the standard measurement of tool tip dynamics.
- Connect vibration to sound and learn to interpret the frequency spectrum and spectrogram.
Each concept is demonstrated interactively: run the cells, listen, adjust a parameter or a slider, and run again. No prior vibrations background is assumed.
Nomenclature¶
The following symbols are used throughout the lesson:
| Symbol | Meaning | Units |
|---|---|---|
| $t$ | time | s |
| $f$ | frequency | Hz |
| $m$ | mass | kg |
| $k$ | stiffness | N/m (often quoted in N/µm) |
| $c$ | viscous damping coefficient | N·s/m |
| $\omega$ | forcing frequency | rad/s |
| $\omega_n$ | natural frequency | rad/s |
| $f_n$ | natural frequency, $f_n = \omega_n / 2\pi$ | Hz |
| $\zeta$ | damping ratio | dimensionless |
| $\omega_d$ | damped natural frequency | rad/s |
| $r$ | frequency ratio, $r = \omega / \omega_n$ | dimensionless |
| $F$ | force | N |
| $x$ | displacement | m (plotted in µm) |
| $X/F$ | frequency response function (FRF) | µm/N |
| $H_1$ | measured FRF, H1 estimator | µm/N |
| $T_c$ | hammer tip contact time | s |
| $f_s$ | audio sample rate | Hz |
In the code cells, $\omega_n$ appears as wn, $\zeta$ as zeta, and $f_n$ as fn, matching this table; subscripted repeats such as fn_r or wn_x are local to a single cell and carry the same meanings and units.
Setup: Install and Import Libraries¶
Run the install cell once. If this is the first install on your machine, restart the kernel afterward (Kernel menu, Restart), then continue from the import cell.
%pip install -q numpy matplotlib plotly
Note: you may need to restart the kernel to use updated packages.
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import rcParams
from matplotlib.animation import FuncAnimation
from IPython.display import HTML, Audio, display
import plotly.graph_objects as go
from plotly.subplots import make_subplots
rcParams.update({
'font.size': 14,
'axes.labelsize': 14,
'xtick.labelsize': 12,
'ytick.labelsize': 12,
'figure.figsize': (8, 5),
'lines.linewidth': 1.5,
'figure.dpi': 100,
})
plt.rcParams['animation.embed_limit'] = 40 # MB
AUDIO_RATE = 48000 # Hz, sample rate for all synthesized sound
print('Libraries loaded.')
Libraries loaded.
The utility functions used throughout the lesson are defined in the collapsed cell below and documented in the Appendix.
# --- Utility functions used throughout the lesson (documented in the Appendix) ---
def fade(x, ms=15, fs=AUDIO_RATE):
"""Apply a short raised-cosine fade-in and fade-out so audio clips start
and stop without clicks."""
n = int(ms * 1e-3 * fs)
ramp = 0.5 * (1 - np.cos(np.pi * np.arange(n) / n))
y = x.copy()
y[:n] *= ramp
y[-n:] *= ramp[::-1]
return y
def compute_spectrum(x, fs, fmax=4000):
"""Amplitude spectrum of signal x on a linear scale, normalized to a peak
value of one. Returns the frequency vector (Hz, limited to fmax) and the
normalized spectrum."""
x = np.asarray(x, dtype=float)
w = np.hanning(len(x))
X = np.fft.rfft((x - x.mean()) * w)
f = np.fft.rfftfreq(len(x), 1 / fs)
amp = 2 * np.abs(X) / np.sum(w)
amp = amp / amp.max()
sel = f <= fmax
return f[sel], amp[sel]
def compute_spectrogram(x, fs, nfft=2048, hop=512, fmax=4000):
"""Short-time amplitude spectrogram on a linear scale, normalized to a
peak value of one. Returns (t_frames, f, S) where S has one column per
time frame and one row per frequency."""
w = np.hanning(nfft)
n_frames = 1 + max(0, (len(x) - nfft) // hop)
f = np.fft.rfftfreq(nfft, 1 / fs)
sel = f <= fmax
S = np.empty((int(sel.sum()), n_frames))
for j in range(n_frames):
S[:, j] = np.abs(np.fft.rfft(x[j * hop:j * hop + nfft] * w))[sel]
S /= S.max()
t_frames = (np.arange(n_frames) * hop + nfft / 2) / fs
return t_frames, f[sel], S
def decimate_for_plot(t, x, max_points=None):
"""Pass plotting data through unchanged by default (max_points=None).
To thin a long record for display, set max_points to an integer: each
output bin then keeps its local minimum and maximum, preserving the
visual envelope without aliasing. Computations always use the
full-rate data."""
n = len(x)
if max_points is None or n <= max_points:
return np.asarray(t), np.asarray(x)
bins = max_points // 2
edge = (n // bins) * bins
xb = np.asarray(x)[:edge].reshape(bins, -1)
tb = np.asarray(t)[:edge].reshape(bins, -1)
tt = np.repeat(tb.mean(axis=1), 2)
xx = np.empty(2 * bins)
xx[0::2] = xb.min(axis=1)
xx[1::2] = xb.max(axis=1)
return tt, xx
def half_sine_pulse(Tc, Fmax=200.0, dur=1.0, fs=AUDIO_RATE):
"""Half-sine hammer impact of contact time Tc (s) and peak force Fmax (N)
in a record of length dur (s). Returns the time vector and force record."""
t = np.arange(int(dur * fs)) / fs
F = np.where(t <= Tc, Fmax * np.sin(np.pi * t / Tc), 0.0)
return t, F
def sdof_response(F, fn=900.0, zeta=0.02, k=8e6, fs=AUDIO_RATE):
"""Displacement response (m) of the Sect. A.3 tool model to a force
record F (N), by semi-implicit Euler integration of Eq. 2.24 with the
force applied on the right side."""
wn = 2 * np.pi * fn
m = k / wn**2
c = 2 * zeta * np.sqrt(m * k)
dt = 1 / fs
x = np.zeros(len(F))
v = 0.0
for i in range(1, len(F)):
v += dt * (F[i - 1] - c * v - k * x[i - 1]) / m
x[i] = x[i - 1] + dt * v
return x
A.1 A Short History of Acoustics¶
Acoustics developed early because its central objects, strings and pipes, were readily available for controlled experiments. Galileo connected pitch to the frequency of vibration in the Two New Sciences of 1638 [3]. Mersenne published the quantitative laws of the vibrating string, relating frequency to length, tension, and mass, at nearly the same time [4]. Boyle showed in 1660 that sound requires a medium, by pumping the air from a vessel containing a ticking watch until the ticking could no longer be heard [5].
Newton calculated the speed of sound in air in the Principia of 1687, obtaining a value about 15 percent below measurement [6]. Laplace supplied the correction in 1816, accounting for the heating of the air during the rapid compressions of a sound wave. In 1822 Fourier stated the theorem this lesson series depends on, that a periodic function can be written as a sum of sinusoids [7]. Helmholtz explained timbre as the ear's perception of the strengths of those harmonics in 1863 [2]. Rayleigh's The Theory of Sound of 1877 organized the mechanics of vibrating systems and the propagation of sound into the mathematical treatment still in use [1]. The twentieth century added the sampling theory of Nyquist and Shannon and the fast Fourier transform of Cooley and Tukey [8]. The latter forms the basis of the computations used in this notebook.
Chatter entered the machining literature with F. W. Taylor's 1907 paper On the Art of Cutting Metals, which he called "the most obscure and delicate of all problems facing the machinist" [9]. Its mechanism, regeneration, was identified by Tobias, Tlusty, and their contemporaries in the 1950s and 1960s. Regenerative chatter is the subject of Part C.
A.2 The Three Kinds of Vibration¶
All vibrating systems can be described by one of three categories [10]:
Free vibration occurs when a system is displaced from its equilibrium position, released, and allowed to vibrate on its own. It vibrates at its natural frequency, and, in any real system, the motion decays away. A struck tuning fork or end mill exhibits free vibration.
Forced vibration occurs when a system is driven by an external force that continues in time. The system vibrates at the forcing frequency, whatever its value, with an amplitude that depends on how close the forcing frequency is to the natural frequency. An unbalanced washing machine on spin cycle represents forced vibration. A milling cut, where the teeth strike the workpiece in a steady rhythm, also represents forced vibration.
Self-excited vibration occurs when a steady input of energy is converted, by the system itself, into vibration at or near its own natural frequency. A bowed violin string exhibits self-excited vibration. Chatter in machining is also a self-excited vibration, although the mechanisms are different.
This classification is the backbone of the whole lesson. Each kind of vibration produces a characteristic sound and learning to tell them apart by ear is the practical skill we are building. Listen to the following three previews: the ring of a tapped tool (free), the hum of a stable milling cut (forced), and the harsh mixture of chatter and forced vibration (self-excited, with its mechanism description deferred to Parts B and C).
▸ Key terms: free vibration, forced vibration, self-excited vibration
# --- The three kinds of vibration, as sound ---
dur = 2.0
t = np.arange(int(dur * AUDIO_RATE)) / AUDIO_RATE
# free vibration: ring-down of a tool-like system (fn = 900 Hz, zeta = 0.02)
fn, zeta = 900.0, 0.02
wn = 2 * np.pi * fn
wd = wn * np.sqrt(1 - zeta**2)
free = 0.8 * np.exp(-zeta * wn * t) * np.sin(wd * t)
# forced vibration: steady hum at a 250 Hz forcing frequency and its harmonics
forced = np.zeros_like(t)
for k in range(1, 9):
forced += (1 / k) * np.sin(2 * np.pi * k * 250 * t)
forced *= 0.5 / np.max(np.abs(forced))
# self-excited vibration preview: the same hum plus a strong tone near the
# natural frequency that does not belong to the 250 Hz family
self_excited = forced + 0.7 * np.sin(2 * np.pi * 913 * t)
self_excited *= 0.5 / np.max(np.abs(self_excited))
fig = make_subplots(rows=1, cols=3, shared_yaxes=True,
subplot_titles=('free (ring-down)', 'forced (steady hum)',
'self-excited (harsh mixture)'))
n_show = int(0.04 * AUDIO_RATE)
for col, sig in enumerate([free, forced, self_excited], start=1):
tt, xx = decimate_for_plot(t[:n_show] * 1e3, sig[:n_show])
fig.add_trace(go.Scatter(x=tt, y=xx, mode='lines',
line=dict(color='#1f77b4', width=1),
showlegend=False), row=1, col=col)
fig.update_xaxes(title_text='t (ms)', row=1, col=col)
fig.update_yaxes(title_text='Amplitude', row=1, col=1)
fig.update_layout(title='Fig. A.1 — The three kinds of vibration (first 40 ms of each)',
template='simple_white', height=340,
margin=dict(l=60, r=20, t=80, b=50))
fig.show()
for name, sig in [('Free vibration (a tapped tool)', free),
('Forced vibration (a stable cut)', forced),
('Self-excited vibration (chatter, preview only)', self_excited)]:
print(name)
display(Audio(fade(sig), rate=AUDIO_RATE))
Free vibration (a tapped tool)
Forced vibration (a stable cut)
Self-excited vibration (chatter, preview only)
Free vibration dies away on its own; if the sound persists, something is still supplying energy. Forced vibration is steady and clean. Its pitch is set entirely by the forcing frequency. Change the forcing rhythm and the pitch follows. Self-excited vibration is the troublemaker: the system converts a steady energy supply into vibration at a frequency of its own choosing, near its natural frequency, so the sound contains a new component, the chatter frequency, that does not follow the forcing rhythm. In the preceding third clip, the 250 Hz hum and its multiples are the forced part, and the 913 Hz tone riding on top is not related to the forcing frequency family. In later Parts we detect chatter by finding such a tone.
A.3 The Lumped Parameter Model¶
We begin with a simple, lumped parameter model where all the mass is concentrated at a single coordinate location and the spring is massless. A mass $m$ is attached to a linear spring $k$ that provides a restoring force proportional to the mass displacement from its equilibrium position. This is the position where the spring has already stretched due to the weight of the mass element. For free vibration with no damping, the equation of motion is:
$$m\ddot{x} + kx = 0 \tag{2.1}$$
Two physical ingredients appear here, and only two: an elastic restoring force, described by the stiffness $k$, and an inertial force, described by the mass $m$. Displace the mass, and the spring pulls it back; inertia carries it through equilibrium and, with no damping to remove energy from the motion, it overshoots; the spring pulls it back again. The result is oscillation at the natural frequency:
$$\omega_n = \sqrt{\frac{k}{m}}$$
in rad/s, or $f_n = \omega_n / (2\pi)$ in Hz. Stiffer systems vibrate at higher natural frequencies; heavier systems vibrate at lower natural frequencies. The motion itself is a cosine at that frequency:
$$x = 2A\cos(\omega_n t + \beta) \tag{2.14}$$
where the amplitude $A$ and phase $\beta$ come from how the motion was started (the initial displacement and velocity). Without damping this vibration would continue forever, which no real system does; damping is the subject of the next section.
A milling tool clamped in its holder is, to a good first approximation, exactly this system. Typical values for the tool tip dynamics are a stiffness of a few to a few tens of N/µm and natural frequencies from several hundred Hz to several kHz. These frequencies are within the range of human hearing. Let's compute one.
▸ Key terms: stiffness, mass, natural frequency
# --- A tool-like example: from k and fn to m ---
k = 8e6 # N/m (8 N/um), a representative tool tip stiffness
fn = 900.0 # Hz, a representative tool tip natural frequency
wn = 2 * np.pi * fn
m = k / wn**2 # the mass consistent with these values
print(f'Stiffness k = {k:.1e} N/m ({k / 1e6:.0f} N/um)')
print(f'Natural frequency fn = {fn:.0f} Hz (omega_n = {wn:.0f} rad/s)')
print(f'Equivalent mass m = k / omega_n^2 = {m:.3f} kg')
Stiffness k = 8.0e+06 N/m (8 N/um) Natural frequency fn = 900 Hz (omega_n = 5655 rad/s) Equivalent mass m = k / omega_n^2 = 0.250 kg
Animation: Spring-Mass Free Vibration¶
The animation displays the motion of the physical system (the mass on its spring) over time. The red dot marks the current position on the motion trace. This combination, the object on one side and its signal on the other, is how we will look at every system in this lesson.
# --- Animation: undamped spring-mass drawing its displacement trace ---
f_anim = 2.0 # Hz. Try changing this!
A_anim = 1.0
t_anim = np.linspace(0, 2.0, 200)
x_anim = A_anim * np.sin(2 * np.pi * f_anim * t_anim)
fig_a, (ax_sys, ax_tr) = plt.subplots(1, 2, figsize=(12, 5),
gridspec_kw={'width_ratios': [1, 2]})
fig_a.subplots_adjust(wspace=0.25)
plt.close(fig_a) # Prevent static display of empty figure
def spring_pts(y_top, y_bot, n_coils=8, width=0.22):
n_pts = 2 * n_coils + 2
y_pts = np.linspace(y_top, y_bot, n_pts)
x_pts = np.zeros(n_pts)
x_pts[1:-1] = width * (-1) ** np.arange(2 * n_coils)
return x_pts, y_pts
def init():
return []
def animate(frame):
ax_sys.cla()
ax_sys.set_xlim(-1.5, 1.5)
ax_sys.set_ylim(-2.6, 2.6)
ax_sys.set_aspect('equal')
ax_sys.set_axis_off()
ax_sys.set_title('Spring-mass system (Eq. 2.1)')
ax_sys.plot([-0.8, 0.8], [2.2, 2.2], 'k-', lw=3) # fixed wall
ax_sys.fill_between([-0.8, 0.8], 2.2, 2.4, color='gray', alpha=0.5)
mass_y = x_anim[frame]
sx, sy = spring_pts(2.2, mass_y + 0.25)
ax_sys.plot(sx, sy, 'k-', lw=1.5)
rect = plt.Rectangle((-0.4, mass_y - 0.25), 0.8, 0.5,
fc='#1f77b4', ec='black', lw=2)
ax_sys.add_patch(rect)
ax_tr.cla()
ax_tr.set_xlim(0, t_anim[-1])
ax_tr.set_ylim(-1.3, 1.3)
ax_tr.set_xlabel('t (s)')
ax_tr.set_ylabel('x')
ax_tr.set_title(f'x(t) at f = {f_anim} Hz (Eq. 2.14)')
ax_tr.grid(True, alpha=0.3)
ax_tr.plot(t_anim[:frame + 1], x_anim[:frame + 1], color='#1f77b4')
ax_tr.plot(t_anim[frame], x_anim[frame], 'o', color='#d62728', markersize=6)
return []
anim = FuncAnimation(fig_a, animate, init_func=init,
frames=len(t_anim), interval=40, blit=False)
HTML(anim.to_jshtml())