using ACME
using Plots
= 44100
fs
function sine(t; freq=10, ppv=10)
= @. ppv/2 * sin(2*pi*t*freq)
x return reshape(x, 1, length(x))
end;
function saw(t; freq=10, ppv=10)
= @. ppv/2*(2*mod(t*freq, 1)-1)
x return reshape(x, 1, length(x))
end;
function square(t; freq=10, ppv=10)
= @. ppv/2*(2.0*(mod(t*freq, 1) > 0.5) - 1)
x return reshape(x, 1, length(x))
end;
1 Intro
Julia is a high-level, general-purpose dynamic programming language. Its features are well suited for numerical analysis and computational science.
I’ve been curious about Julia for a while, and came across a neat package for it called ACME.jl:
“a package for the simulation of electrical circuits, focusing on audio effect circuits”
It has a clever, compact syntax for specifying circuits, and supports a wide range of components. I thought it’d be interesting to run some of the circuits from the mki-x-ESEDU VCF series through it (see Part 1, Part 2, Part 3)!
2 Parsing netlists
I’ve setup a script that converts a standard spiceworld Circuit
object to a ACME.jl listing. It first lists all the components, then for each node, lists all the connects that occur at that node. As a simple example the following netlist:
# python code
def rc_1st_order_passive():
= f"""
netlist Vin 1 0 saw
R1 1 2 1000
C1 0 2 1.0e-6
J1 2 0
"""
= parse_netlist(netlist, "RC1stOrderPassive")
circuit = acme_from_circuit(circuit, "rc_1st_order_passive") output_path
produces the following Julia code:
# julia code
using ACME
using Plots
= 44100
fs
function saw(t; freq=10, ppv=10)
= @. ppv/2*(2*mod(t*freq, 1)-1)
x return reshape(x, 1, length(x))
end;
function rc_1st_order_passive(::Type{Circuit}; )
@circuit begin
# elements
= voltagesource()
jin = resistor(1000)
r1 = capacitor(1e-06)
c1 = voltageprobe()
jout_1
# node connections
#= node 00 =# jin[-] == c1[1] == jout_1[-] == gnd
#= node 01 =# jin[+] == r1[1]
#= node 02 =# r1[2] == c1[2] == jout_1[+]
end
end
= DiscreteModel(rc_1st_order_passive(Circuit), 1//fs)
model
= [n/fs for n in 0:10000]
ts = saw(ts, freq=25)
x = run!(model, x)
y print("Running rc_1st_order_passive complete!\n")
plot(ts, x[1, :], label="V_in", size=(750,350))
plot!(ts, y[1, :], label="V_out", size=(750,350))
Note that it includes some plotting code. We’ll setup some common functions too:
3 Examples
3.1 Passive RC, 2nd Order
function rc_2nd_order_passive(::Type{Circuit}; )
@circuit begin
# elements
= voltagesource()
jin = resistor(1000)
r1 = capacitor(1e-06)
c1 = resistor(1000)
r2 = capacitor(1e-06)
c2 = voltageprobe()
jout_1
# node connections
#= node 00 =# jin[-] == c1[1] == c2[1] == jout_1[-] == gnd
#= node 01 =# jin[+] == r1[1]
#= node 02 =# r1[2] == c1[2] == r2[1]
#= node 03 =# r2[2] == c2[2] == jout_1[+]
end
end
= DiscreteModel(rc_2nd_order_passive(Circuit), 1//fs)
model
= [n/fs for n in 0:10000]
ts = saw(ts, freq=25)
x = run!(model, x)
y
plot(ts, x[1, :], label="V_in", size=(750,350))
plot!(ts, y[1, :], label="V_out", size=(750,350))
3.2 Active RC, 2nd order
function rc_2nd_order_active(::Type{Circuit}; Rf=5000)
@circuit begin
# elements
= voltagesource()
j0 = resistor(Rf)
r1 = capacitor(1e-06)
c1 = opamp()
opa = resistor(Rf)
r2 = capacitor(1e-06)
c2 = voltageprobe()
jout_1
# node connections
#= node 00 =# j0[-] == c1[2] == c2[2] == jout_1[-] == opa["out-"] == gnd
#= node 01 =# j0[+] == r1[1]
#= node 02 =# r1[2] == c1[1] == opa["in+"]
#= node 03 =# opa["in-"] == opa["out+"] == r2[1]
#= node 04 =# r2[2] == c2[1] == jout_1[+]
end
end
= DiscreteModel(rc_2nd_order_active(Circuit), 1//fs)
model
= [n/fs for n in 0:10000]
ts = saw(ts, freq=25)
x = run!(model, x)
y
plot(ts, x[1, :], label="V_in", size=(750,350))
plot!(ts, y[1, :], label="V_out", size=(750,350))
3.3 Fixed Resonance, RC 2nd order
function rc_2nd_order_fixed_resonance(::Type{Circuit}; Rf=10000.0)
@circuit begin
# elements
= voltagesource()
j0 = resistor(Rf)
r1 = capacitor(4.7e-08)
c1 = opamp()
opa1 = resistor(Rf)
r2 = capacitor(4.7e-08)
c2 = opamp()
opa2 = voltageprobe()
jout_1
# node connections
#= node 00 =# j0[-] == c2[2] == jout_1[-] == opa1["out-"] == opa2["out-"] == gnd
#= node 01 =# j0[+] == r1[1]
#= node 02 =# r1[2] == c1[2] == opa1["in+"]
#= node 03 =# opa1["in-"] == opa1["out+"] == r2[1]
#= node 04 =# r2[2] == c2[1] == opa2["in+"]
#= node 05 =# c1[1] == opa2["in-"] == opa2["out+"] == jout_1[+]
end
end
= DiscreteModel(rc_2nd_order_fixed_resonance(Circuit), 1//fs)
model
= [n/fs for n in 0:10000]
ts = saw(ts, freq=25)
x = run!(model, x)
y
plot(ts, x[1, :], label="V_in", size=(750,350))
plot!(ts, y[1, :], label="V_out", size=(750,350))
3.4 Variable Resonance, RC 2nd order
function rc_2nd_order_variable_resonance(::Type{Circuit}; y=0.5,Rf=500)
@circuit begin
# elements
= voltagesource()
j0 = resistor(100000)
r1 = resistor(33000)
r2 = opamp()
opa1 = resistor(Rf)
r3 = opamp()
opa2 = resistor(Rf)
r4 = opamp()
opa3 = potentiometer(100000, (y == nothing ? () : (y,))...)
p = opamp()
opa4 = resistor(68000)
r5 = resistor(100000)
r6 = capacitor(1e-06)
c1 = capacitor(1e-06)
c2 = voltageprobe()
jout_1
# node connections
#= node 00 =# j0[-] == r2[2] == p[3] == r5[2] == c2[2] == jout_1[-] == opa1["out-"] == opa2["out-"] == opa3["out-"] == opa4["out-"] == gnd
#= node 01 =# j0[+] == r1[1]
#= node 02 =# r1[2] == r2[1] == opa1["in+"]
#= node 03 =# opa1["in-"] == opa1["out+"] == r3[1]
#= node 04 =# r3[2] == opa2["in+"] == c1[2]
#= node 05 =# opa2["in-"] == opa2["out+"] == r4[1]
#= node 06 =# r4[2] == opa3["in+"] == c2[1]
#= node 07 =# opa3["in-"] == opa3["out+"] == p[1] == jout_1[+]
#= node 08 =# p[2] == opa4["in+"]
#= node 09 =# opa4["in-"] == r5[1] == r6[1]
#= node 10 =# opa4["out+"] == r6[2] == c1[1]
end
end
= []
plots for y in [0.1, 0.2, 0.5, 0.8]
= DiscreteModel(rc_2nd_order_variable_resonance(Circuit, y=y), 1//fs)
model
= [n/fs for n in 0:10000]
ts = saw(ts, freq=25)
x = run!(model, x)
vout
= plot(ts, dropdims(x, dims=1), label="V_in", title=string("y=", y))
p plot!(ts, dropdims(vout, dims=1), label="V_out")
push!(plots, p)
end
plot(plots..., size=(750,350))
Note the unbounded oscillation that grows with ideal (i.e. non voltage clamped) opamps!
3.5 Diode Ladder Full
This produces quite a long code listing, expand below!
Code
function diode_ladder_full(::Type{Circuit}; ygain=0.5,yoffset=0.5,ycv1=0.5,ycv2=0.5,yr=0.5)
@circuit begin
# elements
= voltagesource()
j0 = potentiometer(100000, (ygain == nothing ? () : (ygain,))...)
pgain = capacitor(1e-06)
cdc = resistor(100000)
r1 = resistor(1000)
r2 = opamp()
opa1 = resistor(33000)
r3 = resistor(33000)
r4 = resistor(2000)
r5 = opamp()
opa2 = resistor(33000)
r6 = resistor(33000)
r7 = resistor(33000)
r8 = opamp()
opa3 = resistor(33000)
r9 = resistor(33000)
r10 = voltagesource(-12.0)
jneg1 = resistor(100000)
r11 = resistor(27000)
r12 = diode()
dcv = opamp()
opa4 = resistor(14000)
r13 = voltagesource(12.0)
jpos = voltagesource(-12.0)
jneg = potentiometer(100000, (yoffset == nothing ? () : (yoffset,))...)
poffset = resistor(27000)
r14 = resistor(100000)
r15 = voltagesource()
jcv1 = potentiometer(100000, (ycv1 == nothing ? () : (ycv1,))...)
pcv1 = resistor(68000)
r16 = voltagesource()
jcv2 = potentiometer(100000, (ycv2 == nothing ? () : (ycv2,))...)
pcv2 = resistor(68000)
r17 = diode()
d1 = capacitor(1e-09)
c1 = diode()
d2 = capacitor(1e-09)
c2 = diode()
d3 = capacitor(1e-09)
c3 = diode()
d4 = capacitor(1e-09)
c4 = diode()
d5 = capacitor(1e-09)
c5 = diode()
d6 = opamp()
opa5 = potentiometer(100000, (yr == nothing ? () : (yr,))...)
pr = opamp()
opa6 = resistor(100000)
r18 = diode()
dp1 = diode()
dp2 = resistor(20000)
rtrim = resistor(1000)
r19 = opamp()
opa7 = resistor(33000)
r20 = capacitor(1e-06)
cac = resistor(100000)
r21 = opamp()
opa8 = voltageprobe()
jout_1
# node connections
#= node 00 =# j0[-] == pgain[1] == r2[2] == opa2["in+"] == r8[2] == jneg1[-] == r12[2] == opa4["in+"] == jpos[-] == jneg[-] == jcv1[-] == pcv1[1] == jcv2[-] == pcv2[1] == c2[2] == c3[2] == c4[2] == pr[1] == rtrim[2] == opa7["in+"] == r21[2] == jout_1[-] == opa1["out-"] == opa2["out-"] == opa3["out-"] == opa4["out-"] == opa5["out-"] == opa6["out-"] == opa7["out-"] == opa8["out-"] == gnd
#= node 01 =# j0[+] == pgain[3]
#= node 02 =# pgain[2] == cdc[1]
#= node 03 =# cdc[2] == r1[1]
#= node 04 =# r1[2] == r2[1] == opa1["in+"]
#= node 05 =# opa1["in-"] == opa1["out+"] == r3[1] == r9[1]
#= node 06 =# r3[2] == r4[1] == opa2["in-"] == r6[2]
#= node 07 =# r4[2] == r5[1] == d1[+]
#= node 08 =# r5[2] == opa2["out+"]
#= node 09 =# r6[1] == r7[1] == r11[2] == r12[1] == dcv[-]
#= node 10 =# r7[2] == r8[1] == opa3["in+"]
#= node 11 =# opa3["in-"] == r9[2] == r10[1]
#= node 12 =# opa3["out+"] == r10[2] == d6[-]
#= node 13 =# jneg1[+] == r11[1]
#= node 14 =# dcv[+] == opa4["out+"] == r13[1]
#= node 15 =# opa4["in-"] == r13[2] == r15[1] == r16[2] == r17[2]
#= node 16 =# jpos[+] == poffset[3]
#= node 17 =# poffset[1] == r14[1]
#= node 18 =# jneg[+] == r14[2]
#= node 19 =# poffset[2] == r15[2]
#= node 20 =# jcv1[+] == pcv1[3]
#= node 21 =# pcv1[2] == r16[1]
#= node 22 =# jcv2[+] == pcv2[3]
#= node 23 =# pcv2[2] == r17[1]
#= node 24 =# d1[-] == c1[1] == d2[+]
#= node 25 =# d2[-] == c2[1] == d3[+]
#= node 26 =# d3[-] == c3[1] == d4[+] == opa5["in+"]
#= node 27 =# d4[-] == c4[1] == d5[+]
#= node 28 =# d5[-] == c5[1] == d6[+]
#= node 29 =# opa5["in-"] == opa5["out+"] == pr[3] == r19[1]
#= node 30 =# pr[2] == opa6["in+"]
#= node 31 =# opa6["in-"] == r18[1] == dp1[+] == dp2[-] == rtrim[1]
#= node 32 =# c1[2] == c5[2] == opa6["out+"] == r18[2] == dp1[-] == dp2[+]
#= node 33 =# r19[2] == opa7["in-"] == r20[1]
#= node 34 =# opa7["out+"] == r20[2] == cac[1]
#= node 35 =# cac[2] == r21[1] == opa8["in+"]
#= node 36 =# opa8["in-"] == opa8["out+"] == jout_1[+]
end
end
for yr in [0.4, 0.5, 0.6, 0.7]
= DiscreteModel(diode_ladder_full(Circuit, ygain=1.0, yoffset=0.5,ycv1=0.25,ycv2=0.5,yr=yr), 1//fs)
model
= [n/fs for n in 0:20000]
ts = saw(ts, freq=25)
vin = sine(ts, freq=1)
vcv1in = [0 for t in ts]
vcv2in = [vin[1, :], vcv1in[1, :], vcv2in]
x = permutedims(hcat(x...))
x = run!(model, x)
y
plot(ts, x[1, :], label="V_in", size=(750,350), title=string("yr = ", yr))
plot!(ts, x[2, :], label="V_cvin1")
display(plot!(ts, y[1, :], label="V_out"))
end
4 Conclusions
It was super easy to get up and running with the library, and it handled quite complex circuits with no issue at all! The full diode ladder circuit takes about an hour to symbolically solve (numerical solution is much faster of course), so the near instantaneous result here is impressive. The library will be a useful way to cross check my results against.