Language .save / .tran / .solver

.save / .tran / .solver

Directives appear at the top level, outside any module, and start with a .. They configure the simulation as a whole — how long it runs, what gets recorded, and how the equations are integrated.

.tran — transient analysis

start Start time (currently always 0)
stop End time
step The largest step the run may take

One step-size number with one meaning:

  • fixed-step solvers — every step is exactly step.
  • variable-step solversstep is the ceiling; the controller may go below, never above.

You do not choose the opening step. On a variable-step solver the run should not start at its ceiling, so the runtime applies SPICE's first-timestep rule — min(step, stop/50) / 10 — and grows from there. If that opening step will not converge the solver halves it and retries, as does the DC solve, which retries with progressively more damping before reporting failure. A step slightly too coarse for the first microsecond of a circuit therefore costs a few retries, not a failed run. Fixed-step solvers are left exactly as written, because there step is every step the run will take and rescaling it would change the deck's sample rate.

There is no lower bound on dt, by design. Enforcing one was implemented and reverted: it broke both BJT amplifier examples at t = 0 even when set as low as 1 ns, because a bipolar cold start genuinely needs picosecond steps for a few steps before it settles — and any bound loose enough to allow that is too loose to bound anything. Runaway back-off is limited internally instead, relative to where the back-off began, which needs no number from the deck.

Raising step is a useful first response to a circuit that will not converge. The companion conductance of every capacitor scales as C/dt, so very small steps make the matrix progressively harder to solve rather than easier.

Migrating from the four-argument form

.tran(start, stop, tstep, max_step) was accepted until this release, where tstep was the initial step rather than a minimum. Drop the third argument and keep the fourth:

A four-argument .tran is a hard compile error carrying this same instruction, never a silent reinterpretation. The old tstep is not worth preserving: on a variable-step solver it only set where the run started, and within roughly 35 steps every adaptive solver had grown to max_step anyway. A step of 0 is likewise rejected outright — it would produce a run that can never advance time.

.save — signal logging

.save(signal1, signal2, ...) lists what ends up as columns in simulation_output.csv. Signals are referenced by their fully-qualified mangled name, using dot notation to reach into module instances.

Each argument resolves in one of three ways:

  • If the name matches an MNA node, it's read from the circuit solution vector.
  • If it matches a named circuit element that owns a branch row, its current is logged — see below.
  • Otherwise it's read from the logic values map (a VM signal).

Logging branch currents

A .save() argument may name a circuit element, in which case the current through it is logged rather than a voltage. Two spellings mean exactly the same thing:

Either way the CSV column is headed I(main.vsense), so a current column is never confusable with a voltage column. Element names and net names are guaranteed disjoint by the compiler, so the bare form is unambiguous.

Only elements that own an MNA branch row can be logged this way — voltage_source, L, VCVS and CCVS. Naming a resistor is a compile error that points you at the ammeter idiom rather than silently logging zeros.

Column order: node voltages and logic signals come first, in the order written; every branch-current column is appended after them.

.zcd — zero-crossing detection

Enables zero-crossing detection for circuits whose logic outputs switch discretely — comparators, PWM generators, digital controllers. When active, the solver probes one step ahead at each timestep before committing. If any logic variable jumps by more than 1.0 between the current state and the probe, a discontinuity is declared and the crossing time is isolated by bisection down to 1 ps. The timestep is then shortened to land exactly on the event boundary, which stops the integrator stepping across a discontinuity and introducing phase error or false transients.

  • Bisection tolerance: 10⁻¹² s (1 picosecond).
  • Performance: the check is skipped entirely when .zcd is absent — analog-only circuits pay nothing.
  • When to use: any circuit where a logic block produces a step output — a PWM comparator, a relay model, a digital PI controller whose output saturates.

.op — DC operating point

Instructs the VM to solve for the DC operating point before the transient begins.

Both boot paths now run the same DC Newton solve. Writing .op and omitting it currently differ only in a log line. Previously the no-.op path was a single LU solve at G + (1/dt)C with no Jacobian — and because a junction in Hover stamps nothing into G (a diode is an R to an internal node plus a current_source, and current sources stamp only the right-hand side), that solve treated every junction as an open circuit and handed the transient the answer to a structurally different netlist. Junction decks effectively required .op; purely because of that shortcut. It is gone, so a diode or transistor deck no longer misbehaves for want of the directive.

The solve itself uses source stepping: every voltage source is scaled through a twelve-rung α ladder from 5 % to 100 %, applied at stamp time so that driven sources ramp consistently on every evaluation path. Each rung runs up to 500 Newton iterations against a fresh numerical Jacobian, damped by a blind ±0.05 per-component cap rather than the transient's trust region — a from-zero DC solve starts with every node at 0 V, where a diagonally-scaled radius would happily propose a 105 V step. A deck with no voltage sources at all has nothing to ramp and solves the full-strength problem directly.

If the ladder fails to converge, the solve is retried up to four times with more damping, halving the dt that sets α each time; vm->time_step is restored afterwards either way, so a rung's damping value never leaks into the first transient step. After the last retry the run continues from an unconverged bias with a warning on stderr rather than aborting.

