Andrew Simper gave a great talk at ADC 2020 conference (slides here) covering a wide range of topics in circuit modeling. This was one of the first times I was exposed to the details of circuit modeling and it really sparked my interest. Here we’ll investigate some of the simple circuits presented there.
2 LPF, HPF
Figure 1: 1 pole lowpass, 1 pole highpass
We start with the 1 pole lowpass, 1 pole highpass filter. If you’ve not already, it is worth looking at the previous post VCF: Part 1 - RC Filters.
A little work is needed to use the notation in Andrew’s slides, we use the assumptions argument to map between the naming schemes.
Code
def generate_lpfhpf_maths():# chained lpf, hpf netlist =""" V0 1 0 V_in R1 1 2 R1 C1 3 2 0.47e-6 C2 3 0 0.01e-6 R2 3 0 R2 J 3 0 """# switch to symbols used in notes briefly R1, gr1, R2, gr2 = symbols('R1, gr1, R2, gr2') c1gc, gc1, c2gc, gc2 = symbols('c1.gc, gc1, c2.gc, gc2') c1ieq, ic1eq, c2ieq, ic2eq = symbols('c1.ieq, ic1eq, c2.ieq, ic2eq') assumptions = {R1: 1/gr1, R2: 1/gr2, c1gc: gc1, c2gc: gc2, c1ieq: ic1eq, c2ieq: ic2eq} class_name ="RCBand"+str(np.random.randint(100000)) rules, (A, x, b) = run_MNA(netlist, class_name, assumptions=assumptions, method="LUsolve") display(Markdown("## Solutions from SPICEWORLD code")) display(Markdown("The auto-generated matrix problem reads:")) display(Eq(MatMul(A, x), b)) display(Eq(Mul(A, x), b)) display(Markdown("Pulling out the second and third rows reads:")) kcl = (A*x-b) display(Eq(0, kcl[1])) display(Eq(0, kcl[2])) v_2, v_3, V_in = symbols("v_2, v_3, V_in") v_3_spiceworld = rules.solutions[3].rhs v_2_spiceworld = rules.solutions[2].rhs# little manipulation to get into same format v_3_num, v_3_denom = fraction(factor(v_3_spiceworld)) v_3_v_3_denomnum = collect(v_3_denom, [gr1, gc1]) v_3_num = collect(v_3_num, [gr1]) v_3_spiceworld = v_3_num / v_3_denom display(Markdown("This yields solutions from SPICEWORLD, which after a little re-ordering reads:")) display(Eq(v_3, v_3_spiceworld)) display(Eq(v_2, v_2_spiceworld)) display(Markdown("## Solutions from talk")) display(Markdown("{width=500}")) display(Markdown("Writing these out in sympy form, we can verify that they are the same")) v_3_simper = (gc1 * ic2eq + gr1*(ic1eq + ic2eq + gc1*V_in)) / (gr1*(gc2 + gr2) + gc1*(gc2 + gr1 + gr2)) v_2_simper = (gr1*V_in + gc1*v_3 - ic1eq) / (gc1 + gr1) display(Eq(v_3, v_3_simper)) display(Eq(v_2, v_2_simper)) display(Markdown("If we substitude v_3 in, we get the final expression for v_2 that matches the SPICEWORLD solution. ")) v_2_simper = simplify(v_2_simper.subs({v_3: v_3_simper})) display(Eq(v_2, v_2_simper))# prove solutions are the sameassert simplify(v_3_spiceworld-v_3_simper) ==0assert simplify(v_2_spiceworld-v_2_simper) ==0generate_lpfhpf_maths()
2.1 Solutions from SPICEWORLD code
The auto-generated matrix problem reads:
Pulling out the second and third rows reads:
This yields solutions from SPICEWORLD, which after a little re-ordering reads:
2.2 Solutions from talk
Equations for LPF, HPF voltages
Writing these out in sympy form, we can verify that they are the same
If we substitude v_3 in, we get the final expression for v_2 that matches the SPICEWORLD solution.
We can see a nice bandpass frequency response, and the waveform broadly matches that displayed in the slides.
3 Diode
Figure 2: Diode Clipper
We’ll cover diode clipping elsewhere in more detail, so for now we just reproduce the clipper using the netlist derived from the above simple circuit. As we can see, there is a nice saturation applied to the saw.
Finally we can put together the full filter, including diodes. Note that now the filter is nonlinear, the frequency response is no longer defined, and indeed we get an uninterpretable mess from it!
Figure 4: Effect of numerical method on frequency response
Lastly the talk describes the effect of integration method on the frequency response, with the example given as an RC filter. First lets extract the ideal curve data (yellow curve) to work out what is - from below, we see . This is perhaps a mistake in the data generation process, as we’ll try and show below.
Next let’s solve the numerical problem as a function of numerical method , where is trapezoidal method and is backward Euler. If instead we use , we can reproduce the numerical results. Additionally increasing the sampling rate (or decreasing the timestep) we eventually recover the theoretical limit corresponding to , suggesting that the yellow curve plotted in Figure 4 the slides is not correct.
def integration_method_response(a):def integration_method_response_inner(options, a): netlist =f""" V0 1 0 Vin R1 1 2 1000 C1 2 0 1e-8 {options} J 2 0 """ class_name ="CapTest"+str(np.random.randint(100000)) rules, _ = run_MNA(netlist, class_name) code_string = generate_processor(rules)with ModuleLoader(code_string, class_name) as m: f, H_dB, H, phi_deg = calculate_filter_response(m, sampling_frequency=a*44100)return f, H_dB, H, phi_deg fig, ax = plt.subplots(figsize=(10, 5))for i, m inenumerate([0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1]): f, H_dB, H, phi_deg = integration_method_response_inner(f'm={m}', a) label =""if i %2elsef"m = {m}" ax.plot(f, H, color=viridis_11.colors[i], label=label) ax.set_xscale('linear') ax.set_yscale('linear') ax.set_xlim(0, 44100./2) ax.set_ylim(0.0, 1) ax.set_title(f"Sampling rate {a} × 44100.")# drop the zero entry (divide by zero below!) f = f[1:] RC =1450e-8 fc =1.0/(2*np.pi*RC) gain = (fc / f) / np.sqrt(1+ np.power(fc / f, 2)) ax.plot(f, gain, "--", color='cyan', label='RC = 1430e-8') RC =1000e-8 fc =1.0/(2*np.pi*RC) gain = (fc / f) / np.sqrt(1+ np.power(fc / f, 2)) ax.plot(f, gain, "--", color='k', label='RC = 1000e-8') ax.grid() ax.legend() plt.show()integration_method_response(1)integration_method_response(4)integration_method_response(16)