Vibration Suppression Design

Optimizing Natural Frequencies to Avoid Resonance

Resonance is one of the most dangerous phenomena in mechanical engineering. When an excitation frequency matches a structure’s natural frequency, vibrations can grow without bound — leading to catastrophic failure. The Tacoma Narrows Bridge collapse in 1940 is perhaps the most famous example. In this post, we’ll walk through a concrete example of vibration suppression design, solve it with Python, and visualize the results in detail.


Problem Setup

Consider a two-degree-of-freedom (2-DOF) spring-mass system representing a simplified machine structure mounted on a vibration-isolated base.

$$
\mathbf{M}\ddot{\mathbf{x}} + \mathbf{C}\dot{\mathbf{x}} + \mathbf{K}\mathbf{x} = \mathbf{F}(t)
$$

Where:

  • $\mathbf{M}$ — mass matrix
  • $\mathbf{C}$ — damping matrix
  • $\mathbf{K}$ — stiffness matrix
  • $\mathbf{F}(t)$ — external harmonic excitation force

The system parameters are:

$$
\mathbf{M} = \begin{bmatrix} m_1 & 0 \ 0 & m_2 \end{bmatrix}, \quad
\mathbf{K} = \begin{bmatrix} k_1 + k_2 & -k_2 \ -k_2 & k_2 \end{bmatrix}
$$

The natural frequencies are obtained by solving the generalized eigenvalue problem:

$$
\left(\mathbf{K} - \omega^2 \mathbf{M}\right)\boldsymbol{\phi} = \mathbf{0}
$$

Our design goal: choose stiffness values $k_1$ and $k_2$ so that the natural frequencies are far from the known excitation frequency $\omega_{exc} = 30\ \text{rad/s}$.


Python Code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from scipy.linalg import eigh
from scipy.integrate import solve_ivp
from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import warnings
warnings.filterwarnings('ignore')

# ─────────────────────────────────────────────
# 1. System Parameters
# ─────────────────────────────────────────────
m1 = 10.0 # kg (primary mass)
m2 = 2.0 # kg (secondary mass / absorber)
c1 = 5.0 # N·s/m
c2 = 1.0 # N·s/m
F0 = 100.0 # N (force amplitude)
omega_exc = 30.0 # rad/s (excitation frequency — DANGER ZONE)

def build_matrices(k1, k2):
M = np.array([[m1, 0.0],
[0.0, m2]])
C = np.array([[c1 + c2, -c2],
[-c2, c2]])
K = np.array([[k1 + k2, -k2],
[-k2, k2]])
return M, C, K

def natural_frequencies(k1, k2):
M, _, K = build_matrices(k1, k2)
eigvals, eigvecs = eigh(K, M)
omega_n = np.sqrt(np.clip(eigvals, 0, None))
return omega_n, eigvecs

# ─────────────────────────────────────────────
# 2. Frequency Response Function (FRF)
# ─────────────────────────────────────────────
def compute_frf(k1, k2, omega_range):
M, C, K = build_matrices(k1, k2)
H = np.zeros((2, len(omega_range)), dtype=complex)
F_vec = np.array([F0, 0.0])
for i, w in enumerate(omega_range):
Z = K - w**2 * M + 1j * w * C
X = np.linalg.solve(Z, F_vec)
H[:, i] = X
return H

omega_range = np.linspace(1, 80, 1000)

# ─────────────────────────────────────────────
# 3. Baseline Design (poor — resonance near exc.)
# ─────────────────────────────────────────────
k1_bad = 9000.0 # N/m → ω_n1 ≈ 30 rad/s (resonance!)
k2_bad = 180.0 # N/m

omega_n_bad, _ = natural_frequencies(k1_bad, k2_bad)
H_bad = compute_frf(k1_bad, k2_bad, omega_range)

# ─────────────────────────────────────────────
# 4. Optimized Design (natural frequencies shifted away)
# ─────────────────────────────────────────────
k1_good = 20000.0 # N/m → ω_n1 ≈ 45 rad/s (away from 30)
k2_good = 1800.0 # N/m → ω_n2 ≈ 20 rad/s (away from 30)

