Models a spring-mass-damper system, sweeps damping ratio,
and finds the optimal damping for minimum settling time.
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
# -------------------------------------------------------
# System parameters (quarter-car model)
# -------------------------------------------------------
MASS = 250.0 # Sprung mass in kg (quarter of a 1000 kg car)
STIFFNESS = 15000.0 # Spring stiffness in N/m
X0 = 0.05 # Initial displacement in meters (5 cm bump)
V0 = 0.0 # Initial velocity in m/s
OMEGA_N = np.sqrt(STIFFNESS / MASS)
C_CRITICAL = 2 * np.sqrt(STIFFNESS * MASS)
F_N = OMEGA_N / (2 * np.pi)
print(" SUSPENSION SYSTEM PARAMETERS")
print(f" Mass: {MASS:.0f} kg")
print(f" Spring stiffness: {STIFFNESS:.0f} N/m")
print(f" Natural frequency: {F_N:.2f} Hz ({OMEGA_N:.2f} rad/s)")
print(f" Critical damping: {C_CRITICAL:.0f} N*s/m")
print(f" Initial bump: {X0*100:.1f} cm")
def spring_mass_damper_ode(t, y, c):
ODE for spring-mass-damper system.
y = [x, v] where x is displacement and v is velocity.
dvdt = -(c / MASS) * v - (STIFFNESS / MASS) * x
def simulate(c, t_end=2.0, n_points=2000):
Simulate the system with damping coefficient c.
Returns time, displacement, velocity arrays.
lambda t, y: spring_mass_damper_ode(t, y, c),
t = np.linspace(0, t_end, n_points)
def find_settling_time(t, x, threshold_fraction=0.02):
Find the 2% settling time: the last time the displacement
exceeds 2% of the initial displacement.
threshold = threshold_fraction * abs(X0)
settled = np.abs(x) < threshold
return t[-1] # Never settled within simulation window
# Find the last time it was NOT settled
not_settled = np.where(~settled)[0]
if len(not_settled) == 0:
last_unsettled_idx = not_settled[-1]
if last_unsettled_idx >= len(t) - 1:
return t[last_unsettled_idx + 1]
Find the percent overshoot (maximum excursion beyond zero
on the opposite side of the initial displacement).
# For a system starting at positive X0 and settling to 0,
# overshoot is the maximum negative excursion
return abs(min_x) / X0 * 100.0
# -------------------------------------------------------
# 1. Compare three damping regimes
# -------------------------------------------------------
def plot_three_regimes():
"""Plot underdamped, critically damped, and overdamped responses."""
zeta_values = [0.2, 0.5, 1.0, 2.0]
colors = ["tab:red", "tab:blue", "tab:green", "tab:purple"]
"zeta=0.2 (underdamped)",
"zeta=0.5 (underdamped)",
"zeta=1.0 (critically damped)",
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
fig.suptitle("Spring-Mass-Damper: Effect of Damping Ratio",
fontsize=13, fontweight="bold")
for zeta, color, label in zip(zeta_values, colors, labels):
t, x, v = simulate(c, t_end=2.0)
axes[0].plot(t, x * 100, color=color, linewidth=2, label=label)
axes[1].plot(x * 100, v * 100, color=color, linewidth=1.5, label=label)
axes[1].plot(x[0] * 100, v[0] * 100, "o", color=color, markersize=6)
# Time response formatting
axes[0].axhline(y=0, color="black", linewidth=0.5)
axes[0].set_xlabel("Time (s)")
axes[0].set_ylabel("Displacement (cm)")
axes[0].set_title("Step Response")
axes[0].legend(fontsize=8)
axes[0].grid(True, alpha=0.3)
# Phase portrait formatting
axes[1].plot(0, 0, "kx", markersize=10, markeredgewidth=2) # Equilibrium
axes[1].set_xlabel("Displacement (cm)")
axes[1].set_ylabel("Velocity (cm/s)")
axes[1].set_title("Phase Portrait")
axes[1].legend(fontsize=8)
axes[1].grid(True, alpha=0.3)
plt.savefig("damping_comparison.png", dpi=150, bbox_inches="tight")
print("Saved: damping_comparison.png\n")
# -------------------------------------------------------
# 2. Energy dissipation over time
# -------------------------------------------------------
"""Show how energy is dissipated for different damping ratios."""
zeta_values = [0.1, 0.5, 1.0]
colors = ["tab:red", "tab:blue", "tab:green"]
fig, axes = plt.subplots(1, 3, figsize=(14, 4))
fig.suptitle("Energy Dissipation in Spring-Mass-Damper System",
fontsize=13, fontweight="bold")
for idx, (zeta, color) in enumerate(zip(zeta_values, colors)):
t, x, v = simulate(c, t_end=3.0)
pe = 0.5 * STIFFNESS * x**2
axes[idx].fill_between(t, 0, ke, alpha=0.4, color="tab:orange",
axes[idx].fill_between(t, ke, ke + pe, alpha=0.4, color="tab:cyan",
axes[idx].plot(t, total, "k--", linewidth=1, label="Total",
axes[idx].set_xlabel("Time (s)")
axes[idx].set_ylabel("Energy (J)")
axes[idx].set_title(f"zeta = {zeta}")
axes[idx].legend(fontsize=7)
axes[idx].grid(True, alpha=0.3)
axes[idx].set_ylim(bottom=0)
plt.savefig("energy_dissipation.png", dpi=150, bbox_inches="tight")
print("Saved: energy_dissipation.png\n")
# -------------------------------------------------------
# 3. Sweep damping ratio and find optimal
# -------------------------------------------------------
def sweep_and_optimize():
Sweep zeta from 0.1 to 3.0, compute settling time and overshoot
for each value, and find the optimal damping ratio.
zeta_range = np.linspace(0.1, 3.0, 200)
t, x, v = simulate(c, t_end=5.0, n_points=5000)
ts = find_settling_time(t, x, threshold_fraction=0.02)
os_pct = find_overshoot(x)
settling_times.append(ts)
overshoots.append(os_pct)
settling_times = np.array(settling_times)
overshoots = np.array(overshoots)
# Find optimal: minimum settling time
idx_opt = np.argmin(settling_times)
zeta_opt = zeta_range[idx_opt]
ts_opt = settling_times[idx_opt]
os_opt = overshoots[idx_opt]
print(" OPTIMIZATION RESULTS")
print(f" Optimal damping ratio: {zeta_opt:.3f}")
print(f" Optimal damping coeff: {zeta_opt * C_CRITICAL:.0f} N*s/m")
print(f" Settling time (2%): {ts_opt*1000:.0f} ms")
print(f" Overshoot: {os_opt:.1f}%")
# Also find optimal with overshoot constraint (< 10%)
constrained_settling = settling_times.copy()
constrained_settling[~mask] = np.inf
idx_constrained = np.argmin(constrained_settling)
zeta_constrained = zeta_range[idx_constrained]
ts_constrained = settling_times[idx_constrained]
os_constrained = overshoots[idx_constrained]
print(f"\n With overshoot < 10% constraint:")
print(f" Optimal damping ratio: {zeta_constrained:.3f}")
print(f" Settling time (2%): {ts_constrained*1000:.0f} ms")
print(f" Overshoot: {os_constrained:.1f}%")
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
fig.suptitle("Suspension Tuning: Damping Ratio Sweep",
fontsize=13, fontweight="bold")
axes[0].plot(zeta_range, settling_times * 1000, "tab:blue", linewidth=2)
axes[0].axvline(x=zeta_opt, color="red", linestyle="--", alpha=0.7,
label=f"Optimal: zeta={zeta_opt:.3f}")
axes[0].axvline(x=1.0, color="gray", linestyle=":", alpha=0.5,
label="Critical damping")
axes[0].set_xlabel("Damping Ratio (zeta)")
axes[0].set_ylabel("2% Settling Time (ms)")
axes[0].set_title("Settling Time vs. Damping Ratio")
axes[0].legend(fontsize=8)
axes[0].grid(True, alpha=0.3)
axes[1].plot(zeta_range, overshoots, "tab:orange", linewidth=2)
axes[1].axhline(y=10, color="red", linestyle="--", alpha=0.5,
label="10% overshoot limit")
axes[1].axvline(x=1.0, color="gray", linestyle=":", alpha=0.5,
label="Critical damping")
axes[1].set_xlabel("Damping Ratio (zeta)")
axes[1].set_ylabel("Overshoot (%)")
axes[1].set_title("Overshoot vs. Damping Ratio")
axes[1].legend(fontsize=8)
axes[1].grid(True, alpha=0.3)
plt.savefig("suspension_tuning.png", dpi=150, bbox_inches="tight")
print("Saved: suspension_tuning.png\n")
# Plot the optimal response
c_opt = zeta_opt * C_CRITICAL
t, x, v = simulate(c_opt, t_end=2.0)
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot(t, x * 100, "tab:blue", linewidth=2,
label=f"Optimal (zeta={zeta_opt:.3f})")
# Compare with critical and a typical underdamped
for zeta, color, label in [(0.3, "tab:red", "zeta=0.3"),
(1.0, "tab:green", "zeta=1.0")]:
t2, x2, _ = simulate(c, t_end=2.0)
ax.plot(t2, x2 * 100, color=color, linewidth=1.5,
linestyle="--", label=label, alpha=0.7)
ax.axhline(y=0, color="black", linewidth=0.5)
ax.axhline(y=X0 * 100 * 0.02, color="gray", linestyle=":",
alpha=0.4, label="2% band")
ax.axhline(y=-X0 * 100 * 0.02, color="gray", linestyle=":",
ax.set_xlabel("Time (s)")
ax.set_ylabel("Displacement (cm)")
ax.set_title("Optimal Suspension Response vs. Alternatives",
plt.savefig("optimal_response.png", dpi=150, bbox_inches="tight")
print("Saved: optimal_response.png")
# -------------------------------------------------------
# -------------------------------------------------------
if __name__ == "__main__":
Comments