Engee documentation
Notebook

RC bandpass filter for frequency range selection

In this example, a passive bandpass RC filter is calculated and tested for a range of approximately 0.75 kHz to 16 kHz. The script shows the calculation of ratings, the construction of the expected amplitude-frequency response, the launch of ready-made Engee models and the comparison of the signal model with a physical electrical circuit.

Introduction

This example describes the progress of solving the problem.

2W1.2 - Bandwidth

from the book

** Thomas K. Hayes, Paul Horowitz
The Art of Circuit Engineering: Theory and Practice**

by computer simulation in Engee.

The task requires obtaining a filter that attenuates frequencies that are too low, skips the operating range, and weakens the high-frequency component again.

Practically, such a filter can be assembled from two simple RC links. The low-frequency boundary is set by the high-frequency link, and the high-frequency boundary is set by the low-frequency link. It is important to choose the resistances so that the next cascade does not overload the previous one.

Theoretical background

A transfer function is used for the low-frequency link of the first order

For the first-order high-frequency link:

If we consider the cascades to be weakly coupled in terms of load, the bandpass filter describes the product:

The boundary frequencies of each RC link are estimated as

In the physical model, the output resistor of the upper frequency link has a resistance of 1 mOhm. Therefore, it is useful to consider the equivalent resistance for the lower bound.:

Calculation of parameters

In this cell, set the cascade resistances, target frequencies, and standard capacitances. Then compare the calculated border frequencies with the required range.

In [ ]:
R_LP = 10_000.0
C_LP = 1e-9
R_HP = 100_000.0
C_HP = 2e-9
R_load = 1_000_000.0

f_low_target = 750.0
f_high_target = 16_000.0

parallel(R1, R2) = 1 / (1 / R1 + 1 / R2)
R_HP_loaded = parallel(R_HP, R_load)

f_lp = 1 / (2pi * R_LP * C_LP)
f_hp_ideal = 1 / (2pi * R_HP * C_HP)
f_hp_loaded = 1 / (2pi * R_HP_loaded * C_HP)

C_LP_exact = 1 / (2pi * f_high_target * R_LP)
C_HP_exact = 1 / (2pi * f_low_target * R_HP)

calculation_summary = (;
    R_LP,
    C_LP,
    f_lp,
    R_HP,
    C_HP,
    f_hp_ideal,
    R_HP_loaded,
    f_hp_loaded,
    C_LP_exact,
    C_HP_exact,
)

calculation_summary
Out[0]:
(R_LP = 10000.0, C_LP = 1.0e-9, f_lp = 15915.494309189533, R_HP = 100000.0, C_HP = 2.0e-9, f_hp_ideal = 795.7747154594767, R_HP_loaded = 90909.0909090909, f_hp_loaded = 875.3521870054245, C_LP_exact = 9.947183943243459e-10, C_HP_exact = 2.122065907891938e-9)

The nominal value 1 nF for the low-frequency link gives an upper bound of about 15.9 kHz. The nominal value 2 nF for the high-frequency link gives a lower limit of about 796 Hz without load and about 875 Hz at load 1 mOhm. For an educational RC filter, this is close to the required band.

Description of models

Two Engee models have been prepared for testing.

  • RCBandPass_2W_1_2_signal.engee — signal model with blocks Transfer Fcn. It compares the ideal cascade transfer function and approximation with a load of 1 mOhm.
  • RCBandPass_2W_1_2_phys.engee — the physical acausal model. In it, the voltage source feeds the RC circuit: first there is a low-frequency link 10 kOhm + 1 nF, then a high-frequency link 2 nF + 100 kOhm, and a load 1 mOhm is connected to the output.

The sum of three sinusoids is applied to the input: low frequency 200 Hz, frequency in the band 3 kHz and high frequency 30 kHz. This signal helps to see which components the filter suppresses and which it skips.

image.png
image.png

Environment preparation

In this cell, specify the paths to the finished models and the folder for the result files. If you are running the example from another directory, change the dir variable.

In [ ]:
using Plots

dir = @__DIR__
output_dir = dir
model_paths = Dict(
    :signal => joinpath(dir, "RCBandPass_2W_1_2_signal.engee"),
    :physical => joinpath(dir, "RCBandPass_2W_1_2_phys.engee"),
)
string("prepared; dir=", dir)
Out[0]:
"prepared; dir=/user/maxim-sidorov/ivaroprl"

The environment is ready: the models are next to the script, and the new graphs and tables will be saved to the same folder.

Analytical verification

Before running the models, build the calculated amplitude-frequency response. It shows the expected bandwidth and the effect of the load on the lower bound.

In [ ]:
freqs = exp10.(range(log10(10.0), log10(100_000.0), length = 800))