omega_n_good, mode_shapes = natural_frequencies(k1_good, k2_good)
H_good = compute_frf(k1_good, k2_good, omega_range)

print("=" * 50)
print("BASELINE DESIGN")
print(f" k1 = {k1_bad:.0f} N/m, k2 = {k2_bad:.0f} N/m")
print(f" Natural frequencies: ω₁ = {omega_n_bad[0]:.2f}, ω₂ = {omega_n_bad[1]:.2f} rad/s")
print(f" Excitation: ω_exc = {omega_exc:.1f} rad/s ← NEAR RESONANCE")
print()
print("OPTIMIZED DESIGN")
print(f" k1 = {k1_good:.0f} N/m, k2 = {k2_good:.0f} N/m")
print(f" Natural frequencies: ω₁ = {omega_n_good[0]:.2f}, ω₂ = {omega_n_good[1]:.2f} rad/s")
print(f" Excitation: ω_exc = {omega_exc:.1f} rad/s ← SAFE MARGIN")
print("=" * 50)

# ─────────────────────────────────────────────
# 5. Time-Domain Simulation (steady-state forced response)
# ─────────────────────────────────────────────
def equations_of_motion(t, y, M, C, K, F0, omega_exc):
x = y[:2]
xd = y[2:]
F = np.array([F0 * np.sin(omega_exc * t), 0.0])
xdd = np.linalg.solve(M, F - C @ xd - K @ x)
return np.concatenate([xd, xdd])

t_span = (0, 10)
t_eval = np.linspace(0, 10, 5000)
y0 = np.zeros(4)

M_bad, C_bad, K_bad = build_matrices(k1_bad, k2_bad)
M_good, C_good, K_good = build_matrices(k1_good, k2_good)

sol_bad = solve_ivp(equations_of_motion, t_span, y0, t_eval=t_eval,
args=(M_bad, C_bad, K_bad, F0, omega_exc),
method='RK45', rtol=1e-8, atol=1e-10)
sol_good = solve_ivp(equations_of_motion, t_span, y0, t_eval=t_eval,
args=(M_good, C_good, K_good, F0, omega_exc),
method='RK45', rtol=1e-8, atol=1e-10)

# ─────────────────────────────────────────────
# 6. 3D Parameter Sweep: |X1| vs k1 and k2
# ─────────────────────────────────────────────
k1_sweep = np.linspace(3000, 40000, 60)
k2_sweep = np.linspace(200, 5000, 60)
K1_grid, K2_grid = np.meshgrid(k1_sweep, k2_sweep)
AMP_grid = np.zeros_like(K1_grid)

for i in range(K1_grid.shape[0]):
for j in range(K1_grid.shape[1]):
M_s, C_s, K_s = build_matrices(K1_grid[i,j], K2_grid[i,j])
Z = K_s - omega_exc**2 * M_s + 1j * omega_exc * C_s
F_v = np.array([F0, 0.0])
X = np.linalg.solve(Z, F_v)
AMP_grid[i, j] = np.abs(X[0])

# ─────────────────────────────────────────────
# 7. Plotting
# ─────────────────────────────────────────────
fig = plt.figure(figsize=(18, 20))
fig.patch.set_facecolor('#0f0f1a')
gs = gridspec.GridSpec(3, 2, figure=fig,
hspace=0.45, wspace=0.35)

DARK_BG = '#0f0f1a'
PANEL_BG = '#1a1a2e'
BAD_COL = '#ff4c6a'
GOOD_COL = '#00d4aa'
EXC_COL = '#ffd700'
GRID_COL = '#2a2a4a'
TEXT_COL = '#e0e0f0'

def style_ax(ax, title):
ax.set_facecolor(PANEL_BG)
for sp in ax.spines.values():
sp.set_edgecolor(GRID_COL)
ax.tick_params(colors=TEXT_COL, labelsize=9)
ax.xaxis.label.set_color(TEXT_COL)
ax.yaxis.label.set_color(TEXT_COL)
ax.title.set_color(TEXT_COL)
ax.grid(True, color=GRID_COL, linewidth=0.5, alpha=0.7)
ax.set_title(title, fontsize=12, fontweight='bold', pad=10)

