Qu

Signal processing

RC filter response

RC low-pass filter: transient step response and Bode plot.

catalog/qu_rc_filter.qu

A single-pole RC low-pass (Vin -- R -- node -- C -- ground) is simple enough to model directly, with no circuit-solver package needed: the step response has a closed form, and the frequency response is one line of complex-vector arithmetic (a voltage divider against the capacitor's frequency-dependent impedance). Both views are cross-checked against an explicit-Euler integration of the governing ODE dv/dt = (vin - v) / tau.

What it exercises

How it works

Circuit

R = 1e3          # Ohm
C = 100e-9       # F
tau = R * C      # time constant (s)
fc = 1 / (2*pi*tau)
print("R={R:.0f} Ohm, C={C*1e9:.0f} nF, tau={tau*1e6:.2f} us, fc={fc:.1f} Hz")

Step response: closed form vs explicit-Euler integration

Fs = 2e6
dt = 1 / Fs
N = round(8 * tau / dt)
t = (0 to N - 1) * dt

v_exact = 1 - exp(-t / tau)

v_euler = zeros(1, N)
for k = 1 to N - 1
    v_euler[k] = v_euler[k - 1] + dt * (1 - v_euler[k - 1]) / tau
end for

step_err = max(abs(v_euler - v_exact))
print("Euler vs closed-form step response: max err = {step_err:.2e}")

Frequency response (Bode): H(f) = 1 / (1 + i*2*pi*f*tau)

f = logspace(1, 7, 300)
H = 1 ./ (1 + 2i*pi*f*tau)
mag_db = 20 * log10(abs(H))
phase_deg = angle(H) * 180 / pi

Hc = 1 / (1 + 2i*pi*fc*tau)
print("|H(fc)| = {abs(Hc):.4f} (should be ~0.7071, the -3 dB point)")

Plot

Three panels side by side (1 row x 3 cols): the default figure canvas (900x600, a 3:2 aspect meant for ONE panel) divides its width three ways but keeps the full height, so each panel came out ~300 wide x ~500 tall -- narrow and needlessly tall, with titles wrapping awkwardly. Sized explicitly instead so each panel lands close to square.

figure_size(1350, 450)

subplot(1, 3, 1)
plot(t * 1e6, v_exact, label="closed form", color="blue")
plot(t * 1e6, v_euler, label="Euler", color="orange")
xlabel("Time (us)")
ylabel("Vout / Vin")
title("Step response")
legend()

subplot(1, 3, 2)
semilogx(f, mag_db)
xlabel("Frequency (Hz)")
ylabel("Magnitude (dB)")
title("Bode magnitude, fc={fc:.0f} Hz")
grid on

subplot(1, 3, 3)
semilogx(f, phase_deg)
xlabel("Frequency (Hz)")
ylabel("Phase (deg)")
title("Bode phase")
grid on

show plot

Results

printed
R=1000 Ohm, C=100 nF, tau=100.00 us, fc=1591.5 Hz
Euler vs closed-form step response: max err = 9.22e-4
|H(fc)| = 0.7071 (should be ~0.7071, the -3 dB point)
0 0.2 0.4 0.6 0.8 1.0 0 300 600 Step response Time (us) Vout / Vin closed form Euler -70 -60 -50 -40 -30 -20 -10 0 101 103 105 107 Bode magnitude, fc=1592 Hz Frequency (Hz) Magnitude (dB) -80 -60 -40 -20 0 101 103 105 107 Bode phase Frequency (Hz) Phase (deg)
Drawn by running the program above.

The whole program

Runnable as it stands: qu run catalog/qu_rc_filter.qu

# qu_rc_filter.qu -- RC low-pass filter: transient step response and Bode plot.

#

# A single-pole RC low-pass (Vin -- R -- node -- C -- ground) is simple

# enough to model directly, with no circuit-solver package needed: the step

# response has a closed form, and the frequency response is one line of

# complex-vector arithmetic (a voltage divider against the capacitor's

# frequency-dependent impedance). Both views are cross-checked against an

# explicit-Euler integration of the governing ODE dv/dt = (vin - v) / tau.

#

# Constructs exercised (all real, verified-working builtins):

#   logspace, exp             -- frequency grid and the closed-form step

#   1 ./ (1 + 2i*pi*f*tau)    -- the transfer function, complex elementwise

#   abs / angle               -- magnitude and phase from a complex vector

#   indexed assignment in a for loop -- explicit-Euler ODE integration

#   subplot, semilogx, legend, grid on


backend auto

#%% Circuit

R = 1e3          # Ohm

C = 100e-9       # F

tau = R * C      # time constant (s)

fc = 1 / (2*pi*tau)
print("R={R:.0f} Ohm, C={C*1e9:.0f} nF, tau={tau*1e6:.2f} us, fc={fc:.1f} Hz")

#%% Step response: closed form vs explicit-Euler integration

Fs = 2e6
dt = 1 / Fs
N = round(8 * tau / dt)
t = (0 to N - 1) * dt

v_exact = 1 - exp(-t / tau)

v_euler = zeros(1, N)
for k = 1 to N - 1
    v_euler[k] = v_euler[k - 1] + dt * (1 - v_euler[k - 1]) / tau
end for

step_err = max(abs(v_euler - v_exact))
print("Euler vs closed-form step response: max err = {step_err:.2e}")

#%% Frequency response (Bode): H(f) = 1 / (1 + i*2*pi*f*tau)

f = logspace(1, 7, 300)
H = 1 ./ (1 + 2i*pi*f*tau)
mag_db = 20 * log10(abs(H))
phase_deg = angle(H) * 180 / pi

Hc = 1 / (1 + 2i*pi*fc*tau)
print("|H(fc)| = {abs(Hc):.4f} (should be ~0.7071, the -3 dB point)")

#%% Plot

# Three panels side by side (1 row x 3 cols): the default figure canvas

# (900x600, a 3:2 aspect meant for ONE panel) divides its width three ways

# but keeps the full height, so each panel came out ~300 wide x ~500 tall

# -- narrow and needlessly tall, with titles wrapping awkwardly. Sized

# explicitly instead so each panel lands close to square.

figure_size(1350, 450)

subplot(1, 3, 1)
plot(t * 1e6, v_exact, label="closed form", color="blue")
plot(t * 1e6, v_euler, label="Euler", color="orange")
xlabel("Time (us)")
ylabel("Vout / Vin")
title("Step response")
legend()

subplot(1, 3, 2)
semilogx(f, mag_db)
xlabel("Frequency (Hz)")
ylabel("Magnitude (dB)")
title("Bode magnitude, fc={fc:.0f} Hz")
grid on

subplot(1, 3, 3)
semilogx(f, phase_deg)
xlabel("Frequency (Hz)")
ylabel("Phase (deg)")
title("Bode phase")
grid on

show plot