codexrc-low-pass-bode-and-step-response.py
rc-low-pass-bode-and-step-response.py
"""Plot original ideal RC predictions; emit PNG into the current directory.
Tested with Python 3.14, matplotlib 3.10.7 and NumPy 2.3.5.
"""
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
def main():
plt.rcParams.update({'font.size': 12, 'font.family': 'DejaVu Sans'})
resistance, capacitance = 2200.0, 47e-9
tau = resistance * capacitance
fc = 1 / (2 * np.pi * tau)
blue, orange, gray = '#146eaa', '#b56c0b', '#657382'
fig = plt.figure(figsize=(10.2, 6.0), dpi=110, facecolor='white')
grid = fig.add_gridspec(2, 2, width_ratios=[1, 1.15])
gain = fig.add_subplot(grid[0, 0])
phase = fig.add_subplot(grid[1, 0], sharex=gain)
step = fig.add_subplot(grid[:, 1])
frequencies = np.geomspace(fc / 100, fc * 100, 600)
ratio = frequencies / fc
gain.semilogx(frequencies / 1000, -10 * np.log10(1 + ratio**2), color=blue, lw=2.5)
gain.scatter([fc / 1000], [-10 * np.log10(2)], color=blue, zorder=3)
gain.annotate('−3.01 dB', xy=(fc / 1000, -10 * np.log10(2)),
xytext=(fc / 1000 * 2.6, -9), fontsize=11,
arrowprops={'arrowstyle': '-', 'color': gray})
gain.set(ylabel='Voltage gain (dB)', ylim=(-42, 2), title='Frequency response')
gain.tick_params(labelbottom=False)
phase.semilogx(frequencies / 1000, -np.degrees(np.arctan(ratio)), color=blue, lw=2.5)
phase.scatter([fc / 1000], [-45], color=blue, zorder=3)
phase.annotate('−45°', xy=(fc / 1000, -45), xytext=(fc / 1000 * 3, -39),
arrowprops={'arrowstyle': '-', 'color': gray}, fontsize=11)
phase.set(xlabel='Frequency (kHz)', ylabel='Output − input phase (°)', ylim=(-94, 3),
yticks=[-90, -45, 0])
for ax in (gain, phase):
ax.axvline(fc / 1000, color=gray, linestyle=':', lw=1.4)
ax.grid(True, alpha=.23)
ax.set_xlim(frequencies[0] / 1000, frequencies[-1] / 1000)
phase.text(.03, .05, f'Corner: {fc / 1000:.4f} kHz', transform=phase.transAxes, fontsize=11)
times = np.linspace(0, 1100e-6, 600)
step.plot([-70, 0, 0, 1100], [0, 0, 3, 3], color=gray, linestyle='--', lw=1.6,
label='Input step')
step.plot(times * 1e6, 3 * (-np.expm1(-times / tau)), color=blue, lw=2.5,
label='C = 47 nF')
step.plot(times * 1e6, 3 * (-np.expm1(-times / (2 * tau))), color=orange, lw=2.5,
label='C = 94 nF')
step.scatter([tau * 1e6], [3 * (1 - np.exp(-1))], color=blue, zorder=3)
step.annotate('63.2% at 103.4 µs', xy=(tau * 1e6, 3 * (1 - np.exp(-1))),
xytext=(220, 1.6), arrowprops={'arrowstyle': '-', 'color': gray}, fontsize=11)
for scale, color in ((1, blue), (2, orange)):
settling = scale * tau * np.log(100) * 1e6
step.plot([settling, settling], [0, 2.97], color=color, linestyle=':', lw=1.2)
step.scatter([settling], [2.97], color=color, zorder=3)
step.axhline(2.97, color=gray, lw=1, linestyle=':')
step.text(540, 2.77, '99% level', fontsize=11, color=gray)
step.text(450, .55, '1% settling:\n476.2 µs → 952.3 µs', fontsize=12, color=gray)
step.set(xlabel='Time since step (µs)', ylabel='Voltage (V)',
xlim=(-70, 1100), ylim=(-.12, 3.23), title='Step response and slower smoothing')
step.grid(True, alpha=.23)
step.legend(loc='lower right', fontsize=11)
fig.suptitle('One RC time constant sets both bandwidth and settling', fontsize=17, y=.98)
fig.text(.5, .018, 'Ideal model: R = 2.2 kΩ, C = 47 nF, τ = 103.4 µs. Analytical curves; no measured data.',
ha='center', fontsize=11, color=gray)
fig.subplots_adjust(left=.085, right=.975, top=.88, bottom=.13, wspace=.32, hspace=.12)
fig.savefig(Path.cwd() / (Path(__file__).stem + '.png'), facecolor='white', transparent=False)
plt.close(fig)
if __name__ == '__main__':
main()