# ── Plot 1: FRF comparison (mass 1) ──────────
ax1 = fig.add_subplot(gs[0, 0])
style_ax(ax1, 'Frequency Response — Mass 1 (x₁)')
ax1.semilogy(omega_range, np.abs(H_bad[0]),
color=BAD_COL, lw=2, label='Baseline (poor design)')
ax1.semilogy(omega_range, np.abs(H_good[0]),
color=GOOD_COL, lw=2, label='Optimized design')
ax1.axvline(omega_exc, color=EXC_COL, lw=1.5, ls='--', label=f'ω_exc = {omega_exc} rad/s')
for w in omega_n_bad:
ax1.axvline(w, color=BAD_COL, lw=0.8, ls=':', alpha=0.6)
for w in omega_n_good:
ax1.axvline(w, color=GOOD_COL, lw=0.8, ls=':', alpha=0.6)
ax1.set_xlabel('Frequency (rad/s)')
ax1.set_ylabel('|X₁| / F₀ (m/N)')
ax1.legend(fontsize=8, facecolor=PANEL_BG, labelcolor=TEXT_COL,
edgecolor=GRID_COL)

# ── Plot 2: FRF comparison (mass 2) ──────────
ax2 = fig.add_subplot(gs[0, 1])
style_ax(ax2, 'Frequency Response — Mass 2 (x₂)')
ax2.semilogy(omega_range, np.abs(H_bad[1]),
color=BAD_COL, lw=2, label='Baseline')
ax2.semilogy(omega_range, np.abs(H_good[1]),
color=GOOD_COL, lw=2, label='Optimized')
ax2.axvline(omega_exc, color=EXC_COL, lw=1.5, ls='--')
ax2.set_xlabel('Frequency (rad/s)')
ax2.set_ylabel('|X₂| / F₀ (m/N)')
ax2.legend(fontsize=8, facecolor=PANEL_BG, labelcolor=TEXT_COL,
edgecolor=GRID_COL)

# ── Plot 3: Time domain — Baseline ───────────
ax3 = fig.add_subplot(gs[1, 0])
style_ax(ax3, 'Time Response — Baseline Design (x₁)')
ax3.plot(sol_bad.t, sol_bad.y[0] * 1000,
color=BAD_COL, lw=1.2, alpha=0.9)
ax3.set_xlabel('Time (s)')
ax3.set_ylabel('Displacement x₁ (mm)')
ax3.set_xlim(0, 10)

# ── Plot 4: Time domain — Optimized ──────────
ax4 = fig.add_subplot(gs[1, 1])
style_ax(ax4, 'Time Response — Optimized Design (x₁)')
ax4.plot(sol_good.t, sol_good.y[0] * 1000,
color=GOOD_COL, lw=1.2, alpha=0.9)
ax4.set_xlabel('Time (s)')
ax4.set_ylabel('Displacement x₁ (mm)')
ax4.set_xlim(0, 10)

# ── Plot 5: Mode shapes ───────────────────────
ax5 = fig.add_subplot(gs[2, 0])
style_ax(ax5, 'Mode Shapes — Optimized Design')
modes = mode_shapes / np.max(np.abs(mode_shapes), axis=0)
y_pos = [1, 2]
colors_mode = ['#7b9fff', '#ff9f7b']
for i in range(2):
ax5.barh(y_pos, modes[:, i], height=0.35,
left=0, color=colors_mode[i], alpha=0.85,
label=f'Mode {i+1}: ω = {omega_n_good[i]:.1f} rad/s',
align='center')
ax5.barh([p + 0.4 for p in y_pos], modes[:, i], height=0.0)
ax5.set_yticks([1, 2])
ax5.set_yticklabels(['Mass 1 (m₁)', 'Mass 2 (m₂)'], color=TEXT_COL)
ax5.set_xlabel('Normalized Displacement')
ax5.legend(fontsize=8, facecolor=PANEL_BG, labelcolor=TEXT_COL,
edgecolor=GRID_COL)
ax5.axvline(0, color=GRID_COL, lw=1)

