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.

Julia 1.9

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.@circuitMacro
@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")

Note

Instead of (\longleftrightarrow), one can also use ==.

source

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!Function
add!(c::Circuit, elem::Element)

Adds the element elem to the circuit c, creating and returning a new, unique reference designator, leaving its pins unconnected.

source
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.

source
Base.delete!Function
delete!(c::Circuit, designator::Symbol)

Deletes the element named designator from the circuit c (disconnecting all its pins).

source
ACME.connect!Function
connect!(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 Symbols, 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
source
ACME.disconnect!Function
disconnect!(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.

source

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.SimpleSolverType
SimpleSolver

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.

source
ACME.HomotopySolverType
HomotopySolver{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.

source
ACME.CachingSolverType
CachingSolver{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.

source

The default solver used is a HomotopySolver{CachingSolver{SimpleSolver}}.