gain_bp(f, R_hp) = begin
    tau_lp = R_LP * C_LP
    tau_hp = R_hp * C_HP
    omega = 2pi * f
    (omega * tau_hp) / sqrt((1 + (omega * tau_hp)^2) * (1 + (omega * tau_lp)^2))
end

p_response = plot(
    freqs,
    20 .* log10.(gain_bp.(freqs, R_HP));
    xscale = :log10,
    label = "no load",
    xlabel = "Frequency, Hz",
    ylabel = "Amplitude, dB",
    title = "Estimated frequency response of the RC bandpass filter",
)
plot!(p_response, freqs, 20 .* log10.(gain_bp.(freqs, R_HP_loaded)); label = "Rh = 1 mOhm")
vline!(p_response, [750.0, 16_000.0]; label = "task boundaries", linestyle = :dash)
hline!(p_response, [-3.0]; label = "-3 dB", linestyle = :dot)

response_path = joinpath(output_dir, "rc_bandpass_response.png")
savefig(p_response, response_path)
p_response
Out[0]:

The graph shows the rise of the characteristic after the lower boundary and the decline after the upper boundary. The load 1 mOhm slightly shifts the lower limit upwards, because it simultaneously reduces the resistance of the output arm of the high-frequency cascade.

Simulation of the model

Now download the signal model and run the simulation. The code uses software management by Engee: engee.load, engee.open, engee.run and engee.close.

In [ ]:
missing_models = [path for path in values(model_paths) if !isfile(path)]

if isempty(missing_models)
    signal_payload = Core.eval(Main, quote
        model = engee.load($(model_paths[:signal]); force = true)
        engee.open(model)
        results = engee.run(model)
        engee.close(model; force = true)
        (; results, result_text = repr(results))
    end)

    signal_results = signal_payload.results
    string("signal model simulated; results=", signal_payload.result_text)
else
    string("missing_models=", join(missing_models, ";"))
end
Out[0]:
"signal model simulated; results=SimulationResult(\n    run_id => 5,\n    \"bp_load_1M.1\" => WorkspaceArray{Float64}(\"RCBandPass_2W_1_2_signal/bp_load_1M.1\")\n,\n    \"input_sum.1\" => WorkspaceArray{Float64}(\"RCBandPass_2W_1_2_signal/input_sum.1\")\n,\n    \"bp_ideal.1\" => WorkspaceArray{Float64}(\"RCBandPass_2W_1_2_signal/bp_ideal.1\")\n\n)"

The signal model returns the input signal, the output of the ideal bandpass filter, and the output of the approximation with a load of 1 mOhm.

Checking the physical model

Run the physical model. It tests the same idea as an electrical circuit with a voltage source, resistors, capacitors, electric ground, voltage sensor, and block Solver Configuration.

In [ ]:
physical_payload = Core.eval(Main, quote
    model = engee.load($(model_paths[:physical]); force = true)
    engee.open(model)
    results = engee.run(model)
    engee.close(model; force = true)
    (; results, result_text = repr(results))
end)

physical_results = physical_payload.results
string("physical model simulated; results=", physical_payload.result_text)
Out[0]:
"physical model simulated; results=SimulationResult(\n    run_id => 7,\n    \"input_sum.1\" => WorkspaceArray{Float64}(\"RCBandPass_2W_1_2_phys/input_sum.1\")\n,\n    \"vout_sensor.V\" => WorkspaceArray{Float64}(\"RCBandPass_2W_1_2_phys/vout_sensor.V\")\n\n)"

The physical model was completed successfully: the output voltage is recorded by the sensor.

Analysis of simulation results

Build time graphs of the signal model. They show how, out of the sum of the three harmonics of the signal, there remains mainly a component in the bandwidth.

In [ ]:
function series_from_result(results, key)
    df = results[key]
    return df.time, df.value
end

time, input_signal = series_from_result(signal_results, "input_sum.1")
_, y_ideal = series_from_result(signal_results, "bp_ideal.1")
_, y_load = series_from_result(signal_results, "bp_load_1M.1")

p_time = plot(
    time .* 1000,
    input_signal;
    label = "entrance",
    xlabel = "Time, ms",
    ylabel = "Voltage, relative units",
    title = "Filtering the sum of three tones",
)
plot!(p_time, time .* 1000, y_ideal; label = "the perfect cascade model")
plot!(p_time, time .* 1000, y_load; label = "with a load of 1 mOhm")

time_plot_path = joinpath(output_dir, "rc_bandpass_time_signals.png")
savefig(p_time, time_plot_path)
p_time
Out[0]:

The output is less influenced by the slow component 200 Hz and the high-frequency component 30 kHz. The signal in the range of 3 kHz passes better than the others, so the shape becomes closer to the midrange harmonic.