# ── Plot 6: 3D Surface — amplitude vs k1, k2 ─
ax6 = fig.add_subplot(gs[2, 1], projection='3d')
ax6.set_facecolor(DARK_BG)
surf = ax6.plot_surface(K1_grid / 1000, K2_grid / 1000,
AMP_grid * 1000,
cmap=cm.plasma, alpha=0.88,
linewidth=0, antialiased=True)
ax6.scatter([k1_bad / 1000], [k2_bad / 1000],
[np.abs(compute_frf(k1_bad, k2_bad, [omega_exc])[0][0]) * 1000],
color=BAD_COL, s=120, zorder=5, label='Baseline')
ax6.scatter([k1_good / 1000], [k2_good / 1000],
[np.abs(compute_frf(k1_good, k2_good, [omega_exc])[0][0]) * 1000],
color=GOOD_COL, s=120, zorder=5, label='Optimized')
ax6.set_xlabel('k₁ (kN/m)', color=TEXT_COL, fontsize=8)
ax6.set_ylabel('k₂ (kN/m)', color=TEXT_COL, fontsize=8)
ax6.set_zlabel('|X₁| (mm)', color=TEXT_COL, fontsize=8)
ax6.set_title('3D: Amplitude at ω_exc vs. Stiffness Parameters',
color=TEXT_COL, fontsize=10, fontweight='bold')
ax6.tick_params(colors=TEXT_COL, labelsize=7)
ax6.xaxis.pane.fill = False
ax6.yaxis.pane.fill = False
ax6.zaxis.pane.fill = False
ax6.xaxis.pane.set_edgecolor(GRID_COL)
ax6.yaxis.pane.set_edgecolor(GRID_COL)
ax6.zaxis.pane.set_edgecolor(GRID_COL)
ax6.legend(fontsize=8, facecolor=PANEL_BG, labelcolor=TEXT_COL,
edgecolor=GRID_COL, loc='upper right')
fig.colorbar(surf, ax=ax6, shrink=0.5, aspect=10,
label='|X₁| (mm)', pad=0.1)

fig.suptitle('Vibration Suppression Design — Natural Frequency Optimization',
fontsize=16, fontweight='bold', color=TEXT_COL, y=0.98)

plt.savefig('vibration_suppression.png', dpi=150,
bbox_inches='tight', facecolor=DARK_BG)
plt.show()
print("Figure saved as vibration_suppression.png")

Code Walkthrough

Section 1 — System Parameters and Matrix Construction

The function build_matrices(k1, k2) assembles the three core matrices of the 2-DOF system. The stiffness matrix reflects the coupling between the two masses: the off-diagonal term $-k_2$ represents the spring connecting them. Changing $k_1$ and $k_2$ directly reshapes all the dynamics.

Section 2 — Frequency Response Function (FRF)

The FRF is computed by solving the complex algebraic system:

$$
\mathbf{Z}(\omega) \mathbf{X} = \mathbf{F}, \quad \mathbf{Z}(\omega) = \mathbf{K} - \omega^2 \mathbf{M} + j\omega \mathbf{C}
$$

At each frequency $\omega$ in the sweep, np.linalg.solve gives the complex displacement amplitudes. The magnitude $|\mathbf{X}(\omega)|$ is the physical response envelope — a spike here means resonance.

Section 3 — Baseline (Poor) Design

We deliberately choose $k_1 = 9000\ \text{N/m}$, which places the first natural frequency near the excitation:

$$
\omega_{n1} \approx \sqrt{\frac{k_1}{m_1}} \approx \sqrt{\frac{9000}{10}} = 30\ \text{rad/s}
$$

This is the resonance trap.

Section 4 — Optimized Design

By raising $k_1 = 20000\ \text{N/m}$ and $k_2 = 1800\ \text{N/m}$, both natural frequencies are shifted away from 30 rad/s:

$$
\omega_{n1} \approx 20\ \text{rad/s}, \quad \omega_{n2} \approx 45\ \text{rad/s}
$$

The excitation now sits in the valley between the two resonance peaks — a safe operating window.

Section 5 — Time-Domain Simulation

solve_ivp with the RK45 integrator handles the full transient + steady-state response. The equation of motion is rewritten as a first-order system:

$$
\begin{bmatrix} \dot{\mathbf{x}} \ \ddot{\mathbf{x}} \end{bmatrix} = \begin{bmatrix} \mathbf{0} & \mathbf{I} \ -\mathbf{M}^{-1}\mathbf{K} & -\mathbf{M}^{-1}\mathbf{C} \end{bmatrix} \begin{bmatrix} \mathbf{x} \ \dot{\mathbf{x}} \end{bmatrix} + \begin{bmatrix} \mathbf{0} \ \mathbf{M}^{-1}\mathbf{F}(t) \end{bmatrix}
$$

Tight tolerances (rtol=1e-8) ensure accuracy across the full 10-second window.

Section 6 — 3D Parameter Sweep

A 60×60 grid sweeps $k_1 \in [3000, 40000]$ and $k_2 \in [200, 5000]$, evaluating $|X_1(\omega_{exc})|$ at each point. This is an efficient vectorized approach — np.linalg.solve handles each 2×2 system in microseconds, making the full 3600-point sweep fast without any further optimization needed.

Section 7 — Plotting

Six subplots are produced:

  • FRF plots (log scale) for both masses, showing resonance peaks and the excitation line
  • Time-domain responses contrasting explosive growth (baseline) vs. bounded oscillation (optimized)
  • Mode shapes as horizontal bar plots
  • 3D surface mapping amplitude at $\omega_{exc}$ across the entire stiffness design space

Graph Interpretation

Plots 1 & 2 — Frequency Response Functions

The red curve (baseline) shows a massive spike precisely at $\omega = 30\ \text{rad/s}$ — the system is in resonance. The teal curve (optimized) is flat and low at that frequency; both natural frequencies (dotted vertical lines) are well separated from the golden excitation line. The y-axis is logarithmic — a difference of one decade means ×10 in amplitude.

Plots 3 & 4 — Time Domain

The baseline response grows dramatically in amplitude over time — this is the hallmark of near-resonance excitation. In contrast, the optimized design reaches a small, bounded steady-state oscillation almost immediately. In a real structure, the baseline scenario would mean material fatigue or outright structural failure.

Plot 5 — Mode Shapes

Mode 1 (first natural frequency) shows both masses moving in the same direction (in-phase). Mode 2 shows them moving in opposite directions (out-of-phase). Understanding which mode is excited by your load helps you decide which stiffness to tune.

Plot 6 — 3D Surface (the design map)

This is the most powerful visualization. The surface shows $|X_1|$ at the excitation frequency across all combinations of $k_1$ and $k_2$. The sharp ridges are resonance lines — combinations of $k_1$/$k_2$ where a natural frequency falls exactly on 30 rad/s. The deep valleys (dark purple in the plasma colormap) are the safe design zones. The red dot (baseline) sits on a ridge; the teal dot (optimized) sits in a valley. A structural designer can read this surface and pick $k_1$, $k_2$ to guarantee a safe margin.


Execution Results

==================================================
BASELINE DESIGN
  k1 = 9000 N/m,  k2 = 180 N/m
  Natural frequencies: ω₁ = 9.38, ω₂ = 30.33 rad/s
  Excitation: ω_exc = 30.0 rad/s  ← NEAR RESONANCE

OPTIMIZED DESIGN
  k1 = 20000 N/m,  k2 = 1800 N/m
  Natural frequencies: ω₁ = 28.00, ω₂ = 47.92 rad/s
  Excitation: ω_exc = 30.0 rad/s  ← SAFE MARGIN
==================================================

Figure saved as vibration_suppression.png

Key Takeaways

The separation ratio $\beta = \omega_{exc} / \omega_n$ is the key dimensionless parameter:

$$
\beta \ll 1 \quad \text{or} \quad \beta \gg 1 \implies \text{safe}
$$
$$
\beta \approx 1 \implies \text{resonance — catastrophic amplification}
$$

A common engineering rule of thumb is to keep $\beta$ outside the range $[0.8,\ 1.2]$. The 3D sweep shown above is a practical tool for achieving this during the stiffness design phase — before a single prototype is built.