Generates step responses and Bode plots for RC and RLC circuits.
from scipy.integrate import solve_ivp
import matplotlib.pyplot as plt
# -------------------------------------------------------
# -------------------------------------------------------
def rc_step_response(R, C, V_step=1.0, t_end=None, n_points=1000):
Simulate step response of a series RC circuit.
Returns time array and capacitor voltage array.
t_end = 5 * tau # 5 time constants
dVc_dt = (V_step - Vc) / (R * C)
sol = solve_ivp(ode, [0, t_end], [0.0],
dense_output=True, max_step=t_end/500)
t = np.linspace(0, t_end, n_points)
# Analytical solution for comparison
Vc_analytical = V_step * (1 - np.exp(-t / tau))
return t, Vc, Vc_analytical, tau
def rc_bode(R, C, f_min=1.0, f_max=1e6, n_points=500):
Compute Bode plot data for an RC low-pass filter.
Returns frequency, magnitude (dB), and phase (degrees).
f = np.logspace(np.log10(f_min), np.log10(f_max), n_points)
H = 1.0 / (1.0 + 1j * omega * R * C)
mag_db = 20 * np.log10(np.abs(H))
phase_deg = np.degrees(np.angle(H))
f_cutoff = 1.0 / (2 * np.pi * R * C)
return f, mag_db, phase_deg, f_cutoff
# -------------------------------------------------------
# -------------------------------------------------------
def rlc_step_response(R, L, C, V_step=1.0, t_end=None, n_points=1000):
Simulate step response of a series RLC circuit.
Returns time, capacitor voltage, and inductor current.
omega_n = 1.0 / np.sqrt(L * C)
zeta = R / 2.0 * np.sqrt(C / L)
Q = 1.0 / (2.0 * zeta) if zeta > 0 else float('inf')
# Enough time to see the response settle
# Underdamped: several oscillation periods
omega_d = omega_n * np.sqrt(1 - zeta**2)
t_end = max(10 * 2 * np.pi / omega_d, 20 / (zeta * omega_n))
t_end = 10 / (zeta * omega_n)
I_L = y[0] # Inductor current
V_C = y[1] # Capacitor voltage
dI_L_dt = (V_step - R * I_L - V_C) / L
return [dI_L_dt, dV_C_dt]
sol = solve_ivp(ode, [0, t_end], [0.0, 0.0],
dense_output=True, max_step=t_end/2000)
t = np.linspace(0, t_end, n_points)
return t, V_C, I_L, omega_n, zeta, Q
def rlc_bode(R, L, C, f_min=1.0, f_max=1e6, n_points=500):
Compute Bode plot data for a series RLC circuit
(voltage across capacitor / input voltage).
f = np.logspace(np.log10(f_min), np.log10(f_max), n_points)
# Transfer function: H(jw) = 1 / (1 - w^2*LC + jwRC)
H = 1.0 / (1 - omega**2 * L * C + 1j * omega * R * C)
mag_db = 20 * np.log10(np.abs(H))
phase_deg = np.degrees(np.angle(H))
f_resonant = 1.0 / (2 * np.pi * np.sqrt(L * C))
return f, mag_db, phase_deg, f_resonant
# -------------------------------------------------------
# -------------------------------------------------------
"""Full analysis of an RC circuit."""
print(" RC CIRCUIT ANALYSIS")
f_c = 1.0 / (2 * np.pi * R * C)
print(f" R = {R:.1f} ohm")
print(f" C = {C*1e6:.2f} uF")
print(f" Time constant (tau) = {tau*1e3:.3f} ms")
print(f" Cutoff frequency = {f_c:.1f} Hz")
print(f" 5*tau (settling) = {5*tau*1e3:.3f} ms")
t, Vc_sim, Vc_analytical, _ = rc_step_response(R, C)
f, mag, phase, _ = rc_bode(R, C, f_min=f_c/100, f_max=f_c*100)
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
fig.suptitle(f"RC Circuit: R={R} ohm, C={C*1e6:.2f} uF", fontsize=13,
axes[0, 0].plot(t * 1e3, Vc_sim, "tab:blue", linewidth=2,
axes[0, 0].plot(t * 1e3, Vc_analytical, "r--", linewidth=1.5,
label="Analytical", alpha=0.8)
axes[0, 0].axhline(y=0.632, color="gray", linestyle=":", alpha=0.5)
axes[0, 0].axvline(x=tau * 1e3, color="gray", linestyle=":", alpha=0.5,
label=f"tau = {tau*1e3:.3f} ms")
axes[0, 0].set_xlabel("Time (ms)")
axes[0, 0].set_ylabel("Capacitor Voltage (V)")
axes[0, 0].set_title("Step Response")
axes[0, 0].legend(fontsize=8)
axes[0, 0].grid(True, alpha=0.3)
# Error between simulation and analytical
error = np.abs(Vc_sim - Vc_analytical)
axes[0, 1].semilogy(t * 1e3, error + 1e-16, "tab:red", linewidth=1.5)
axes[0, 1].set_xlabel("Time (ms)")
axes[0, 1].set_ylabel("Absolute Error (V)")
axes[0, 1].set_title("Simulation vs. Analytical Error")
axes[0, 1].grid(True, alpha=0.3)
axes[1, 0].semilogx(f, mag, "tab:blue", linewidth=2)
axes[1, 0].axhline(y=-3, color="red", linestyle="--", alpha=0.7,
axes[1, 0].axvline(x=f_c, color="gray", linestyle=":", alpha=0.5,
label=f"fc = {f_c:.1f} Hz")
axes[1, 0].set_xlabel("Frequency (Hz)")
axes[1, 0].set_ylabel("Magnitude (dB)")
axes[1, 0].set_title("Bode Plot: Magnitude")
axes[1, 0].legend(fontsize=8)
axes[1, 0].grid(True, alpha=0.3, which="both")
axes[1, 1].semilogx(f, phase, "tab:orange", linewidth=2)
axes[1, 1].axhline(y=-45, color="gray", linestyle=":", alpha=0.5,
axes[1, 1].set_xlabel("Frequency (Hz)")
axes[1, 1].set_ylabel("Phase (degrees)")
axes[1, 1].set_title("Bode Plot: Phase")
axes[1, 1].legend(fontsize=8)
axes[1, 1].grid(True, alpha=0.3, which="both")
plt.savefig("rc_analysis.png", dpi=150, bbox_inches="tight")
print("Plot saved as rc_analysis.png\n")
def analyze_rlc(R, L, C):
"""Full analysis of an RLC circuit."""
print(" RLC CIRCUIT ANALYSIS")
omega_n = 1.0 / np.sqrt(L * C)
f_n = omega_n / (2 * np.pi)
zeta = R / 2.0 * np.sqrt(C / L)
Q = 1.0 / (2.0 * zeta) if zeta > 0 else float('inf')
damping_type = "Underdamped (will ring)"
damping_type = "Critically damped"
damping_type = "Overdamped"
print(f" R = {R:.1f} ohm")
print(f" L = {L*1e3:.3f} mH")
print(f" C = {C*1e6:.2f} uF")
print(f" Natural frequency = {f_n:.1f} Hz")
print(f" Damping ratio (z) = {zeta:.4f}")
print(f" Quality factor (Q) = {Q:.2f}")
print(f" Type: {damping_type}")
t, V_C, I_L, _, _, _ = rlc_step_response(R, L, C)
f, mag, phase, f_res = rlc_bode(R, L, C, f_min=f_n/100, f_max=f_n*100)
fig, axes = plt.subplots(2, 2, figsize=(12, 8))
f"RLC Circuit: R={R} ohm, L={L*1e3:.2f} mH, C={C*1e6:.2f} uF "
f"(zeta={zeta:.3f}, Q={Q:.1f})",
fontsize=11, fontweight="bold")
axes[0, 0].plot(t * 1e3, V_C, "tab:blue", linewidth=2)
axes[0, 0].axhline(y=1.0, color="gray", linestyle="--", alpha=0.5,
axes[0, 0].set_xlabel("Time (ms)")
axes[0, 0].set_ylabel("Capacitor Voltage (V)")
axes[0, 0].set_title("Step Response: Voltage")
axes[0, 0].legend(fontsize=8)
axes[0, 0].grid(True, alpha=0.3)
axes[0, 1].plot(t * 1e3, I_L * 1e3, "tab:green", linewidth=2)
axes[0, 1].set_xlabel("Time (ms)")
axes[0, 1].set_ylabel("Inductor Current (mA)")
axes[0, 1].set_title("Step Response: Current")
axes[0, 1].grid(True, alpha=0.3)
axes[1, 0].semilogx(f, mag, "tab:blue", linewidth=2)
axes[1, 0].axhline(y=0, color="gray", linestyle="--", alpha=0.3)
axes[1, 0].axvline(x=f_res, color="red", linestyle=":", alpha=0.7,
label=f"f_res = {f_res:.1f} Hz")
axes[1, 0].set_xlabel("Frequency (Hz)")
axes[1, 0].set_ylabel("Magnitude (dB)")
axes[1, 0].set_title("Bode Plot: Magnitude")
axes[1, 0].legend(fontsize=8)
axes[1, 0].grid(True, alpha=0.3, which="both")
axes[1, 1].semilogx(f, phase, "tab:orange", linewidth=2)
axes[1, 1].axhline(y=-90, color="gray", linestyle=":", alpha=0.5)
axes[1, 1].set_xlabel("Frequency (Hz)")
axes[1, 1].set_ylabel("Phase (degrees)")
axes[1, 1].set_title("Bode Plot: Phase")
axes[1, 1].grid(True, alpha=0.3, which="both")
plt.savefig("rlc_analysis.png", dpi=150, bbox_inches="tight")
print("Plot saved as rlc_analysis.png\n")
# -------------------------------------------------------
# Main: run both analyses
# -------------------------------------------------------
if __name__ == "__main__":
# RC circuit: 1k ohm, 10 uF (tau = 10 ms, fc = 15.9 Hz)
analyze_rc(R=1000.0, C=10e-6)
# RLC circuit: 100 ohm, 10 mH, 1 uF
# This gives an underdamped response with visible ringing
analyze_rlc(R=100.0, L=10e-3, C=1e-6)
# Try an overdamped case too
print("\n--- Overdamped RLC for comparison ---\n")
analyze_rlc(R=1000.0, L=10e-3, C=1e-6)
Comments