One caveat when reading the log: OP complete is printed unconditionally, including when the inner loop ran out without converging. It means the operating point was attempted, not that it succeeded — check the warning line and the first few rows of output instead.

When to use: still write it on any circuit with a well-defined DC bias point — amplifiers, filters, motor drives — both as documentation of intent and because the two paths are not guaranteed to stay identical. Omit it only when the startup transient itself is what you're simulating.

.solver — solver type

.solver() takes 1 required argument plus 4 optional ones, all positional:

Name Default Meaning Emitted as
solverName — (required) Bare identifier: euler_fixed, euler_adaptive, gauss_siedel, trapezoidal, trapezoidal_fixed, bdf2, ndf2. Unknown name = hard compile error. struct choice
reltol 1e-3 Relative tolerance, every row strategy.rtol
vntol 1e-6 V Absolute tolerance for node voltages strategy.atol
max_iter 100 Newton iterations before the step is rejected strategy.max_iter (cast to int)
abstol 1e-12 A Absolute tolerance for branch currents strategy.abstol

Every argument after solverName is optional, and passing 0 for any of them means “use this solver's default” rather than “use zero”. An argument the chosen solver does not use is ignored, never an error — the same latitude SPICE's .OPTIONS takes.

Which solvers honour the voltage/current split. abstol applies to bdf2, ndf2, trapezoidal and euler_adaptive, which share one convergence test (runtime/solvers/newton_core.hpp) that splits the solution vector by row: the first n_nodes rows are node voltages and take vntol, the rest are branch currents and take abstol. On gauss_siedel and trapezoidal_fixed, argument 5 is ignored and argument 2 still applies to every row. On euler_fixed, which runs no Newton loop, no tolerance argument applies at all.

Argument 2 was called abstol before this release and applied to every row. One absolute tolerance could not be right for both halves of the vector — 1e-6 is a sensible microvolt on a node and a very loose microamp on a branch — so it kept its position, was renamed vntol, and a fifth argument took over currents at SPICE's 1e-12 A. Existing decks are unaffected in their voltage behaviour; branch currents are now held tighter, which can change iteration counts.

Implicit solvers treat the circuit as a differential-algebraic system and solve G·x = b at each timepoint, enforcing Kirchhoff's laws and voltage-source constraints simultaneously. They're unconditionally stable for linear circuits; nonlinear devices need an inner Newton-Raphson loop.

Every implicit solver here uses Modified Newton: the Jacobian is reused across multiple inner iterations rather than recomputed every time. Reuse policy differs — some recompute once per timestep unconditionally, others carry a Jacobian across many timesteps and refresh it only once a convergence failure proves it stale.

Damping also differs by solver, but all use diagonally-scaled trust-region damping: each variable's proposed step is measured against its own current magnitude rather than one global voltage budget, so the same solver behaves correctly on a millivolt junction and a kilovolt bus inside one circuit.

Available solvers

Solver Description Simulink SPICE
euler_fixed Backward Euler, fixed timestep, first-order, no tunable parameters. A single implicit solve per step with no error control. Use as a debugging baseline, or when you need a known-simple, always-converges reference trace. ode1
euler_adaptive Backward Euler with Modified Newton-Raphson, trust-region damping, and convergence-driven adaptive timestepping. Expands dt by 1.5× when the inner loop converges quickly, contracts by 0.5× on failure. General-purpose first-order solver. ode15s (1st order)
gauss_siedel Backward Euler with fixed-point under-relaxation (ω = 0.05). No Jacobian is formed at all, making it structurally immune to every Jacobian-related failure mode the others share. Converges slowly but predictably — useful as a diagnostic cross-check, since a difference in outcome isolates the problem to the Newton machinery rather than the circuit. Not recommended for general use.
trapezoidal Variable-step Crank-Nicolson with Modified Newton-Raphson and trust-region damping. A-stable. Falls back to Backward Euler for one step after a ZCD event or a rejected step, to suppress numerical ringing before resuming 2nd-order accuracy. ode23t Trap
trapezoidal_fixed The fixed-step counterpart to trapezoidal: same 2nd-order formula and Newton loop, no adaptive step control at all. A foundation-testing tool — it isolates whether a convergence failure comes from the core Newton/Jacobian mechanics or from the adaptive layer on top. ode23t Trap
bdf2 2nd-order Backward Differentiation Formula (Gear's method) with Modified Newton-Raphson, trust-region damping and adaptive timestepping. L-stable — actively damps spurious oscillations after switching events. Uses a Backward Euler primer on startup and after any step rejection. ode15s Gear
ndf2 Shampine & Reichelt's order-2 Numerical Differentiation Formula — the real algorithm underlying ode15s. Adds a κ-weighted correction to plain BDF2, improving accuracy within the same A-stable order-2 regime. (Per the Dahlquist second barrier, no linear multistep method of order 3+ can be A-stable, so it deliberately stops at 2.) Starts at order 1 and promotes once enough step history exists. ode15s Gear (NDF)