Comparison with a physical circuit

Build the input and output of the physical model. This graph is needed to make sure that the physical circuit behaves like a calculated bandpass filter.

In [ ]:
time_phys, input_phys = series_from_result(physical_results, "input_sum.1")
_, vout_phys = series_from_result(physical_results, "vout_sensor.V")

p_phys = plot(
    time_phys .* 1000,
    input_phys;
    label = "physical model input",
    xlabel = "Time, ms",
    ylabel = "Voltage, V",
    title = "Checking the physical RC circuit",
)
plot!(p_phys, time_phys .* 1000, vout_phys; label = "output of the physical model")

physical_plot_path = joinpath(output_dir, "rc_bandpass_physical_time.png")
savefig(p_phys, physical_plot_path)
p_phys
Out[0]:

The physical circuit also suppresses the low and high components. A slight difference from the ideal transfer function is expected: real cascades interact via impedances, and the output is additionally loaded with a resistance of 1 mOhm.

Summary table

Put the calculated indicators in a table. It records the band boundaries and transmission coefficients at the test frequencies.

In [ ]:
case_rows = [
    (case = "ideal", R_hp = R_HP),
    (case = "load_1M", R_hp = R_HP_loaded),
]

summary_rows = [
    (;
        case = row.case,
        f_hp_Hz = 1 / (2pi * row.R_hp * C_HP),
        f_lp_Hz = f_lp,
        gain_200Hz = gain_bp(200.0, row.R_hp),
        gain_750Hz = gain_bp(750.0, row.R_hp),
        gain_3kHz = gain_bp(3_000.0, row.R_hp),
        gain_16kHz = gain_bp(16_000.0, row.R_hp),
        gain_30kHz = gain_bp(30_000.0, row.R_hp),
    )
    for row in case_rows
]

summary_csv = joinpath(output_dir, "rc_bandpass_summary.csv")
open(summary_csv, "w") do io
    println(io, "case,f_hp_Hz,f_lp_Hz,gain_200Hz,gain_750Hz,gain_3kHz,gain_16kHz,gain_30kHz")
    for row in summary_rows
        println(io, join((row.case, row.f_hp_Hz, row.f_lp_Hz, row.gain_200Hz, row.gain_750Hz, row.gain_3kHz, row.gain_16kHz, row.gain_30kHz), ","))
    end
end

summary_rows
Out[0]:
2-element Vector{@NamedTuple{case::String, f_hp_Hz::Float64, f_lp_Hz::Float64, gain_200Hz::Float64, gain_750Hz::Float64, gain_3kHz::Float64, gain_16kHz::Float64, gain_30kHz::Float64}}:
 (case = "ideal", f_hp_Hz = 795.7747154594767, f_lp_Hz = 15915.494309189533, gain_200Hz = 0.2437278406404212, gain_750Hz = 0.6851064392173065, gain_3kHz = 0.9498460237713993, gain_16kHz = 0.704361397337135, gain_30kHz = 0.4684850032871685)
 (case = "load_1M", f_hp_Hz = 875.3521870054245, f_lp_Hz = 15915.494309189533, gain_200Hz = 0.22272201468675978, gain_750Hz = 0.6499191682006742, gain_3kHz = 0.9433569704186011, gain_16kHz = 0.7041789727228396, gain_30kHz = 0.46845041976732643)

The table confirms the engineering goal: the transmission coefficient of about 3 kHz is the highest among the selected test frequencies, and the frequencies 200 Hz and 30 kHz are noticeably weakened.

Conclusion

We calculated and tested the RC bandpass filter for a range of approximately 0.75 kHz to 16 kHz. The ratings 10 kOhm + 1 nF and 2 nF + 100 kOhm give close band boundaries, and the load 1 mOhm moderately shifts the lower boundary. The Engee signal and physical models consistently show the suppression of low and high components during the passage of a midrange signal.

List of literature

  1. Hayes T.K., Horowitz P. ** The art of circuit engineering. Theory and practice.** BHV-Petersburg, 2022. pp.130-132
  2. Horowitz P., Hill U. ** The art of circuit engineering.** Volume 1. Mir, 1993.
  3. Bessonov L. A. Theoretical foundations of electrical engineering. Electrical circuits. Gardariki, 2002.
  4. Sedra A., Smith K. Microelectronic circuits. Williams.
  5. Engee Help Center. Block documentation Transfer Fcn, Sine Wave, Add, Resistor, Capacitor, Electrical Reference, Controlled Voltage Source, Voltage Sensor, Solver Configuration.
  6. Engee Help Center. Software model management: engee.load, engee.open, engee.run, engee.close.