Amber is a native Julia circuit and dynamical-system simulator. It combines a Julia-native circuit language with retained hierarchy, a typed compiler IR, sparse generalized modified nodal analysis (MNA), nonlinear and time-domain solvers, noise analysis, control theory, statistical studies, and reproducible result metadata.
There is no external SPICE process behind Amber. A circuit is an ordinary Julia object, compilation produces an inspectable sparse numerical representation, and analyses return Julia data that can be measured, transformed, serialized, or passed directly to the rest of the Julia ecosystem.
Amber is currently an early-stage 0.1 package. It is useful for inspectable
analog simulation, research, education, model development, and workflows that
benefit from programmatic circuit generation. Read Scope and model
boundaries before using it for sign-off work.
Four views of Amber: transient and frequency-domain analysis, charge-based CMOS characterization, and a two-port Smith chart. Generated with AmberMakie from the reproducible example.
Deeper analyses: inverter noise margins, correlated bridge variation, spectral and integrated amplifier noise, and a 10 kHz oscillator starting up. The bridge variation is illustrative. Reproduce the figure with the advanced example.
Design exploration: closed-loop poles crossing the stability boundary, an eye diagram with sinusoidal interference, sampled yield over two resistor values, and Lissajous curves around an RLC resonance. The yield variations are illustrative. Reproduce the figure with the design-space example.
| Area | Capabilities |
|---|---|
| Circuit construction | @circuit, reusable @subcircuit templates, programmatic CircuitBuilder, arrays, loops, conditionals, retained instance paths, named observations |
| Devices | R, G, C, L, independent and controlled sources, diodes, NPN BJTs, NMOS/PMOS, switches, behavioral op-amps, nonlinear behavioral sources |
| Extended devices | Zener, Schottky, LED, photodiode, solar cell, N/P JFET, thermistor, varistor, controlled resistor, potentiometer, multiplier, limiter, comparator, ideal transformer, diode bridge, crystal, lumped RLGC line |
| Physical details | resistor excess noise and material families, passive package parasitics, calibrated capacitor loss and dielectric absorption, diode depletion/diffusion charge, BJT charge, MOS gate capacitances, matched devices |
| Core analyses | operating point, BDF1/BDF2 transient, small-signal AC, parameter sweeps, periodic steady state |
| Noise | stationary frequency-domain noise, input-referred noise, contribution budgets, integrated noise, stochastic transient noise, cyclostationary periodic noise, oscillator phase noise |
| RF and control | multiport Z/Y/S/ABCD/H parameters, descriptor-system linearization, poles, zeros, stability, root locus, step/impulse response, bias-preserving loop gain and margins |
| Measurements | voltage/current/power/charge/state traces, transfer functions, bandwidth, delay, resonances, FFT spectra, THD, THD+N, SNR, SINAD, SFDR, ENOB, sampling and propagation metrics, CMOS inverter noise margins and switching energy |
| Visualization (optional AmberMakie) | linked result workbenches, RF/control plots, eye and jitter views, CMOS bias dashboards, inverter and switching plots, mismatch studies, figures with metadata |
| Studies and reproducibility | copy-on-write parameter overrides, Monte Carlo with independent/correlated/process/matched variation, sample replay, failure retention, provenance, stable TOML serialization |
| Diagnostics | structural validation, floating-net and ideal-constraint detection, hierarchy-aware lookup errors, dominant residual reporting, validity warnings |
Amber requires Julia 1.10 or later. Until it is registered, install it directly from GitHub:
using Pkg
Pkg.add(url="https://github.com/thimotedupuch/Amber.jl")For development, clone the repository and instantiate it:
git clone https://github.com/thimotedupuch/Amber.jl
cd Amber.jl
julia --project=. -e 'using Pkg; Pkg.instantiate(); Pkg.test()'The @circuit form turns a function-like definition into a reusable,
immutable CircuitDesign. Names on the left-hand side become stable net and
device names.
using Amber
@circuit LowPass(; R=10kΩ, C=10nF) begin
gnd = ground()
vin = node()
vout = node()
Source = voltage_source(
vin, gnd;
dc=1V,
ac=1V,
waveform=Step(low=0V, high=1V, at=100μs, rise=1μs),
)
R1 = resistor(vin, vout; value=R)
C1 = capacitor(vout, gnd; value=C)
observe(voltage(vout); name=:output)
end
filter = LowPass()Values such as 10kΩ, 1V, and 100μs are plain SI-scaled Julia numbers.
The suffixes make circuit code readable without imposing a runtime units
dependency.
Validate before a long run, then choose an analysis:
isempty(check(filter)) # true: no structural errors
println(explain(filter)) # compact design and validation summary
op = operating_point(filter)
@assert voltage(op, :vout)[1] ≈ 1V
ac = small_signal(filter, 10Hz => 10MHz; source=:Source, points=301)
gain = transfer(ac; input=voltage(:vin), output=voltage(:vout))
corner = only(cutoff_frequencies(
ac;
input=voltage(:vin),
output=voltage(:vout),
))
tran = transient(
filter,
0s => 1ms;
initial=:discharged,
max_step=2μs,
event_mode=:exact,
)
vout = observation(tran, :output) # the named observation
iin = current(tran, :Source)All result data is directly accessible. result.axis is time for transient
results and frequencies(result) is the AC grid. Traces are vectors; no
plotting package is required.
rows = result_table(tran) # dependency-free Vector{NamedTuple}
meta = provenance(tran) # circuit, parameters, analysis, warnings
summary = report(tran) # readable report with dictionary access
println(summary) # includes solver/model warnings
warnings = summary[:warnings] # structured data remains available
selected = result_table(tran; signals=(output_V=:output, supply_A=current(:Source)))report includes solver and model-validity warnings. For operating-point,
transient, and small-signal results, use report(result; detailed=true) to
include the full iteration and integration histories. Dict(report(result))
returns a plain dictionary. Transient device metrics default to the full saved
record, including startup. Use report(tran; window=200μs => 1ms) to select
a time window; reports label both requested bounds and actual saved-sample
bounds. RMS current uses a time-weighted trapezoidal integral of squared current
on the selected saved samples. Window bounds must lie within the saved record;
no boundary interpolation is performed.
In a REPL or notebook, displaying a circuit or an operating-point, transient,
AC, noise, sweep, or Monte Carlo result gives a compact text summary instead
of internal solver arrays. Result summaries distinguish solver convergence
from physical validity, show units and saved ranges, and highlight partial runs
and failed study points. Use explain_failure(result) for an incomplete
simulation's recorded reason and next steps, or result.failures for study
failures. Use report(result) or validity_report(result) for simulation model checks.
Display does not run a simulation or compute model-validity metrics.
Use AmberMakie when interactive Makie workbenches, Bode plots, Smith charts, spectra, noise budgets, or publication figures are wanted. It is a separate package so the simulation core does not depend on a graphics stack.
Each example below is self-contained. Save a block as simulate.jl and run
julia --project=. simulate.jl in an environment containing Amber.
For common tasks, jump to op-amp wiring and noise measurement or rectifier startup, ripple, and device warnings.
A quarter-bridge uses one active 350 Ω strain gauge and three fixed resistors. With gauge factor 2 and 500 microstrain, its fractional resistance change is 0.001. The differential output is negative for the terminal order below: about −1.25 mV with 5 V excitation. This ideal example omits lead resistance, resistor mismatch, self-heating, and amplifier loading.
using Amber
@circuit StrainBridge(; strain=500e-6, gauge_factor=2.0, R=350Ω) begin
gnd = ground(); excitation = node(); sense = node(); reference = node()
Supply = voltage_source(excitation, gnd; dc=5V)
Gauge = resistor(excitation, sense; value=R * (1 + gauge_factor * strain))
R2 = resistor(sense, gnd; value=R)
R3 = resistor(excitation, reference; value=R)
R4 = resistor(reference, gnd; value=R)
end
bridge = StrainBridge()
@assert isempty(check(bridge))
bias = operating_point(bridge)
@assert bias.stats[:converged]
vbridge = only(voltage(bias, :sense, :reference))
expected = 5V * (1 / (2 + 2.0 * 500e-6) - 1 / 2)
@assert isapprox(vbridge, expected; atol=1e-9)
# Change only the gauge resistance; retain the compiled topology.
strains = range(-1000e-6, 1000e-6; length=21)
study = sweep(compile(bridge), "Gauge.value" => 350Ω .* (1 .+ 2.0 .* strains);
analysis=OperatingPoint(),
metric=r -> only(voltage(r, :sense, :reference)))
@assert failure_rate(study) == 0
println((bridge_output_V=vbridge, strains=strains,
bridge_outputs_V=study.metrics, failures=failure_rate(study)))The five opamp terminals are positive input, negative input, output,
positive supply, negative supply. A follower connects its negative input to
its output. Supply pins are nodes driven by voltage sources; model parameters
belong inside BehavioralOpAmp.
using Amber
@circuit NoisyFollower() begin
gnd = ground(); input = node(); output = node(); vdd = node(); vss = node()
VDD = voltage_source(vdd, gnd; dc=5V)
VSS = voltage_source(vss, gnd; dc=-5V)
Source = voltage_source(input, gnd; dc=1V, ac=1V)
Buffer = opamp(input, output, output, vdd, vss;
model=BehavioralOpAmp(dc_gain=1e5, gain_bandwidth=1MHz,
input_voltage_noise_density=5e-9)) # V/√Hz
Load = resistor(output, gnd; value=10kΩ)
end
follower = NoisyFollower()
op = operating_point(follower)
@assert op.stats[:converged]
println((output_V=only(voltage(op, :output)),))
println(report(op))
nr = noise(follower, 10Hz => 100kHz;
output=voltage(:output), input=:Source, points=201)
println(report(nr))
density = noise_density(nr) # output V/√Hz at frequencies(nr)
input_density = input_referred_noise_density(nr) # input V/√Hz
band = 20Hz => 20kHz
println((band_Hz=band, output_noise_Vrms=integrated_noise(nr, band),
input_noise_Vrms=integrated_noise(nr, band; referred=:input)))output takes an observable such as voltage(:output); input names the
independent source used for input referral. noise_psd(nr) returns V²/Hz for
this voltage output; noise_density(nr) returns its square root, and
integrated_noise integrates the PSD over the named band before taking the
square root. The behavioral model is illustrative; choose noise parameters
appropriate to the device being modeled.
This half-wave rectifier converts a 10 V peak, 50 Hz sine into a DC output. Measure the last two cycles after startup, then compare the ripple with the small-ripple estimate ΔV ≈ Iload / (f C). Half-wave rectification recharges once per input cycle; a full-wave bridge would use twice the input frequency.
using Amber, Statistics
@circuit SmoothingRectifier(; C=470μF, R=1kΩ) begin
gnd = ground(); input = node(); output = node()
Source = voltage_source(input, gnd;
waveform=Sine(amplitude=10V, frequency=50Hz))
D1 = diode(input, output;
model=JunctionDiode(saturation_current=2nA, ideality=1.7,
series_resistance=120mΩ))
C1 = capacitor(output, gnd; value=C, esr=180mΩ)
Load = resistor(output, gnd; value=R)
end
rectifier = SmoothingRectifier()
@assert isempty(check(rectifier))
tr = transient(rectifier, 0s => 300ms; initial=:discharged,
max_step=50μs, saveat=50μs, reltol=1e-6)
@assert tr.stats[:converged]
settled_window = 260ms => 300ms
println(report(tr)) # full record, including startup stresses and validity warnings
println(report(tr; window=settled_window)) # device metrics over the late window
settled = (tr.axis .>= first(settled_window)) .& (tr.axis .<= last(settled_window))
vdc = mean(voltage(tr, :output)[settled])
ripple = peak_to_peak(voltage(:output); window=settled_window)(tr)
ripple_estimate = (vdc / 1kΩ) / (50Hz * 470μF)
@assert 0V < vdc < 10V
println((window_s=settled_window, dc_output_V=vdc, ripple_Vpp=ripple,
estimated_ripple_Vpp=ripple_estimate))The reports explicitly warn that the capacitor ripple-current rating is unspecified: solver convergence does not establish its thermal validity. The full-record device metrics include startup; the selected-window report and ripple measurement use the same late window. This window is caller-selected, not an automatic steady-state detection.
The estimate ignores diode conduction time and ESR, so it is a sanity check,
not an exact equality. Reduce max_step and compare successive late cycles
before relying on a ripple measurement. Increase C to explore the tradeoff
between ripple and charging-current peaks, using current(tr, :D1).
Sweep the input of a complementary MOS inverter from ground to its 5 V supply. The NMOS bulk connects to ground and the PMOS bulk to the supply; transistor terminals are ordered drain, gate, source, bulk. These illustrative Level-1 parameters describe an educational model, not a specific fabrication process.
using Amber
@circuit LogicInverter begin
gnd = ground(); supply = node(); input = node(); output = node()
VDD = voltage_source(supply, gnd; dc=5V)
Input = voltage_source(input, gnd; dc=0V)
PullDown = nmos(output, input, gnd, gnd;
model=Level1MOSFET(threshold_voltage=0.7V,
transconductance=2mA/V^2, channel_length_modulation=0.03/V))
PullUp = pmos(output, input, supply, supply;
model=Level1MOSFET(threshold_voltage=0.7V,
transconductance=1mA/V^2, channel_length_modulation=0.03/V))
Load = capacitor(output, gnd; value=20pF)
end
inverter = LogicInverter()
@assert isempty(check(inverter))
inputs = range(0V, 5V; length=101)
vtc = sweep(compile(inverter), "Input.dc" => inputs;
analysis=OperatingPoint(), metric=r -> only(voltage(r, :output)))
@assert failure_rate(vtc) == 0
outputs = Float64.(vtc.metrics)
@assert outputs[1] > 4.9V # low input gives high output
@assert outputs[end] < 0.1V # high input gives low output
# Approximate switching point: the sampled point nearest Vout = Vin.
k = argmin(abs.(outputs .- inputs))
println((input_V=collect(inputs), output_V=outputs,
switching_input_V=inputs[k], failures=failure_rate(vtc)))Refine the input grid near the transition for a more precise switching point.
The capacitor is open at DC; switching delay requires a transient analysis
with a pulse input and resolved rise/fall times. Use ChargeBasedMOSFET when
continuous weak-inversion behavior and conserving terminal charges matter.
For more detailed models, see the precision bridge, rectifier startup, and CMOS inverter switching examples.
@circuit supports ordinary Julia expressions, including loops and
conditionals. Assigning a primitive gives it a name; an unassigned primitive is
still part of the circuit but receives an automatic name.
@circuit ParallelBank(; branches=8, r=10kΩ) begin
gnd = ground()
input = node()
output = node()
Source = voltage_source(input, gnd; dc=1V)
if branches > 0
for _ in 1:branches
resistor(input, output; value=r)
end
end
Load = resistor(output, gnd; value=r)
observe(voltage(output); name=:output)
endTop-level circuit arguments are parameters: LowPass(R=47kΩ) builds the same
structure with a different value. Structural choices can be ordinary Julia
arguments too, as in ParallelBank(branches=12).
@subcircuit defines explicit ports and parameters. A subcircuit has no hidden
global ground; reference and supply connections are explicit.
@subcircuit RCSection(input, output, reference; R=1kΩ, C=1nF) begin
R1 = resistor(input, output; value=R)
C1 = capacitor(output, reference; value=C)
observe(voltage(output); name=:output)
end
@circuit TwoPoleFilter(; R=1kΩ, C=100nF) begin
gnd = ground()
input = node()
middle = node()
output = node()
Source = voltage_source(input, gnd; dc=1V, ac=1V)
First = RCSection(input, middle, gnd; R=R, C=C)
Second = RCSection(middle, output, gnd; R=R, C=C)
end
circuit = TwoPoleFilter()
describe(circuit)
resolve(circuit, "Second.R1")
result = small_signal(circuit, 10Hz => 100kHz; source=:Source)
voltage(result, "First.output") # hierarchy-qualified port/net lookup
current(result, "Second.R1")Hierarchy is retained in the design and elaboration index. It provides stable paths for inspection, parameter updates, observations, reports, and errors, while the solver works on one globally assembled sparse system.
For generated topologies, use CircuitBuilder. Builder handles are ownership
checked, and finish closes the mutable construction phase by producing an
immutable design.
function rc_ladder(stages; R=1kΩ, C=10nF)
builder = CircuitBuilder(:RCLadder)
gnd = ground!(builder, :gnd)
x = node_array!(builder, :x, 0:stages)
add!(builder, voltage_source(x[0], gnd; dc=1V); name=:Source)
for k in 1:stages
add!(builder, resistor(x[k-1], x[k]; value=R); name=(:R, k))
add!(builder, capacitor(x[k], gnd; value=C); name=(:C, k))
end
observe!(builder, voltage(x[stages]); name=:output)
finish(builder)
end
ladder = rc_ladder(200)
summary(ladder)
devices(ladder; kind=:resistor, limit=5)
nets(ladder; limit=5)instances! provides the same pattern for arrays of subcircuits with generated
names, connections, and parameter sets.
bias = operating_point(circuit; temperature=300K)
voltage(bias, :output)
current(bias, "Second.R1")
power(bias, "Second.R1")Newton iterations use variable and row scaling, a backtracking line search, and
typed voltage/current/state tolerances. Operating-point solving starts with
source and temporary-gmin continuation and falls back to a pseudo-transient
path when necessary. Solver behavior is configurable:
options = SolverOptions(
reltol=1e-8,
voltage_abstol=1e-10,
current_abstol=1e-13,
max_newton_iterations=100,
linear_solver=SuiteSparseLU(ordering=:amd, pivot_tolerance=0.1),
)
bias = operating_point(circuit; solver=options)
println(explain_failure(bias))Nonconverged results retain statistics and dominant residual information rather than erasing the attempted run.
Amber implements native BDF1 and variable-step-coefficient BDF2 integration of
stored charge and flux. Nonlinear device charge history participates directly
in the discrete equations, preserving the BDF charge balance.
It supports fixed or adaptive stepping, operating-point or discharged initial
states, capacitor initial voltage, and exact insertion of Step/Pulse
waveform boundaries.
Set saveat to choose output spacing independently of internal steps. Both
endpoints are retained; the last interval is shorter when the duration is not
divisible by saveat. Without saveat, all accepted steps are returned.
Supplying saveat or max_step selects fixed stepping unless adaptive=true
is explicit. Resolve finite source edges with several internal steps even in
exact-event mode. When output omits internal steps, dynamic currents are
reconstructed from the saved charge traces; use sufficiently fine saveat
when measuring current peaks.
result = transient(
circuit,
0s => 5ms;
method=:bdf2,
max_step=1μs,
event_mode=:exact,
)
peak_to_peak(voltage(:output); window=4ms => 5ms)(result)
overshoot(result, :output)The built-in Step, Sine, and Pulse waveforms are callable Julia objects.
Exact-event mode also enables modeled switch charge injection at supported
prescribed control transitions.
Small-signal analysis finds the nonlinear bias point, evaluates the analytic Jacobian and dynamic matrix there, and solves the complex descriptor system at each frequency.
response = small_signal(circuit, 1Hz => 10MHz; source=:Source, points=501)
H = transfer(response; input=voltage(:input), output=voltage(:output))
db20(H)
phase(H; unwrap=true)
group_delay(frequencies(response), H)
passbands(frequencies(response), H)
resonances(frequencies(response), H)Ports turn the same linearized machinery into multiport network data. A network characterization circuit normally omits independent sources at its ports:
@circuit RCNetwork begin
gnd = ground()
input = node()
output = node()
R1 = resistor(input, output; value=1kΩ)
C1 = capacitor(output, gnd; value=100nF)
R2 = resistor(output, gnd; value=10kΩ)
end
network = port_response(
RCNetwork(),
10Hz => 1MHz;
ports=[
Port(:input, :gnd; reference_impedance=50Ω, name=:in),
Port(:output, :gnd; reference_impedance=50Ω, name=:out),
],
)
Z = impedance(network)
Y = admittance(network)
Sparams = network_parameters(network, :s)
ABCD = network_parameters(network, :abcd)
renormalized = renormalize(network, 75Ω)Z, Y, and S are available for any port count. ABCD and hybrid H conversion are available for two-port networks.
Amber can expose a linearized circuit as the descriptor system
E x' = A x + B u
y = C x + D u
This retains algebraic MNA constraints instead of forcing the circuit into an ordinary state-space form.
model = linearize(circuit; inputs=:Source, outputs=voltage(:output))
dcgain(model)
poles(model)
transmission_zeros(model)
natural_frequencies(model)
damping_ratios(model)
isstable(model)
frequency = frequency_response(model, 10Hz => 1MHz; points=400)
margins = stability_margins(frequency)
step = step_response(model, 0s => 10ms; saveat=10μs)
rise_time(step)
settling_time(step)
root_locus(model, 0.0:0.1:2.0)For a single-ended feedback loop, insert an ideal zero-volt voltage source
in series with the loop wire. loop_gain uses Tian's two-injection method,
including loading and reverse transmission. The wire must intersect all return
paths being studied. The returned convention is 1 + L for return difference;
loop_sensitivity and closed_loop_response are the normalized quantities
1/(1+L) and L/(1+L), not arbitrary circuit input/output transfers.
loop = loop_gain(
circuit,
10Hz => 10MHz;
probe=VoltageLoopProbe(:Probe, voltage(:feedback_node)), # either endpoint, relative to ground
)
gain_margin(loop)
phase_margin(loop)
loop_sensitivity(loop)
closed_loop_response(loop)Use voltage(:endpoint, :reference) for a separate reference node. A current
probe requires both a zero-DC current source from a wire endpoint to reference
and a zero-volt series sensing source:
CurrentLoopProbe(:Injection, current(:Sense)). A driven open-loop amplifier
transfer should be measured with linearize / frequency_response.
Noise analysis uses adjoint propagation through the small-signal system. It tracks each physical source, including correlations, so totals can be grouped by component or mechanism.
noise_result = noise(
circuit,
10Hz => 1MHz;
output=voltage(:output),
input=:Source,
points=301,
temperature=300K,
)
output_density = noise_density(noise_result) # V/√Hz here
input_density = input_referred_noise_density(noise_result)
rms_noise = integrated_noise(noise_result, 20Hz => 20kHz)
budget = noise_contributions(noise_result; mechanism=:thermal)Available modeled mechanisms depend on the device: resistor/conductance thermal noise, resistor excess noise, diode shot and flicker noise, BJT shot/base/ collector/flicker noise, MOS channel/gate/flicker noise with gate-channel correlation, and behavioral op-amp input voltage/current noise.
For sampled stochastic trajectories, use a fixed grid and an explicit seed:
noisy = transient_noise(
circuit,
0s => 1ms;
timestep=100ns,
saveat=1μs,
seed=0xA83E,
low_frequency_cutoff=10Hz,
)The same seed and configuration reproduce the same trajectory.
Shooting-based periodic steady state finds a circuit orbit and computes its variational monodromy and Floquet multipliers.
pss = periodic_steady_state(
driven_circuit;
period=1ms,
saveat=5μs,
max_step=5μs,
)
orbit = pss.orbit
multipliers = floquet_multipliers(pss)
pn = periodic_noise(
pss,
10Hz => 100kHz;
output=voltage(:output),
sidebands=-5:5,
)periodic_noise solves a harmonic-lifted cyclostationary system and reports
sideband and source contributions. For a converged autonomous oscillator PSS,
phase_noise(pss, offsets; output=...) additionally computes the adjoint phase
sensitivity, neutral Floquet mode, phase-diffusion coefficient, and single-
sideband phase noise in dBc/Hz.
Uniform transient records can be transformed and characterized without another DSP package:
spectral = spectrum(result; signal=voltage(:output), window=:blackman_harris)
harmonics = harmonic_analysis(
result;
signal=voltage(:output),
fundamental=:auto,
harmonics=10,
window=:hann,
)
thd(harmonics)
thdn(harmonics)
snr(harmonics)
sinad(harmonics)
sfdr(harmonics)
enob(harmonics)
crest_factor(spectral)Nonuniform records are resampled and the operation is recorded in result
warnings. Other helpers include sampling_metrics, propagation_delay,
compare, band_power, quality_factor, and notch_depth.
CMOS measurements are available in Amber without a plotting dependency:
transfer_metrics = invertermetrics(vtc; output=:output)
transfer_metrics.measurements # VIL, VIH, VOH, VOL, NML, NMH, VM
transfer_metrics.warnings # unresolved or invalid transfer curves
measurements = switchingmetrics(tr; input=:input, output=:output,
supply=:VDD, vdd=1.8V, window=100ns => 200ns)switchingmetrics measures mean 50% delays and delivered supply energy over
an explicit window; select a settled cycle for energy/cycle, including leakage.
For a single edge use propagation_delay(tr; input=:input, output=:output, threshold=0.9V, input_edge=:rising, output_edge=:falling, occurrence=1, window=100ns=>200ns). Separate input_threshold and output_threshold support
unequal logic levels. Automatic edge selection rejects ambiguous periodic
records; missing responses or recrossings return NaN. Both timing helpers
reject nonconverged results. Refine the timestep and compare delays and
energy; a converged solver is not an accuracy guarantee. The
Amber-only inverter example demonstrates
this check.
ChargeBasedMOSFET adds continuous weak-to-strong inversion, explicit W/L and
multiplicity, conserving terminal charges, optional junctions, and temperature
laws. It is a bounded native long-channel model, not a foundry model.
mos = ChargeBasedMOSFET(width=8μm, length=2μm)
point = mosfet_operating_point(mos, :nmos, 1.2V, 1V, 0V, 0V)
point.gm_over_id
point.capacitance_matrix # signed dQi/dVj: drain, gate, source, bulkThe same model works with nmos/pmos, compiled parameter updates, AC,
transient and noise. See the model implementation and
verification tests for equations,
characterization sweeps, and conservation checks.
Instance geometry can be set at construction or updated on a compiled circuit:
@circuit MOSBias begin
gnd = ground(); drain = node(); gate = node()
VD = voltage_source(drain, gnd; dc=1.2V)
VG = voltage_source(gate, gnd; dc=1V, ac=1V)
M1 = nmos(drain, gate, gnd, gnd;
model=ChargeBasedMOSFET(channel_length_modulation=0.02/V),
width=8μm, length=2μm)
end
compiled_mos = compile(MOSBias())
wider = with_parameters(compiled_mos, "M1.width" => 12μm)
bias = operating_point(wider; temperature=320K)
characterization = mosfet_operating_point(bias, :M1)
charges = terminal_charges(bias, :M1)Characterization includes signed gm, gds, gmb, terminal currents and
charges, inversion charges, gm_over_id, and intrinsic gain. id is channel
current; currents.drain includes the optional body junction. AC terminal
charges are phasors linearized at the DC bias. Junction areas and perimeters
must be supplied explicitly; they are not inferred from W/L.
Install the companion and a rendering backend into an environment that already contains Amber:
using Pkg
Pkg.add(url="https://github.com/thimotedupuch/Amber.jl", subdir="AmberMakie")
Pkg.add("CairoMakie")A transistor bias grid can be explored without constructing a circuit:
using Amber, AmberMakie, CairoMakie
CairoMakie.activate!()
view = mosfetview(ChargeBasedMOSFET(width=8μm, length=2μm,
channel_length_modulation=0.02/V);
vgs=range(0V, 1.5V; length=101), vds=[0.05V, 0.6V, 1.2V])
handle = workbench(view)
selectbias!(handle; vgs=0.9V, vds=1.2V)
savefigure("mosfet.png", handle) # figure plus a TOML metadata sidecar
close(handle)The dashboard links drain current, gm/ID, intrinsic gain, and gate capacitance.
mosfetplot, gmidplot, and capacitanceplot also compose into ordinary Makie
figures. For kind=:pmos, grid values are polarity-normalized VSG/VSD;
underlying operating points retain signed currents.
Circuit-level helpers cover:
| Workflow | API and measurements |
|---|---|
| Inverter DC transfer | inverterview / inverterplot wrap Amber's invertermetrics: transfer curve, differential gain, switching threshold, unity-gain noise margins from a sweep or raw samples |
| Switching versus load and supply | Amber's switchingmetrics, plus switchingview / switchingplot: 50% propagation delays and delivered supply energy integrated over an explicit window |
| Offset and mismatch | mismatchview / mismatchplot: empirical distributions and mean ± standard deviation grouped by temperature and geometry, retaining failures, seeds, and supplied simulation records |
Switching energy includes leakage over the selected window; use a settled full cycle when reporting energy per cycle. See the runnable CMOS studies demo for inverter and seeded transistor-pair simulations. It retains timestep-refinement history and requires less than 1% change in both delays and energy before accepting a switching point. This is a successive-refinement check, not a bound on absolute error. Variation parameters in that demo are illustrative.
Other workbenches provide linked data cursors, noise integration bands, network
matrix selection and Smith readouts, Floquet participation, and Monte Carlo
sample browsing/replay. Eye diagrams and jitter views operate on transient
records. explore runs parameter studies with caching and pinned results;
reportfigure, savefigure, and copyrecipe support reproducible reporting.
Use an interactive Makie backend for mouse-driven exploration and CairoMakie
for headless exports. See the AmberMakie guide for usage.
compiled = compile(circuit)
tuned = with_parameters(
compiled,
"First.R1.value" => 1.2kΩ,
"Second.C1.value" => 82nF,
)
@assert tuned.topology === compiled.topologyNumerical overrides copy only affected parameter batches. The elaboration index, sparse pattern, stamp locations, and unaffected batches are shared. Parameters that change circuit structure—such as a package parasitic that adds an internal element—are rejected and require rebuilding the design. This includes BJT base resistance, switch clock feedthrough, and op-amp input capacitance and bias current, even when changed by replacing the entire model. Capacitor value changes also require rebuilding when dielectric loss or absorption derives additional elements from that value.
Use this mechanism directly, or through a failure-aware sweep:
study = sweep(
compiled,
Symbol("First.R1.value") => range(500Ω, 2kΩ; length=31);
analysis=SmallSignal([1kHz]),
metric=r -> abs(voltage(r, :output)[1]),
)
successful(study)
failure_rate(study)Every point retains its simulation or structured failure.
using Statistics
mc = monte_carlo(
circuit;
analysis=SmallSignal([1kHz]; source=:Source),
samples=10_000,
seed=2026,
parallel=true,
variations=Dict(
Symbol("First.R1.value") => Gaussian(1kΩ, 10Ω),
Symbol("Second.C1.value") => LogNormal(log(100nF), 0.03),
),
metric=r -> abs(voltage(r, :output)[1]),
)
mean(mc)
std(mc)
quantile(mc, 0.99)
confidence_interval(mc)
yield_rate(mc, value -> value > 0.2)
yield_confidence_interval(mc, value -> value > 0.2)
failure_rate(mc)Monte Carlo supports Gaussian, log-normal, and uniform variations; process
variation; covariance-based CorrelatedVariation; automatic numeric
tolerance parameters; and correlated mismatch for BJT MatchedGroups. It
stores per-sample seeds, draws, outputs, and failures, making any sample
replayable:
replayed = replay_sample(mc, circuit, 17; metric=r -> abs(voltage(r, :output)[1]))
@assert replayed == sample_values(mc)[17]Amber observables are explicit values:
voltage(:out) # node voltage
voltage(:outp, :outn) # differential voltage
current(:Q1, :collector) # device/terminal current
power(:Q1) # absorbed device power
charge(:D1) # stored device charge
state(:A1, :dominant_pole) # internal dynamic stateThe same observable can be declared in a circuit, evaluated from a result, used
as a transfer-function endpoint, or passed to a measurement. Hierarchical names
use strings such as "FrontEnd.Q1"; array paths use names such as
"stage[4].R1".
Designs have deterministic, schema-versioned TOML serialization that preserves their hierarchy:
save_circuit("filter.toml", circuit)
restored = load_circuit("filter.toml")
@assert compile(restored).fingerprint == compile(circuit).fingerprint
save_monte_carlo("offset-study.toml", mc)
restored_mc = load_monte_carlo("offset-study.toml")Snapshots and results carry structural, parameter, and compiled fingerprints.
provenance, report, and validity_report expose these together with solver
statistics and warnings.
Serialization intentionally rejects arbitrary Julia functions. Circuits using behavioral source closures should be reconstructed from code instead of treated as portable data.
The core constructors are:
- Passive:
resistor,conductance,capacitor,inductor. - Sources:
voltage_source,current_source,transconductance,voltage_controlled_voltage_source,current_controlled_current_source, andcurrent_controlled_voltage_source. - Semiconductor:
diodewithJunctionDiode,npnwithGummelPoonBJT, andnmos/pmoswithLevel1MOSFETorChargeBasedMOSFET. - Behavioral:
opampwithBehavioralOpAmp,analog_switchwith smooth or event switch models, and nonlinearbehavioral_current_source/behavioral_voltage_sourcewith user-supplied constitutive laws and analytic gradients.
The extended catalog adds these constructors, using existing primitives and analytic behavioral laws:
| Family | Constructors |
|---|---|
| Junctions and light sensors | zener, schottky, led, photodiode, solar_cell |
| JFETs | njfet, pjfet |
| Signal conditioning | analog_multiplier, voltage_limiter, comparator, voltage_controlled_resistor |
| Nonlinear and adjustable resistance | varistor, thermistor, potentiometer |
| Composite networks | ideal_transformer, bridge_rectifier, crystal, transmission_line |
Catalog defaults are illustrative: the comparator is smooth and memoryless (no delay or hysteresis), the thermistor uses a fixed supplied temperature, and the LED models an electrical junction without optical output. The transmission line is a lumped pi-section RLGC approximation whose R/L/G/C parameters are total line values. See the catalog example and constructor docstrings for terminal order and limits.
Use material=, package=, and dielectric= to attach reusable physical models
to ordinary resistor and capacitor constructors. The catalog now includes
24 additional types alongside ThinFilm, SMD0603, C0G, and DebyeBranches:
| Family | Available models | Supported behavior |
|---|---|---|
| Resistor materials | ThinFilm, ThickFilm, MetalFilm, CarbonFilm, CarbonComposition, MetalFoil, Wirewound |
Configurable power-law excess current noise, added to resistor thermal noise |
| Surface-mount packages | SMD0201, SMD0402, SMD0603, SMD0805, SMD1206, SMD1210, SMD2010, SMD2512 |
Explicit resistor or capacitor parasitics; names use imperial size codes |
| Leaded and generic packages | Axial, Radial, PassivePackage |
The same lumped parasitic parameters, for leaded parts or custom configurations |
| Ceramic dielectrics | C0G, X7R, X5R |
Loss calibrated at an explicit reference frequency |
| Film and mica dielectrics | Polypropylene, Polyester, PPS, Mica |
The same calibrated loss model |
| Bulk capacitor technologies | AluminumElectrolytic, Tantalum |
The same calibrated loss model; specify leakage and absorption separately |
| Dielectric absorption | DebyeBranches |
Additional series RC relaxation branches in parallel with the main capacitor |
The additions were prioritized as packages first, resistor materials second, and capacitor dielectrics third, so they reuse the existing simulation kernels. The complete inventory and roadmap includes other device models and the next priorities: resistor temperature and voltage dependence, capacitor bias/temperature/aging curves, magnetic cores, electrothermal packages, and broadband loss models.
Technology names identify families, not manufacturer parts. Noise coefficients, loss tangent, and package parasitics default to zero; supply measured or datasheet values for the intended part. Equal parameters produce equal electrical behavior across family names. The following values are illustrative.
Component constructors reject unsupported keywords, including misplaced model
parameters: use diode(a, b; model=JunctionDiode(ideality=2.)), for example.
Inductor winding_resistance and series_resistance are aliases; specify only one.
See the component keyword audit for
where parameters enter each analysis and which effects require rebuilding.
Every package accepts four finite, nonnegative parameters:
- Resistors use
series_inductancein series with the resistance andparallel_capacitanceacross the external terminals. - Capacitors use
esrandeslin series. Component-levelesrandesloverride their package values, including an explicit zero.
Nonzero fields for the other component kind are rejected when adding the component. A package name does not infer
mounting geometry or thermal properties. Model wirewound inductance explicitly
through the package; Wirewound() alone adds no inductance.
All resistor materials accept excess_noise_coefficient (default zero),
excess_current_exponent (2), excess_frequency_exponent (1), and
excess_reference_frequency (1 Hz). Their excess current-noise PSD is
coefficient * abs(I)^current_exponent * (reference_frequency/f)^frequency_exponent.
The coefficient's units depend on the current exponent. Resistor tc1,
temperature_coefficient, and voltage_coefficient still reject nonzero values.
R2 = resistor(a, b; value=4.7kΩ,
material=ThickFilm(excess_noise_coefficient=1e-12),
package=SMD0402(series_inductance=0.4nH, parallel_capacitance=20fF),
)
C2 = capacitor(a, b; value=1μF,
dielectric=Polypropylene(loss_tangent=0.001, reference_frequency=1kHz),
package=Radial(esr=0.2Ω, esl=5nH),
leakage_resistance=1GΩ,
)A nonzero loss_tangent requires an explicit positive reference_frequency in
Hz. Amber adds a constant series resistance
Rloss = loss_tangent / (2π * reference_frequency * nominal_capacitance) to the
component/package ESR. This supports AC, transient, and thermal-noise analyses
with the same RC model. The calibration describes the nominal capacitor alone;
other parasitics change the terminal loss. If a supplied ESR already includes
dielectric loss, use that ESR alone to avoid counting the loss twice.
Dielectric names do not model DC-bias derating, temperature curves, aging,
polarization, or voltage limits. Leakage and dielectric absorption are separate
supported effects, configured with leakage_resistance and
dielectric_absorption=DebyeBranches(...).
Debye branches add Ci = C * fraction[i] and Ri = time_constant[i] / Ci;
fractions must be finite and nonnegative, and time constants finite and positive.
Zero fractions add no branch. The main capacitance remains the nominal value.
Example physical models include:
R1 = resistor(a, b; value=10kΩ,
material=ThinFilm(
excess_noise_coefficient=1e-18,
),
package=SMD0603(series_inductance=0.6nH, parallel_capacitance=40fF),
)
C1 = capacitor(a, b; value=10nF,
dielectric=C0G(loss_tangent=1e-4, reference_frequency=1kHz),
dielectric_absorption=DebyeBranches(
time_constants=[1μs, 1ms],
fractions=[0.002, 0.001],
),
package=SMD0603(esr=30mΩ, esl=500pH),
)These options elaborate into explicit internal primitives before sparse compilation, so parasitic current and stored state participate in the same equations as the ideal element. Capacitor loss tangent produces a constant series resistance calibrated at the explicit reference frequency; it does not imply constant loss tangent across frequency.
All named material, package, and dielectric models support model_parameters,
with_model_parameter, circuit serialization, and result provenance:
base_package = SMD0805(esr=30mΩ, esl=500pH)
custom_package = with_model_parameter(base_package, :esr, 50mΩ)
model_parameters(custom_package) # base_package is unchangedRebuild a circuit after changing its package, dielectric, or absorption model;
with_parameters rejects replacement of these structural options. Updating a
compiled capacitor's value changes only its main capacitance: the expanded
loss resistor and absorption branches stay fixed. Rebuild to preserve the loss
tangent calibration or absorption fractions when changing nominal capacitance.
Amber uses a compiler pipeline rather than assembling an opaque matrix afresh for each solve:
Julia DSL / CircuitBuilder
│
▼
immutable CircuitDesign + reusable SubcircuitTemplate IR
│ validation, hierarchy elaboration, parameter evaluation
▼
HierarchicalCompiledTopology + ParameterStore
│ │
├─ typed unknown/equation layouts
├─ global sparse CSC pattern
├─ precomputed residual/Jacobian slots
└─ structure-of-arrays device batches
│
▼
reusable SimulationWorkspace
│
▼
DC / transient / AC / noise / PSS
Compilation classifies unknowns as node voltages, branch currents, device states, or partition interfaces. Equations are separately classified as KCL, voltage constraints, dynamic states, or device auxiliaries. Voltage sources and inductors receive branch-current unknowns; dynamic compact models can own state unknowns. This metadata connects a failed numerical row back to a named device or state.
Amber evaluates residuals in the descriptor form
F(x, x', t) = 0
and separately assembles stored charge/flux q(x) and its Jacobian through
storage_jacobian!. Transient integration uses d(q(x))/dt + f(x,t) = 0;
frequency analyses assemble the static and dynamic matrices independently.
The continuous residual API retains its derivatives at nonzero x'.
The same device
kernels therefore serve Newton DC, implicit BDF integration, AC linearization,
descriptor control analysis, PSS variational analysis, and noise propagation.
Hierarchy elaboration computes global solver-net identities without discarding instance paths. Device contracts declare terminal, branch, state, and stamp shapes. The compiler then:
- counts unknowns and equations;
- builds one sorted sparse CSC pattern;
- records the exact
nzvalslot for every device derivative; - batches like devices into typed structure-of-arrays kernels; and
- separates topology from numerical parameters.
SimulationWorkspace owns reusable residual, derivative, scaled-Jacobian,
factorization, and Newton buffers. It caches the linear current and storage
matrices even in mixed nonlinear circuits. Linear circuits reuse numeric
factorizations when matrix parameters and the integration coefficient allow it;
changing independent source amplitudes does not invalidate those matrices.
AC sweeps reuse symbolic analysis across frequencies. residual!, jacobian!, and
residual_jacobian! expose the in-place assembly layer for advanced users and
model verification. Residual-only evaluations leave the Jacobian buffer intact
and do not call behavioral-source gradients. Newton line searches use a separate
candidate residual buffer. Conservative transient steps evaluate charge-based
MOS currents and charges together, requesting only the derivatives they need.
Repeated updates can resolve a parameter selector once:
compiled = compile(circuit)
resistance = parameter_handle(compiled, "R1.value")
tuned = with_parameters(compiled, resistance => 2kΩ)Handles also support instance ranges and remain valid for snapshots sharing the same compiled topology. Sweeps and Monte Carlo reuse resolved selectors internally. Within an update call, each affected batch is copied once; unchanged batch fingerprints are reused.
Before numerical solving, check detects missing ground, floating DC networks,
unsupported terminal contracts, ideal voltage-constraint loops, and conflicting
ideal voltages. Lookups rank nearby names and suggest corrections. During a
failure, solver statistics retain iteration history, rejected continuation or
time steps, convergence strategy, warnings, and the dominant typed residual.
structural_analysis(compiled; mode=:dc) reports equation incidence, unmatched
equations and unknowns, and strongly connected equation blocks in dependency
order. Use mode=:time to include storage dependencies. Regularization diagonals
are excluded. Linear incidence uses actual coefficients; nonlinear incidence
conservatively includes possible device branches. A complete matching does not
prove numerical nonsingularity. This inspection API does not eliminate equations
or perform DAE index reduction.
Immutable designs and compiled snapshots prevent old results from changing when parameters are updated. Structural and parameter fingerprints identify exactly what was solved. Monte Carlo derives a seed per sample before optional threaded execution, so scheduling does not change the experiment. Stable serialization uses bounded, schema-versioned TOML readers rather than Julia object deserialization.
Amber is intentionally transparent about what it does not yet model:
- Semiconductor models are compact engineering models, not foundry-qualified
BSIM libraries. The level-1 MOSFET omits subthreshold behavior, a body diode,
short-channel effects, and substrate networks.
ChargeBasedMOSFETadds continuous inversion and optional junctions but omits short-channel and non-quasi-static effects; its noise model omits junction shot noise. - Temperature dependence is partial; there is no electrothermal or self-heating solution.
- Behavioral op-amp parameters are richer than the currently enforced device equations; verify slew/current-limit behavior needed by a particular study.
- Stochastic transient noise uses a fixed grid. Power-law noise requires finite frequency limits.
- Periodic and phase-noise accuracy depends on PSS convergence, orbit sampling, and sideband truncation. Convergence should be checked as those are refined.
- Multiple equilibria, ideal constraints, stiff dynamics, and extreme scale separation can require realistic parasitics, informed initial conditions, and numerical refinement.
- There is currently no SPICE netlist importer/exporter or foundry model-card parser.
- SI suffixes such as
kΩ,nF, andMHzare readable scale factors, not a dimensional type system. Internally, Amber uses SI-valuedFloat64andComplexF64data.
Treat model validity and numerical convergence as separate questions. Inspect
result.stats, provenance(result), and validity_report(result); refine time
steps, frequency grids, PSS samples, and sideband counts; and compare critical
results against theory or an independent implementation.
The example gallery is executable and covers:
- a practical RC filter with package and material effects;
- diode rectification and startup;
- common-emitter and differential amplifiers;
- a Wien oscillator and a CMOS ring oscillator;
- sample-and-hold switching with exact events;
- a generated 200-section RLGC transmission line;
- a switched buck converter;
- hierarchical active filters;
- correlated Monte Carlo on a precision bridge;
- CMOS transfer and propagation delay;
- conductance-crossbar matrix multiplication;
- a light detector and transformer divider using the extended device catalog;
- difficult nonlinear convergence and regenerative circuits; and
- systems beyond electronics, including thermal, SIR epidemic, Hodgkin-Huxley, Josephson-junction, and acoustic waveguide models built from behavioral constitutive laws.
Run an example from the repository root:
julia --project=. examples/03_common_emitter/noise.jl
julia --project=. examples/07_rlgc_line/step_response.jl
julia --project=. examples/11_precision_bridge/monte_carlo.jlRun the complete test suite with:
using Pkg
Pkg.test()The tests include analytic identities, conservation laws, Jacobian checks,
convergence-order tests, metamorphic tests, external reference values, result
snapshot checks, and the on-disk example gallery. Benchmarks live in
benchmark/.
Amber is licensed under the MIT License.


