User Guide
Element Creation
All circuit elements are created by calling corresponding functions; see the Element Reference for details.
Unitful elements
ACME provides a package extension for Unitful to support quantities with units when constructing elements. E.g. resistor(4.7e3)
and resistor(4.7u"kΩ")
are equivalent after Unitful
has been loaded. This can increase readability and help catch bugs (e.g. resistor(5u"V")
will throw an error). The input and output signals of the curcuit models will still be unitless, however.
Package extensions require Julia 1.9 or later. Consequently, unitful quantities are not supported on earlier Julia versions.
Circuit Description
Circuits are described using Circuit
instances, which are most easily created using the @circuit
macro:
ACME.@circuit
— Macro@circuit begin #= ... =# end
Provides a simple domain-specific language to decribe circuits. The begin
/end
block can hold element definitions of the form refdes = elementfunc(params)
and connection specifications of the form refdes1[pin1] ⟷ refdes2[pin2]
.
Example
To create a circuit with a voltage source connected to a resistor:
@circuit begin
src = voltagesource(5)
r = resistor(1000)
src[+] ⟷ r[1]
src[-] ⟷ r[2]
end
Alternatively, connection specifications can be given after an element specification, separated by commas. In that case, the refdes
may be omitted, defaulting to the current element.
Example
@circuit begin
src = voltagesource(5)
r = resistor(1000), src[+] ⟷ [1], src[-] ⟷ [2]
end
Finally, a connection endpoint may simply be of the form netname
, to connect to a named net. (Such named nets are created as needed.)
Example
@circuit begin
src = voltagesource(5), [-] ⟷ gnd
r = resistor(1000), [1] ⟷ src[+], [2] ⟷ gnd
end
If a net or pin specification is not just a single symbol or number, and has to be put in quotes (e.g. "in+"
, "9V"
)
Instead of ⟷
(\longleftrightarrow
), one can also use ==
.
The pins provided by each type of element are described in the Element Reference.
Instead of or in addition to using the @circuit
macro, Circuit
instances can also be populated and modified programmatically using the following functions:
ACME.add!
— Functionadd!(c::Circuit, elem::Element)
Adds the element elem
to the circuit c
, creating and returning a new, unique reference designator, leaving its pins unconnected.
add!(c::Circuit, designator::Symbol, elem::Element)
Adds the element elem
to the circuit c
with the reference designator designator
, leaving its pins unconnected. If the circuit already contained an element named designator
, it is removed first.
Base.delete!
— Functiondelete!(c::Circuit, designator::Symbol)
Deletes the element named designator
from the circuit c
(disconnecting all its pins).
ACME.connect!
— Functionconnect!(c::Circuit, pins::Union{Symbol,Tuple{Symbol,Any}}...)
Connects the given pins (or named nets) to each other in the circuit c
. Named nets are given as Symbol
s, pins are given as Tuple{Symbols,Any}
s, where the first entry is the reference designator of an element in c
, and the second entry is the pin name. For convenience, the latter is automatically converted to a Symbol
as needed.
Example
circ = Circuit()
add!(circ, :r, resistor(1e3))
add!(circ, :src, voltagesource(5))
connect!(circ, (:src, -), (:r, 2), :gnd) # connect to gnd net
ACME.disconnect!
— Functiondisconnect!(c::Circuit, p::Tuple{Symbol,Any})
Disconnects the given pin p
from anything else in the circuit c
. The pin is given as aTuple{Symbols,Any}
, where the first entry is the reference designator of an element in c
, and the second entry is the pin name. For convenience, the latter is automatically converted to a Symbol
as needed. Note that if e.g. three pin p1
, p2
, and p3
are connected then disconnect!(c, p1)
will disconnect p1
from p2
and p3
, but leave p2
and p3
connected to each other.
For example, a cascade of 20 RC-lowpasses could be generated by:
circ = @circuit begin
src = voltagesource(), [-] ⟷ gnd
output = voltageprobe(), [-] ⟷ gnd
end
pin = (:src, +)
for i in 1:20
resrefdes = add!(circ, resistor(1000))
caprefdes = add!(circ, capacitor(10e-9))
connect!(circ, (resrefdes, 1), pin)
connect!(circ, (resrefdes, 2), (caprefdes, 1))
connect!(circ, (caprefdes, 2), :gnd)
global pin = (resrefdes, 2)
end
connect!(circ, pin, (:output, +))
Model Creation and Use
A Circuit
only stores elements and information about their connections. To simulate a circuit, a model has to be derived from it. This can be as simple as:
model = DiscreteModel(circ, 1/44100)
Here, 1/44100
denotes the sampling interval, i.e. the reciprocal of the sampling rate at which the model should run. Optionally, one can specify the solver to use for solving the model's non-linear equation:
model = DiscreteModel(circ, 1/44100, HomotopySolver{SimpleSolver})
See Solvers for more information about the available solvers.
Once a model is created, it can be run:
y = run!(model, u)
The input u
is matrix with one row for each of the circuit's inputs and one column for each time step to simulate. Likewise, the output y
will be a matrix with one row for each of the circuit's outputs and one column for each simulated time step. The order of the rows will correspond to the order in which the respective input and output elements were added to the Circuit
. So for above circuit, we may obtain the first 100 samples of the impulse response with
run!(model, [1 zeros(1,99)])
# output
1×100 Matrix{Float64}:
1.83357e-8 3.1622e-7 2.59861e-6 … 0.00465423 0.00459275 0.00453208
To simulate a circuit without inputs, a matrix with zero rows may be passed:
y = run!(model, zeros(0, 100))
The internal state of the model (e.g. capacitor charges) is preserved accross calls to run!
.
Each invocation of run!
in this way has to allocate some memory as temporary storage. To avoid this overhead when running the same model for many small input blocks, a ModelRunner
instance can be created explicitly:
runner = ModelRunner(model, false)
run!(runner, y, u)
By using a pre-allocated output y
as in the example, allocations in run!
are reduced to a minimum.
Upon creation of a DiscreteModel
, its internal states (e.g. capacitor charges) are set to zero. It is also possible to set the states to a steady state (if one can be found) with:
steadystate!(model)
This is often desirable for circuits where bias voltages are only slowly obtained after turning them on.
Solvers
ACME.SimpleSolver
— TypeSimpleSolver
The SimpleSolver
is the simplest available solver. It uses Newton iteration which features fast local convergence, but makes no guarantees about global convergence. The initial solution of the iteration is obtained by extrapolating the last solution found (or another solution provided externally) using the available Jacobians. Due to the missing global convergence, the SimpleSolver
is rarely useful as such.
ACME.HomotopySolver
— TypeHomotopySolver{BaseSolver}
The HomotopySolver
extends an existing solver (provided as the type parameter) by applying homotopy to (at least theoretically) ensure global convergence. It can be combined with the SimpleSolver
as HomotopySolver{SimpleSolver}
to obtain a useful Newton homtopy solver with generally good convergence properties.
ACME.CachingSolver
— TypeCachingSolver{BaseSolver}
The CachingSolver
extends an existing solver (provided as the type parameter) by storing found solutions in a k-d tree to use as initial solutions in the future. Whenever the underlying solver needs more than a preset number of iterations (defaults to five), the solution will be stored. Storing new solutions is a relatively expensive operation, so until the stored solutions suffice to ensure convergence in few iterations throughout, use of a CachingSolver
may actually slow things down.
See M. Holters, U. Zölzer, "A k-d Tree Based Solution Cache for the Non-linear Equation of Circuit Simulations" for a more detailed discussion.
The default solver used is a HomotopySolver{CachingSolver{SimpleSolver}}
.