This guide is written for a developer who can read C# but does not yet feel comfortable with circuit math or electronics vocabulary. You do not need an electrical engineering background to follow it. The goal is to explain what SpiceSharp is doing, why it needs a matrix, and how the pieces fit together in software terms.
SpiceSharpParser reads a SPICE netlist, but the numerical circuit solution is done by SpiceSharp. This article explains the handoff and then looks inside the engine: modified nodal analysis, sparse matrix solving, Newton iteration, transient integration, and the matrix contribution of each supported component category.
The short version is:
SPICE text
-> SpiceNetlistParser.ParseNetlist()
-> parse-tree model
-> SpiceSharpReader.Read()
-> SpiceSharp Circuit + Simulations + Exports
-> simulation.Execute(circuit)
-> SpiceSharp builds and solves sparse matrix equations
SpiceSharpParser creates the Circuit, simulation objects such as OP, DC, AC, and
Transient, and export objects. The sparse matrix, Newton loops, timestep loops, and
linear algebra live in the SpiceSharp dependency.
If you are new to electronics, read in this order:
- Vocabulary: learn the words that appear everywhere.
- Main Objects: map those words to SpiceSharp classes.
- Modified Nodal Analysis: understand why a circuit becomes equations.
- Sparse Matrix And Solver Internals: understand how those equations are stored and solved.
- Component Stamp Atlas: use it as a reference, not as something to memorize.
The formulas are there because the simulator ultimately solves numbers. You can still understand the architecture if you treat each formula as a recipe for where a component adds values into a table.
| Term | Plain meaning |
|---|---|
| Circuit | A graph of components connected by wires. |
| Node | A wire connection point. All points on the same ideal wire have the same voltage. |
Ground / 0 |
The reference node. Its voltage is defined as 0 V. |
| Voltage | Electrical "level" at a node, measured relative to ground or another node. |
| Current | Flow through a component branch. |
| Resistance | How strongly a resistor opposes current. Higher resistance means less current. |
| Conductance | The inverse of resistance: g = 1 / R. Higher conductance means more current. |
| Source | A component that injects a known voltage or current. |
| Operating point | The steady DC solution before time-varying or AC effects. |
| Nonlinear | A device whose behavior is not a straight-line relation. Diodes and transistors are nonlinear. |
| Newton iteration | A repeated guessing process for solving nonlinear equations. |
| Jacobian | A matrix of local slopes/derivatives used by Newton iteration. |
| Transient | Time-domain simulation: what happens as time moves forward. |
| AC | Small-signal frequency-domain simulation. |
| Matrix | A table of coefficients used to solve many equations at once. |
| RHS | Right-hand side vector: the known values in the equation system. |
| Stamp | A small component contribution added into the big global matrix/RHS. |
The most important mental model:
circuit components
-> many small local rules
-> one big table of equations
-> solver finds node voltages and branch currents
If you know basic software data structures, think of the circuit as a graph and the solver matrix as an indexed table built from that graph.
| Concept | Role | Software analogy |
|---|---|---|
Circuit |
Collection of SpiceSharp entities created from netlist components and models. | Object graph. |
Simulation |
Analysis runner: operating point, DC, AC, transient, or noise. | Job/command object. |
| Behavior | Analysis-specific implementation attached to an entity. | Strategy object. |
| Simulation state | Shared numerical state: variables, solver, current solution, history. | Runtime context. |
| Solver | Sparse linear system used for modified nodal analysis. | Specialized database/table plus equation solver. |
| Export | Reader for voltages, currents, properties, plots, prints, and measurements. | Query over current runtime state. |
SpiceSharp uses behavior interfaces to separate component data from numerical work. A resistor entity can have one set of parameters, but its biasing behavior knows how to load a DC conductance stamp, its frequency behavior knows how to load a complex AC stamp, and dynamic devices can also have time-domain behavior.
In other words, SpiceSharp is not one giant switch statement over component names.
It is closer to a plugin system: each component provides behaviors, and the simulation
asks for the behaviors it needs.
SpiceSharp solves circuits with modified nodal analysis (MNA). The phrase sounds academic, but the idea is developer-friendly:
turn the circuit into a set of equations
put those equations into a matrix
ask a solver for the unknown values
The unknown values are usually node voltages and a few special branch currents. Once
the solver finds those values, SpiceSharp can answer questions such as V(out),
I(V1), or "what is the current through this resistor?"
MNA is the numerical language that lets resistors, capacitors, sources, semiconductors, behavioral devices, and subcircuits all contribute to one shared system of equations.
The basic idea is:
- Use node voltages as the main unknowns.
- Add extra current unknowns when a device imposes a voltage equation.
- Let every component stamp its local equations into one global matrix.
- Solve the global matrix repeatedly until the circuit state is known.
Y * x = rhs
x = unknown solution vector
Y = admittance/Jacobian matrix
rhs = right-hand-side vector
Read this like a programming problem:
Y = table of coefficients
x = values we want to find
rhs = known values
The solver receives Y and rhs, then computes x.
In pure linear DC circuits, Y can be read as an admittance matrix. In real SpiceSharp
simulations, it is more general:
- in nonlinear DC analysis,
Yis the Newton Jacobian, - in AC analysis,
Yis complex and frequency-dependent, - in transient analysis,
Yincludes companion-model terms from the integration method, - for ideal voltage-defined devices,
Yalso contains constraint equations.
So the safest name is "the MNA matrix" or "the Jacobian matrix", even though many rows look like ordinary conductance/admittance rows.
Plain nodal analysis uses one equation per non-ground node. Each equation is Kirchhoff's current law (KCL). You do not need to memorize the physics to understand the software shape. KCL means:
for every node, the current going in must match the current going out
That works well for resistors and current sources, because their currents can be written directly in terms of node voltages.
For example, a resistor between a and b has:
i(a -> b) = g * (V(a) - V(b))
That current can be expanded into coefficients on V(a) and V(b), so it fits directly
into KCL rows.
Example 1: resistor to ground:
R1 out 0 1k
There is one unknown node voltage: V(out). The resistor value is known, so the
conductance is known:
g = 1 / 1000
The node rule is:
current through R1 = 0
g * V(out) = 0
That is already a plain nodal equation. No extra unknown is needed. The matrix can have one row and one column:
[ g ] [ V(out) ] = [ 0 ]
Example 2: current source plus resistor:
I1 out 0 2m
R1 out 0 1k
The current source value is known: 2mA. The resistor current is still
g * V(out). The node equation can be written directly:
g * V(out) = -2mA
Depending on sign convention the RHS may appear as +2mA or -2mA, but the important
point is the same: the current source contributes a known number. It does not create a
new unknown.
Matrix form:
[ g ] [ V(out) ] = [ -2mA ]
The exact sign depends on the direction assigned to I1. The structural lesson is what
matters here: the current source changes the RHS, not the unknown vector.
Ideal current sources are easy for nodal analysis: they inject a known current, so they go straight into the RHS vector. They do not add a new unknown.
Ideal voltage sources are different. They are the reason MNA is "modified". A voltage source tells the solver:
V(p) - V(n) = value
but it does not tell the solver its current in advance. That is like having a method that returns one property but hides another property you still need. MNA fixes this by adding a new unknown for the current through the voltage-defined branch. The source then contributes:
- KCL terms in the endpoint node rows,
- one extra row that enforces the voltage constraint,
- one extra column for the source branch current.
Example 3: voltage source plus resistor:
V1 in 0 10
R1 in 0 1k
The voltage source tells us:
V(in) = 10
The resistor current is:
I(R1) = g * V(in)
But what is the current through V1? It depends on the rest of the circuit. The source
will provide whatever current is necessary to keep V(in) at 10 V. Plain nodal
analysis has nowhere to put that unknown source current.
MNA adds one extra unknown:
x = [
V(in),
I(V1)
]
Now the equations can say two things:
row V(in): g * V(in) + I(V1) = 0
row I(V1): V(in) = 10
Matrix form:
[ g 1 ] [ V(in) ] = [ 0 ]
[ 1 0 ] [ I(V1) ] [ 10 ]
This is the "modified" part in one picture: the voltage source added a branch-current unknown and a voltage-constraint row.
Example 4: voltage source between two non-ground nodes:
V1 p n 5
The source says:
V(p) - V(n) = 5
Again, its current is unknown. MNA adds I(V1):
x = [
V(p),
V(n),
I(V1)
]
The source contributes this shape:
row V(p): +I(V1)
row V(n): -I(V1)
row I(V1): +V(p) - V(n) = 5
Matrix contribution:
[ 0 0 1 ] [ V(p) ] [ 0 ]
[ 0 0 -1 ] [ V(n) ] = [ 0 ]
[ 1 -1 0 ] [ I(V1) ] [ 5 ]
Other components connected to p and n add their own terms into the first two rows.
The voltage source itself provides the branch-current column and constraint row.
This same pattern appears for independent voltage sources, voltage-controlled voltage sources, current-controlled voltage sources, inductors in many formulations, and some dynamic or behavioral devices.
The solution vector x is an ordered list of values the simulator is trying to find.
Think of it as an array:
double[] x = solver.Solve();The most common values in that array are node voltages. Some components add extra
current values. For a simple circuit with node voltages V(in), V(out), and one
ideal voltage source branch current I(V1), the unknown vector may look like:
x = [
V(in),
V(out),
I(V1)
]
SpiceSharp assigns these variables to solver indices during setup. Components refer to variables through references created by the binding context and simulation state. Once the references are bound, behavior code can load the matrix by index instead of looking up node names on every iteration.
Common variable kinds are:
| Variable kind | Example | Why it exists |
|---|---|---|
| Node voltage | V(out) |
Main MNA unknown for a non-ground node. |
| Voltage-source current | I(V1) |
Required because ideal voltage sources impose voltage, not current. |
| Inductor current | I(L1) |
Often used so the inductor can express v = L * di/dt. |
| Controlled-source current | I(E1), I(H1) |
Required for voltage-output controlled sources. |
| Internal model variable | device-specific | Used by some dynamic, nonlinear, or expanded models. |
The row and column with the same index refer to different roles:
column k = coefficient multiplying unknown x[k]
row k = equation that must be satisfied for variable k
For a node-voltage variable, the row is usually a KCL equation. For a branch-current variable, the row is usually a voltage constraint or device constitutive equation.
Ground, usually node 0, is the reference node. Its voltage is not solved because it is
defined to be 0 V.
SpiceSharp sparse matrix APIs reserve index 0 as a special throw-away location for ignored ground terms. Real solver equations use positive indices. This lets device code stamp a uniform pattern without special-casing every grounded terminal.
For a resistor from out to ground, the full two-node stamp still looks like:
Y[out,out] += g
Y[out,0] -= g
Y[0,out] -= g
Y[0,0] += g
Only the Y[out,out] term participates in the real solve. The ground terms are routed
to the special index-0 location.
This convention is small but important. It lets component behaviors stamp the same shape whether a pin is grounded or not. That keeps device code simple and avoids many conditional branches inside hot solver paths.
Think of each matrix row as one rule the simulator must satisfy.
For node rows, the rule is usually:
sum of currents leaving node = sum of independent current injections
For a resistor from out to ground:
g * V(out) = 0
For a resistor from in to out:
g * (V(in) - V(out)) contributes to row in
g * (V(out) - V(in)) contributes to row out
For a voltage source branch row:
V(p) - V(n) = source voltage
After stamping, all of those local equations are added together into one matrix. If two components touch the same matrix location, their coefficients are summed. This is why the operation is called stamping: each behavior contributes its local stencil to the global matrix.
Each matrix column represents one unknown value from x. A non-zero entry means:
this equation depends on this unknown
If column out has a coefficient in row in, that means the equation for node in
depends on V(out). Resistors, capacitors, controlled sources, and nonlinear device
Jacobians all create these cross-couplings.
Branch-current columns are different but follow the same rule. If a voltage source
current unknown I(V1) appears in the KCL row for node p, then Y[p,b] += 1 says:
the current through V1 participates in node p's KCL equation
The RHS vector stores known values. If Y is "what multiplies the unknowns", rhs is
"what the equations must equal".
Examples:
| Contribution | Typical RHS effect |
|---|---|
| Independent current source | Adds current to endpoint node rows. |
| Independent voltage source | Adds voltage value to the branch constraint row. |
| Linearized diode | Adds equivalent current source from Newton linearization. |
| Capacitor transient companion | Adds history current from previous accepted timesteps. |
| Inductor transient companion | Adds history voltage/current term. |
| Behavioral expression | Adds current, voltage, or residual value after evaluation. |
In nonlinear analysis, the RHS is not just "independent sources". It also contains the constant part of each local linear approximation. That is what lets the linear system represent a nonlinear circuit at the current Newton guess.
SpiceSharp separates structural setup from numeric loading. This is very similar to separating allocation from per-frame/per-iteration work in normal software.
Setup/binding answers questions like:
- Which variables exist?
- Which solver indices do those variables use?
- Which matrix locations does this behavior need?
- Which RHS entries does this behavior need?
- Which simulation states are available?
Loading answers questions like:
- What conductance does this resistor have right now?
- What current does this source inject at this time?
- What small-signal derivatives does this diode have at the current guess?
- What equivalent conductance does this capacitor get for this timestep?
In simplified form:
setup:
create variables
create behaviors
bind behaviors to solver indices
allocate matrix/RHS element references
each solve attempt:
clear numeric values
call Load() on each active behavior
factor and solve
This separation is central to SpiceSharp performance. Topology and matrix locations are mostly stable, while numeric values change constantly. A transient simulation can reuse the same sparse matrix structure across many timesteps, only changing values such as source waveforms, nonlinear derivatives, and integration coefficients.
The software pattern is:
bind once
load many times
For MNA, the most important behavior method is the load step. Biasing behaviors load real-valued DC/Newton contributions. Frequency behaviors load complex-valued AC contributions. Time behaviors prepare dynamic state and companion-model values used by the biasing load during transient analysis.
Conceptually, a behavior does this:
read present parameters and simulation state
compute local coefficients
add coefficients to Y
add known terms to rhs
For a linear resistor, the local coefficient is simply g = 1 / R.
For a nonlinear diode, the local coefficients depend on the previous solution guess:
gd = local derivative di/dv
ieq = residual current term
For a transient capacitor, the local coefficients depend on timestep and history:
geq = integration coefficient
ihistory = contribution from previous accepted states
Consider this small circuit:
V1 in 0 10
R1 in out 1k
R2 out 0 2k
.OP
Unknown vector:
x = [
V(in),
V(out),
I(V1)
]
Let:
g1 = 1 / 1000
g2 = 1 / 2000
The resulting MNA system is:
row V(in): g1*V(in) - g1*V(out) + I(V1) = 0
row V(out): -g1*V(in) + (g1+g2)*V(out) = 0
row I(V1): V(in) = 10
Matrix form:
[ g1 -g1 1 ] [ V(in) ] [ 0 ]
[ -g1 g1+g2 0 ] [ V(out) ] = [ 0 ]
[ 1 0 0 ] [ I(V1) ] [ 10 ]
The voltage source adds the branch-current unknown and the final constraint row. The
resistors add the conductance terms. Solving gives V(in) = 10 V and
V(out) = 6.666... V.
If the matrix still looks intimidating, focus on the rows:
- first row: KCL at node
in, - second row: KCL at node
out, - third row: voltage source says
V(in)must be10.
The matrix is just the compact table form of those three rules.
For a diode connected from out to ground, the real equation is nonlinear:
i = Is * (exp(V(out) / (n*Vt)) - 1)
SpiceSharp does not solve that exponential equation directly in one matrix solve. Instead, each Newton iteration replaces the diode with a local linear approximation:
i ~= gd * V(out) + ieq
Then the diode loads:
Y[out,out] += gd
rhs[out] -= ieq
After solving, the diode checks whether its current and voltage changed enough to count
as converged. If not, it recomputes gd and ieq around the new guess and loads the
matrix again. This repeats until all nonlinear convergence checks pass or the iteration
limit is reached.
Here is a more concrete picture. Imagine this circuit:
V1 in 0 5
R1 in out 1k
D1 out 0 DMOD
.MODEL DMOD D(IS=1e-14)
.OP
The resistor is linear. Its stamp is always based on:
gR = 1 / 1000
The diode is nonlinear. Its current depends exponentially on V(out), so SpiceSharp
cannot stamp one fixed conductance for the whole solve. Instead it uses the current
guess for V(out).
At one Newton iteration, suppose the current guess is:
Vguess(out) = 0.60 V
The diode behavior evaluates its model at that guess:
id = diode current at 0.60 V
gd = diode slope at 0.60 V
ieq = id - gd * 0.60
Plain English:
id = where the diode curve is at this guess
gd = how steep the diode curve is at this guess
ieq = correction term so the straight-line approximation touches the curve
For this one iteration, the diode pretends to be:
diode current ~= gd * V(out) + ieq
So it stamps:
Y[out,out] += gd
rhs[out] -= ieq
Then the solver solves the whole circuit and may return:
Vnew(out) = 0.67 V
Now the diode checks the change:
change = abs(0.67 - 0.60)
If that change is larger than the allowed tolerance, the diode says "not converged". SpiceSharp starts another Newton iteration using the new guess:
Vguess(out) = 0.67 V
The diode recomputes:
id = diode current at 0.67 V
gd = diode slope at 0.67 V
ieq = id - gd * 0.67
Then it reloads the matrix with the new gd and ieq. The resistor stamp stays the
same, but the diode stamp changes because the diode operating point changed.
A simplified iteration log might look like:
| Iteration | Guess before load | Solved value | Change | Converged? |
|---|---|---|---|---|
| 1 | 0.10 V |
0.58 V |
large | no |
| 2 | 0.58 V |
0.66 V |
smaller | no |
| 3 | 0.66 V |
0.674 V |
smaller | no |
| 4 | 0.674 V |
0.675 V |
tiny | yes |
The numbers are illustrative, not exact SpiceSharp output. The important idea is that each iteration replaces the curved diode equation with a straight-line approximation at the current guess. When the next solved value is close enough to the previous guess, the approximation and the real nonlinear device agree well enough to stop.
This is also why bad nonlinear circuits can fail to converge. If the guess jumps around, or if the diode model creates extreme slopes, each new straight-line approximation may point the solver somewhere unhelpful. Then the iteration limit is reached before the changes become small enough.
For a capacitor, the original equation is differential:
i = C * dv/dt
MNA needs algebraic equations, so transient analysis uses an integration method to turn the capacitor into a companion model at each timestep:
i ~= geq * v + ihistory
That looks like a resistor plus a current source for one solve attempt. When the
timestep changes, or when a timestep is rejected and retried, geq and ihistory are
recomputed. When a timestep is accepted, the capacitor's integration history is
accepted too.
This is why transient analysis is a loop around the MNA solve:
choose timestep
build companion models for that timestep
solve nonlinear MNA system
accept or reject timestep
update history if accepted
SpiceSharp uses the same MNA idea for real and complex systems.
| Analysis | Matrix value type | Meaning |
|---|---|---|
.OP |
real | DC operating-point equations. |
.DC |
real | Repeated DC operating-point equations over swept values. |
.TRAN |
real | Time-domain companion-model equations. |
.AC |
complex | Small-signal frequency-domain equations. |
.NOISE |
complex plus noise data | Small-signal transfer and noise propagation. |
In AC, capacitors and inductors do not use timestep history. They stamp frequency-domain
admittances and impedances using s = j * 2 * pi * frequency. The topology is familiar,
but the numbers are complex.
MNA is the equation format, not the entire simulator. A full SpiceSharp simulation also needs:
- model evaluation,
- temperature updates,
- parameter sweeps,
- Newton iteration,
- convergence checks,
- sparse ordering and factorization,
- timestep control,
- truncation-error control,
- export events.
MNA is the common place where all device physics and simulation control eventually meet: every active behavior must express its local effect as matrix and RHS contributions.
A stamp is a component's small contribution to the big global matrix. If the matrix is a spreadsheet, stamping means "add these numbers to these cells."
For example, a resistor says:
add +g to cell (p,p)
add -g to cell (p,n)
add -g to cell (n,p)
add +g to cell (n,n)
Many components may add to the same cell. The final cell value is the sum of all their contributions.
The tables below use these symbols:
| Symbol | Meaning |
|---|---|
p, n |
Positive and negative output terminals. |
cp, cn |
Positive and negative control terminals. |
b |
Extra branch-current equation/unknown for an ideal voltage-defined branch. |
bc |
Branch current of a controlling voltage source. |
Y[r,c] += a |
Add a to the matrix row r, column c. |
rhs[r] += a |
Add a to the right-hand-side vector row r. |
v(p,n) |
V(p) - V(n). |
s |
Laplace variable. In AC, s = j * 2 * pi * frequency. |
The sign convention used here is the common SPICE convention: a current source from
p to n removes current from p and injects current into n:
rhs[p] -= I
rhs[n] += I
Exact sign details can differ in internal code depending on whether a behavior writes an equivalent residual or source form, but these stamps describe the resulting MNA equations.
This section explains the "spreadsheet" that SpiceSharp builds and solves. You do not need to know advanced linear algebra first. The key idea is:
most circuit matrix cells are zero,
so SpiceSharp stores only the cells that matter.
A dense matrix stores every cell:
5 x 5 dense table
[ ? ? ? ? ? ]
[ ? ? ? ? ? ]
[ ? ? ? ? ? ]
[ ? ? ? ? ? ]
[ ? ? ? ? ? ]
A sparse matrix stores only cells that are actually used:
same logical table, but only these cells exist:
(1,1), (1,2), (2,1), (2,2), (3,5), (5,3)
Circuit matrices are naturally sparse because most components touch only a few nodes.
A resistor between a and b touches four cells. It does not care about every other
node in the circuit.
Example: if a circuit has 10,000 unknowns, a dense matrix has:
10,000 * 10,000 = 100,000,000 cells
But each resistor still touches only four cells. Storing all 100 million cells would be wasteful.
Think of the solver matrix as a large dictionary keyed by (row, column):
matrix[(row, column)] = value
In a real sparse solver it is more specialized than a normal dictionary, but the mental model is useful:
if a component needs a cell:
create or find that cell
keep a fast reference to it
add values to it during every load
Rows are equations. Columns are unknowns. A matrix cell answers:
how much does this unknown contribute to this equation?
The netlist uses names:
R1 in out 1k
V1 in 0 10
The solver needs indexes:
"in" -> solver index 1
"out" -> solver index 2
"I(V1)" -> solver index 3
After setup, component behaviors work with indexes and cached references, not with
strings. That is important because Load() can be called many times:
OP solve: maybe many Newton iterations
DC sweep: many sweep points
Transient solve: many timesteps, each with Newton iterations
AC solve: many frequency points
String lookup in every load would be unnecessary overhead.
SparseMatrix<T> is SpiceSharp's sparse matrix type. It is still logically a square
matrix, but it only stores allocated elements.
It keeps elements connected by row and by column:
row view:
row 2 -> cells (2,1), (2,2), (2,5)
column view:
col 2 -> cells (1,2), (2,2), (4,2)
Why both views? Because solving and factoring a matrix need to walk through rows and columns efficiently. A plain list of cells would be too slow for large circuits.
For a junior developer, the important part is:
SparseMatrix<T> stores only useful cells
and lets the solver navigate those cells efficiently.
Index 0 is special. Grounded terms go to a throw-away index-0 area, because ground is
defined as 0 V and does not need a real solver equation.
MatrixLocation is just a coordinate:
MatrixLocation(row, column)
Read it as:
MatrixLocation(which equation, which unknown)
For a resistor between p and n, the behavior needs four matrix locations:
(p,p)
(p,n)
(n,p)
(n,n)
If n is ground, then locations involving n use index 0 and are ignored by the real
solve.
ElementSet<T> is a helper for cached matrix and RHS entries.
Without caching, a behavior would repeatedly do this:
find Y[p,p]
find Y[p,n]
find Y[n,p]
find Y[n,n]
add values
With caching, setup does the lookup once:
setup:
remember references to Y[p,p], Y[p,n], Y[n,p], Y[n,n]
Then loading is cheap:
load:
add +g, -g, -g, +g to the cached cells
Software analogy:
ElementSet<T> = precomputed references to the cells a component will edit
This is the same performance idea as caching a resolved method, compiled expression, or array index instead of rediscovering it inside a hot loop.
A simplified solve pass looks like this:
1. Clear previous numeric values from Y and rhs.
2. Ask every active behavior to Load().
3. Each behavior adds values to cached matrix/RHS cells.
4. Factor the matrix.
5. Solve for x.
6. Copy x back into simulation variables.
7. Let nonlinear devices check convergence.
Example with one resistor and one current source:
I1 out 0 2m
R1 out 0 1k
Unknown:
x = [ V(out) ]
After loading:
Y = [ g ]
rhs = [ -2mA ]
The solver computes:
V(out) = rhs / g
The real solver is more general, but this tiny example shows the same pattern:
load known component contributions,
then solve for unknown circuit values.
The solver does not know what a resistor, diode, capacitor, or MOSFET is. By the time the solver runs, all electronics meaning has been translated into numbers.
The solver receives:
Y = matrix of coefficients
rhs = known values
The solver returns:
x = solved unknown values
That is it.
For example, the solver does not see:
R1 in out 1k
D1 out 0 DMOD
It sees something more like:
Y[1,1] += ...
Y[1,2] += ...
rhs[1] += ...
This separation is important. Component behaviors understand circuit physics. The solver understands equation systems.
components/behaviors:
"What numbers should be added to the matrix?"
solver:
"Given this matrix, what is x?"
Here is a plain math example, not a full circuit. Suppose loading produced:
Y = [ 3 1 ]
[ 1 2 ]
rhs = [ 7 ]
[ 5 ]
The solver is trying to find:
x = [ x1 ]
[ x2 ]
So the equations are:
3*x1 + 1*x2 = 7
1*x1 + 2*x2 = 5
One possible way to solve by hand:
x1 + 2*x2 = 5
x1 = 5 - 2*x2
3*(5 - 2*x2) + x2 = 7
15 - 6*x2 + x2 = 7
15 - 5*x2 = 7
x2 = 1.6
x1 = 1.8
So:
x = [ 1.8 ]
[ 1.6 ]
For a circuit, x1 and x2 might be voltages or branch currents. The solver does not
care. It just solves the equations.
The tiny example above is easy by hand. Real circuits are not tiny:
small example: 2 unknowns
real circuit: thousands or millions of unknowns
A general dense solver would waste time on zero cells. A sparse solver tries to do work only around cells that exist.
That is why the flow is:
behaviors create sparse matrix entries
solver factors only the sparse structure
solver computes the solution vector
simulation reads solution values back
The solver is a performance-critical data structure plus algorithm. It is not just a formula.
ISolverSimulationState<T> is the simulation state that owns the solver and solution
vector.
The generic T is the number type:
| Analysis | Typical T |
|---|---|
.OP, .DC, .TRAN |
double |
.AC, .NOISE |
complex number |
The solver state is where behaviors get access to:
- the sparse solver,
- the current solution vector,
- variable mappings,
- matrix/RHS entries.
Plain English:
solver state = the shared numerical workspace for this simulation
Once the matrix is loaded, SpiceSharp must solve:
Y * x = rhs
The direct way to think about this is:
given the table Y and known values rhs,
find the unknown values x
Internally, sparse solvers commonly factor the matrix into easier pieces. You may see this written as:
Y = L * U
You do not need to implement LU factorization to read SpiceSharp code, but it helps to know what problem it solves.
Suppose the solver receives:
[ 2 1 ] [ x1 ] = [ 5 ]
[ 4 3 ] [ x2 ] [ 11 ]
The equations are:
2*x1 + 1*x2 = 5
4*x1 + 3*x2 = 11
One hand-solving strategy is elimination:
row2 = row2 - 2*row1
before:
row1: 2*x1 + 1*x2 = 5
row2: 4*x1 + 3*x2 = 11
after:
row1: 2*x1 + 1*x2 = 5
row2: 1*x2 = 1
Now the answer is easy:
x2 = 1
2*x1 + 1 = 5
x1 = 2
Factorization is the solver's organized version of this idea. It prepares the matrix so the solution can be found by simpler forward/backward steps.
The useful mental model is:
factorization prepares the matrix
substitution uses that prepared matrix to get the answer
Factorization is usually the expensive part. Solving after factorization is cheaper.
You can think of factorization like preparing an index before running a query:
without preparation:
solving is hard every time
after factorization:
the matrix is reorganized into a form that is easier to solve
For one loaded matrix, the solver typically does:
factor:
analyze/reorder/factor Y
solve:
use the factored form to compute x
The L and U names come from splitting the prepared matrix into two triangular
systems:
L = lower triangular, values mostly on/below the diagonal
U = upper triangular, values mostly on/above the diagonal
Triangular systems are easier to solve because one variable can be found at a time.
Example upper-triangular system:
[ 2 1 ] [ x1 ] = [ 5 ]
[ 0 1 ] [ x2 ] [ 1 ]
The second row gives x2 = 1. Then the first row gives x1 = 2.
In circuit simulation, this is repeated many times because device values change. The matrix structure may be mostly the same, but the numeric values inside the cells change.
Example:
Newton iteration 1:
diode gd = 0.001
factor and solve
Newton iteration 2:
diode gd = 0.004
same cell locations, different values
factor and solve again
The expensive part is not finding the diode's cells again. Those were cached. The expensive part is solving the changed numeric system.
Sparse solvers have to choose a good processing order. Three words appear often:
pivoting, ordering, and fill-in.
Pivoting means choosing stable values to divide by during factorization. A bad pivot is like dividing by a number that is too close to zero: the result becomes unreliable.
bad first pivot:
[ 0.000000001 1 ]
[ 1 2 ]
If elimination starts at the top-left value, it must divide by 0.000000001. That is
dangerous because tiny rounding errors can become huge.
A safer internal order is:
swap row 1 and row 2:
[ 1 2 ]
[ 0.000000001 1 ]
Now the first pivot is 1, which is much safer. The mathematical answer is the same;
the solver just changed the internal route to get there.
For circuit simulation, weak pivots often happen when the circuit is nearly singular or badly scaled. Examples:
- extremely large and extremely small component values mixed together,
- almost-floating nodes,
- ideal source loops,
- very weak conductance paths.
Ordering means rearranging internal rows and columns to make factorization safer or faster. This does not change the circuit answer; it changes how the solver gets there.
Think of ordering like choosing the order to process tasks with dependencies. Some orders create less temporary work than others.
Suppose x1, x2, x3, and x4 are unknowns, and x1 is connected to all others:
star-like matrix pattern:
x1 x2 x3 x4
r1 X X X X
r2 X X . .
r3 X . X .
r4 X . . X
If the solver eliminates x1 first, it can create new connections between x2, x3,
and x4. That creates extra stored cells.
If it eliminates the leaf-like variables first (x2, x3, x4), it may create fewer
extra cells.
The circuit answer is unchanged. Only the internal work changes.
Fill-in means factorization creates extra non-zero cells that were not present in the original matrix.
Start with this sparse pattern, where X means a stored non-zero and . means zero:
before factorization:
x1 x2 x3
r1 X X .
r2 X X X
r3 . X X
During factorization, eliminating x2 can create a new relationship between x1 and
x3:
after fill-in:
x1 x2 x3
r1 X X F
r2 X X X
r3 F X X
F means fill-in: a cell that was zero in the original matrix but becomes needed by the
factorization process.
Fill-in is not wrong. It is temporary mathematical bookkeeping. But it costs memory and time, so sparse solvers try to choose an ordering that keeps fill-in low.
Developer analogy:
ordering = choose a good execution plan
pivoting = avoid unstable divide-by-nearly-zero steps
fill-in = temporary/intermediate data created by the execution plan
Some fill-in is normal. Too much fill-in makes the solve slower and uses more memory.
Circuit matrices can be very sparse:
10,000 unknowns
dense matrix cells: 100,000,000
actual useful cells: maybe tens or hundreds of thousands
If factorization creates too much fill-in, the solver loses some of the sparse advantage. Good ordering protects performance. Good pivoting protects numerical stability.
In one sentence:
ordering tries to keep the work small;
pivoting tries to keep the math stable;
fill-in is the extra work created along the way.
In many simulations, the circuit topology stays the same while numbers change.
Examples:
- a diode changes
gdandieqeach Newton iteration, - a capacitor changes
geqand history current each timestep, - an AC analysis changes frequency-dependent values at each frequency,
- a DC sweep changes source values at each sweep point.
The matrix cells are mostly the same cells. Only their numeric values change.
That is why SpiceSharp separates:
structure:
which cells exist?
values:
what numbers are in those cells right now?
This is one of the main reasons sparse matrix solving can be fast enough for circuit simulation.
A singular matrix means the equations do not determine one unique solution.
Read it as:
the simulator was asked a question that does not have one unique answer
Common causes:
- a floating node with no DC path to ground,
- two ideal voltage sources fighting with different voltages in parallel,
- an inductor loop or voltage source loop with no resistance path,
- a capacitor-only island in operating point,
- a zero-valued resistor, inductor, or source parameter that removes an expected path,
- a model that creates an impossible operating point.
Example floating node:
C1 floating 0 1u
.OP
In operating point, an ideal capacitor is open. The node floating has no DC rule that
sets its voltage, so the solver cannot find one unique answer.
One common fix is to add a large resistor to ground:
Rleak floating 0 1G
That gives the node a weak DC path and gives the matrix a real equation for the node.
SpiceSharp entities are not the matrix solver. An entity represents a component or model object, while behaviors represent what that object does in a particular analysis.
This is one of the most developer-friendly parts of SpiceSharp. You can understand it with normal object-oriented design:
entity = data and identity
behavior = analysis-specific implementation
That distinction is important:
Entity
owns names, pins, parameters, model references
Behavior
owns analysis-specific numerical work
A resistor entity may expose resistance and temperature parameters. Its biasing behavior loads a real conductance stamp. Its frequency behavior loads an AC stamp. A capacitor has time behavior because it needs integration history. A diode has convergence behavior because its nonlinear current must be checked after each Newton iteration.
Common SpiceSharp behavior interfaces:
| Interface | Purpose |
|---|---|
IBehavior |
Base behavior contract. |
ITemperatureBehavior |
Computes temperature-dependent parameters before solving. |
IBiasingBehavior |
Loads real-valued DC/Newton matrix and RHS entries. |
IBiasingUpdateBehavior |
Updates device state after a biasing solve iteration. |
IConvergenceBehavior |
Checks whether a nonlinear device has converged. |
IFrequencyBehavior |
Loads complex small-signal matrix and RHS entries for AC/noise. |
IFrequencyUpdateBehavior |
Updates frequency-domain state after a frequency solve. |
ITimeBehavior |
Initializes or prepares time-domain state for transient analysis. |
ITruncatingBehavior |
Helps estimate timestep truncation error. |
IAcceptBehavior |
Accepts state after a successful timestep or solve point. |
INoiseBehavior |
Contributes noise sources and noise density data. |
The same component can implement several of these. For example, a diode typically needs temperature behavior, biasing behavior, convergence behavior, frequency behavior, and possibly time behavior for capacitances. A simple resistor may only need temperature, biasing, and frequency behavior.
Do not worry if the behavior list feels long. Most analyses only ask for the behavior types they need. A DC operating-point simulation does not care about noise behavior. An AC simulation does care about frequency behavior.
During setup, SpiceSharp builds behavior containers for entities. A behavior container is a named group of behaviors attached to one entity for one simulation context.
Conceptually:
Circuit entity "R1"
-> behavior container "R1"
-> biasing behavior
-> frequency behavior
-> temperature behavior
The simulation asks for behaviors by interface. Operating point analysis cares about
IBiasingBehavior, IConvergenceBehavior, IBiasingUpdateBehavior, and
ITemperatureBehavior. AC analysis also asks for IFrequencyBehavior. Transient
analysis uses biasing plus time, accept, and truncation behavior.
This design is why SpiceSharp can add new analyses and custom devices without forcing every component into one giant base class.
When user code calls:
simulation.Execute(circuit);SpiceSharp runs a setup and execution pipeline. The exact details differ by analysis, but the flow is roughly:
1. Create simulation states
2. Build behavior containers for circuit entities
3. Bind behaviors to variables, states, and solver entries
4. Apply temperature and parameter setup
5. Initialize analysis-specific state
6. Run the analysis loop
7. Fire export events
8. Accept or update final state
Simulation states are shared services for behaviors. They include things like:
- variable dictionaries,
- solver state,
- current solution vector,
- frequency point,
- time point,
- integration method,
- noise accumulation state.
ISolverSimulationState<T> is the state that exposes the solver and solution vector
used by MNA. Biasing analysis uses real numbers. Frequency analysis uses complex
numbers.
In application terms, state is the object graph that says "where are we right now in this simulation?"
The simulation inspects the circuit and asks each entity to create behaviors relevant to the analysis. Components that do not participate in an analysis simply do not provide that behavior type.
For example:
OP:
temperature behaviors
biasing behaviors
convergence/update behaviors
AC:
operating-point/biasing behaviors
frequency behaviors
TRAN:
operating-point/biasing behaviors
time behaviors
accept/truncation behaviors
Binding connects behaviors to the simulation. This is where behaviors find node variables, create branch-current variables, get solver states, and reserve matrix locations.
You can think of binding as dependency injection for simulation internals. The behavior gets references to the services and variables it will need later.
After binding, the behavior should not need to search for "out" or "V1" by text in
the hot path. It already has references to the variables and matrix entries it will
touch.
Execution is the repeated numerical part. The simulation clears the numeric values, loads all relevant behaviors, solves, updates states, checks convergence, and exports data. The exact loop depends on the analysis:
OP: one nonlinear operating-point solve
DC: many operating-point solves over sweep values
AC: one operating point, then one complex solve per frequency
TRAN: many nonlinear solves over accepted/rejected timesteps
NOISE: operating point plus frequency-domain noise propagation
SpiceSharpParser's *WithEvents simulation wrappers and export objects hook into this
lifecycle so .SAVE, .PRINT, .PLOT, .MEAS, and .FOUR can collect data.
For a junior reader, the useful simplification is:
setup builds the machine
execution runs the machine
exports read values from the machine
For .TRAN, the line "many nonlinear solves over accepted/rejected timesteps" means
there are two nested loops:
outer loop:
move forward in time
inner loop:
solve the circuit at the current time guess
The outer loop chooses a candidate next time, for example from 1.0 us to 1.1 us.
The inner loop solves the circuit at 1.1 us. If the solve converges and the timestep
looks accurate enough, that point is accepted. If not, SpiceSharp tries a smaller step,
for example from 1.0 us to 1.05 us.
So .TRAN is not simply:
for t = 0 to stop:
solve once
It is closer to:
while time < stop:
propose next time
try to solve circuit there
if the point is reliable:
keep it
else:
retry with a smaller step
Binding is the point where component pins become solver variables.
In a netlist, pins are strings:
R1 in out 1k
In the solver, strings are too slow and too vague. SpiceSharp turns them into indexed variables:
"in" -> index 1
"out" -> index 2
SpiceSharp has to answer questions like:
What is the solver index for node "out"?
Does this voltage source need a branch-current variable?
Does this model need an internal equation?
Which matrix entries should this behavior cache?
Which simulation states does this behavior need?
The BindingContext is the object that gives a behavior access to the simulation,
states, and variables during binding.
A node pin such as out maps to a voltage variable:
node "out" -> V(out) -> solver index k
All components connected to that same node share the same variable. This is how KCL
forms naturally: every component connected to out adds terms to row out.
This is similar to many objects holding a reference to the same shared object. The node is shared; every connected component contributes to it.
Some devices need an extra current variable because their current cannot be expressed directly from node voltages alone.
Common examples:
- independent voltage source,
- voltage-controlled voltage source,
- current-controlled voltage source,
- inductor,
- some dynamic or behavioral voltage-output devices.
The branch variable becomes both a column and a row:
column: where the unknown branch current appears in KCL
row: the voltage or device equation defining that branch
After a behavior knows its variable indices, it asks the solver for matrix and RHS
locations. Those locations are commonly stored in an ElementSet<T> or equivalent
references.
This is the performance trick to notice. The behavior does not say "find row p, column n" every time. It caches the answer.
This is efficient because:
setup cost happens once
load cost happens thousands or millions of times
During a transient simulation with many timesteps, a resistor does not rediscover
Y[p,p] on every timestep. It holds the element reference and only adds the current
conductance value.
Some models introduce internal variables that are not directly visible as netlist nodes. These may represent internal branches, transformed state equations, or model-specific unknowns.
From the solver's perspective, they are ordinary variables:
internal variable -> solver index -> row/column in MNA system
The difference is that user-facing exports usually target external node voltages, source currents, or named properties rather than these internal implementation details.
Linear circuits need one matrix solve. Nonlinear circuits need iteration.
If you have written an algorithm that keeps improving a guess until it is "close enough", you already understand the basic shape of convergence.
SpiceSharp's nonlinear solve follows the Newton pattern:
guess x0
repeat:
linearize nonlinear devices around current guess
load Jacobian matrix and RHS
solve for a new x
update device states
test convergence
until converged or iteration limit is reached
The important point is that the matrix is not the original nonlinear equation. It is a linear approximation that is valid near the current guess. Every nonlinear device must cooperate by loading a good local Jacobian and by reporting whether it is converged.
Plain English:
nonlinear device says:
"near the current guess, behave like this simpler linear device"
solver says:
"with those simpler devices, here is the next guess"
Linearization means "replace a curve with a straight line near the current guess."
Imagine a nonlinear function:
y = f(x)
At the current guess x0, the device computes two things:
f(x0) = value at the current guess
df/dx at x0 = local slope at the current guess
Then it uses a straight-line approximation:
f(x) ~= f(x0) + slope * (x - x0)
Rearranged:
f(x) ~= slope * x + (f(x0) - slope * x0)
That shape is perfect for MNA:
slope -> goes into the matrix
f(x0) - slope * x0 correction -> goes into the RHS
Plain English:
matrix gets the local slope
RHS gets the correction that makes the straight line touch the curve
This is why you see names like gd and ieq for a diode:
gd = local slope, resistor-like conductance
ieq = correction current source
Before looking at a diode, use a simpler fake nonlinear current:
i = V(out)^2
Suppose the current guess is:
Vguess(out) = 2
The function value is:
i(2) = 2^2 = 4
The slope of V^2 is 2*V, so at V = 2:
slope = 2 * 2 = 4
The straight-line approximation is:
i ~= f(x0) + slope * (V - x0)
i ~= 4 + 4 * (V - 2)
i ~= 4*V - 4
For this Newton iteration, the nonlinear device behaves like:
current ~= 4 * V(out) - 4
That means:
matrix gets 4
RHS gets correction -4, with sign depending on source direction
After the solve, maybe the new value is:
Vnew(out) = 1.6
The approximation was built around 2, but the solver found 1.6. The next iteration
linearizes around 1.6:
i(1.6) = 2.56
slope = 2 * 1.6 = 3.2
i ~= 2.56 + 3.2 * (V - 1.6)
i ~= 3.2*V - 2.56
So the device loads different matrix/RHS numbers on the next iteration.
A diode current is nonlinear. Conceptually:
id = Is * (exp(vd / (n*Vt)) - 1)
The exact formula is less important than the shape: current grows very quickly when the diode voltage increases.
At the current Newton guess:
vdGuess = V(anode) - V(cathode)
the diode behavior computes:
id = diode current at vdGuess
gd = diode current slope at vdGuess
Then it computes the correction:
ieq = id - gd * vdGuess
For this iteration:
diode current ~= gd * vd + ieq
That looks like:
resistor-like part: gd * vd
current-source part: ieq
So the diode can stamp a linear MNA system:
Y[p,p] += gd
Y[p,n] -= gd
Y[n,p] -= gd
Y[n,n] += gd
rhs[p] -= ieq
rhs[n] += ieq
Then the solver finds a new diode voltage. The diode checks whether the new voltage and
current are close enough to the previous iteration. If not, it recomputes gd and
ieq around the new voltage and loads again.
Simplified iteration table:
| Iteration | Diode voltage guess | Loaded gd |
Loaded ieq |
Solver result | Stop? |
|---|---|---|---|---|---|
| 1 | far from final answer | rough slope | rough correction | big voltage change | no |
| 2 | closer | better slope | better correction | smaller change | no |
| 3 | close | accurate local slope | accurate correction | tiny change | yes |
The exact numbers depend on the diode model and circuit. The pattern is the important part:
guess -> local straight line -> solve -> better guess -> repeat
A behavioral current source can also be nonlinear:
B1 out 0 I={V(out)*V(out)}
That expression is the same fake example as above:
i = V(out)^2
If the current guess is V(out) = 2, the behavior can linearize it as:
i ~= 4*V(out) - 4
The 4*V(out) part acts like a conductance in the matrix. The -4 part acts like an
RHS correction. On the next Newton iteration, if the guess changes to 1.6, the loaded
linear approximation changes too.
This is why nonlinear behavioral expressions can make convergence harder. If the expression has sharp jumps, discontinuities, or very steep slopes, the "straight line near the current guess" may be a poor guide for the next solve.
Convergence usually means that relevant voltages and currents changed by less than the configured absolute and relative tolerances. A device can compare:
- previous terminal voltage versus new terminal voltage,
- previous branch current versus new branch current,
- model-specific internal values,
- charge or dynamic state changes.
SpiceSharpParser maps .OPTIONS abstol and .OPTIONS reltol into the biasing
parameters used by SpiceSharp simulations. abstol protects small currents and values;
reltol scales tolerance with signal magnitude.
So convergence does not mean "exact". It means "close enough according to the requested tolerances."
After the solver finds a candidate solution, update behaviors copy the solution into device state. This lets the next iteration use the latest voltage, current, charge, or small-signal derivative values.
Conceptually:
solve matrix
device.Update()
device.IsConvergent()
The update step and convergence test are separate because a model may need to cache the new solution before deciding whether the change was acceptable.
gmin is a small conductance used by biasing algorithms to avoid completely open
nonlinear junctions or singular intermediate systems. It can help Newton iteration find
a path toward the final solution.
Think of gmin as a tiny helper path that can make early guesses less fragile. It is
not a replacement for a valid circuit.
Continuation methods such as source stepping and gmin stepping are common SPICE techniques:
source stepping:
solve an easier circuit with sources scaled down
gradually scale sources up to final values
gmin stepping:
solve with extra conductance support
gradually reduce support toward final gmin
SpiceSharp has biasing support for these ideas, but this parser only maps a subset of LTspice/PSpice solver options. Unsupported behavior-changing options are reported in compatibility mode instead of silently pretending to match LTspice exactly.
Iteration limits prevent infinite loops:
| Parser option | Meaning |
|---|---|
itl1 |
Operating-point maximum iterations. |
itl2 |
DC sweep maximum iterations. |
itl4 |
Transient Newton maximum iterations. |
When a limit is hit, the issue is usually one of:
- poor initial guess,
- discontinuous behavioral expression,
- floating node,
- unrealistic ideal source network,
- very stiff transient dynamics,
- model parameters outside a numerically reasonable range.
Operating point, DC, transient, AC, and noise all depend on a biasing solution in some way. A simplified Newton iteration is:
create or reuse solver state
create device behaviors
apply temperature and parameter updates
for iteration = 1..maxIterations:
clear Y and rhs numeric values
for each biasing behavior:
behavior.Load()
factor Y
solve Y * x = rhs
for each update behavior:
behavior.Update()
if all convergence behaviors report converged:
accept solution
stop
report non-convergence
Linear circuits usually converge in one load/solve pass. Nonlinear circuits use Newton linearization: each nonlinear device is replaced by a local linear approximation around the current guess. The guess is solved, the device updates its local operating point, and the loop repeats until changes are within tolerances.
If you only remember one thing from this section, remember this:
biasing = find the steady-state starting point
Many other analyses need that starting point before they can do their own work.
SpiceSharp supports several analysis types. Each one answers a different question:
| Analysis | Question it answers |
|---|---|
.OP |
What are the steady voltages and currents? |
.DC |
What happens when I sweep a source or parameter? |
.AC |
How does the circuit respond to tiny sine-wave signals at different frequencies? |
.TRAN |
What happens over time? |
.NOISE |
How much noise appears at the output? |
.OP solves the DC steady state. Capacitors are open circuits and inductors are short
circuits in the DC limit, as implemented by their behaviors. Nonlinear devices iterate
until their voltages and currents converge.
setup biasing state
run Newton loop
export final operating-point variables
.DC repeats the operating-point solve while changing one or more source or parameter
values.
for each sweep point:
set swept value
use previous solution as initial guess when possible
run Newton loop
export sweep data
Using the previous point as a guess is important. Adjacent sweep points are usually close, so convergence is easier than starting from zero each time.
Developer analogy: .DC is a loop around .OP with one input changed each iteration.
.AC first finds an operating point. Then nonlinear devices are linearized around that
point. The engine solves a complex-valued frequency-domain matrix at each frequency:
solve operating point
linearize devices at that bias point
for each frequency:
s = j * 2 * pi * frequency
clear complex Y and rhs
for each frequency behavior:
behavior.Load()
solve complex Y * x = rhs
export complex voltages/currents
Independent AC source values populate the RHS. Capacitors stamp s*C; inductors stamp
s*L in their branch equations or equivalent form.
AC analysis is not "simulate a waveform over time". It is more like asking:
if I poke the circuit with a very small signal at this frequency,
how strongly does the output respond?
.TRAN solves a sequence of time points. Dynamic elements are converted into companion
models by the active integration method. SpiceSharpParser can select trapezoidal, Gear,
or fixed Euler through .OPTIONS method=....
The goal of .TRAN is to answer:
what are the node voltages and branch currents as time moves forward?
A transient simulation is harder than .OP because some components remember the past.
A capacitor remembers previous voltage. An inductor remembers previous current. A
transmission line remembers delayed waves. A pulse source changes value at specific
times.
optionally solve operating point
initialize integration history
while time < stopTime:
choose timestep
for Newton iteration at this timestep:
clear Y and rhs
load biasing and time behaviors
factor and solve
update nonlinear and integration states
check convergence
if timestep accepted:
accept integration history
export transient point
else:
reduce timestep and retry
Time-domain capacitors, inductors, transmission lines, Laplace sources, and waveform sources all depend on current time and integration history.
Developer analogy: .TRAN is a game loop or animation loop, except each frame requires
solving a circuit.
More precise developer analogy:
game loop:
choose next frame time
update world
render frame
transient loop:
choose next circuit time
solve circuit
accept/export point
The difference is that .TRAN may reject a frame. If the numerical result is not good
enough, SpiceSharp does not keep it. It shrinks the timestep and tries again.
.NOISE uses the operating point and small-signal frequency-domain behavior. Noise
behaviors contribute source densities, and the engine propagates them through the
linearized circuit.
Transient analysis turns differential equations into algebraic MNA equations at each time point. The integration method is the rule that performs that conversion.
If the word "differential" is uncomfortable, read it as "depends on how a value changes over time." Capacitors and inductors remember history, so SpiceSharp needs a method for turning that history into numbers for the current timestep.
Dynamic components do not just stamp fixed values. They depend on time history:
| Device | Dynamic quantity |
|---|---|
| Capacitor | voltage history and charge/current relation. |
| Inductor | current history and flux/voltage relation. |
| Semiconductor junction | nonlinear charge and capacitance history. |
| Transmission line | delayed wave history. |
| Laplace source | transfer-function state and optional delay history. |
Integration methods are the rules SpiceSharp uses to move a circuit forward in time.
In .OP, the simulator asks:
what is the steady value?
In .TRAN, the simulator asks:
what is the value at the next time point?
That is harder because some components depend on change over time.
Capacitor:
current depends on how voltage changes over time
i = C * dv/dt
Inductor:
voltage depends on how current changes over time
v = L * di/dt
The solver cannot directly put dv/dt or di/dt into the normal MNA matrix. It needs
ordinary algebra for the current timestep. An integration method converts "change over
time" into a temporary algebraic stamp.
Plain English:
integration method =
recipe for replacing time-derivative behavior
with matrix/RHS numbers for this timestep
For a capacitor, the method creates:
temporary conductance + history current
For an inductor, the method creates:
temporary resistance-like branch term + history term
That is why integration methods are central to .TRAN: without them, SpiceSharp could
not turn capacitors, inductors, semiconductor charges, and other memory-based devices
into equations the solver can handle at each time point.
Simple analogy:
You know where an object was before.
You know the timestep size.
An integration method estimates where it should be now.
Different methods make different tradeoffs:
| Method style | Basic tradeoff |
|---|---|
| Euler-like | Simple, but can need small steps. |
| Trapezoidal-like | More accurate for many smooth circuits. |
| Gear-like | More damping, often better for stiff circuits. |
.TRAN has an outer time loop and an inner solve loop.
Outer loop:
pick the next candidate time
Inner loop:
solve the circuit at that candidate time
Expanded:
time = 0
while time < stopTime:
candidateTime = chooseNextTime(time)
prepare dynamic devices for candidateTime
for iteration = 1..maxNewtonIterations:
clear matrix and rhs
load all component stamps for candidateTime
solve Y * x = rhs
update nonlinear devices
if nonlinear devices converged:
break
if Newton did not converge:
reject candidateTime
reduce timestep
retry
if estimated time-integration error is too large:
reject candidateTime
reduce timestep
retry
accept candidateTime
commit device history
export values
time = candidateTime
This explains the phrase "many nonlinear solves over accepted/rejected timesteps":
many timesteps
each timestep may need many Newton iterations
each timestep may be accepted or rejected
Consider a resistor charging a capacitor:
V1 in 0 PULSE(0 5 0 1n 1n 1m 2m)
R1 in out 1k
C1 out 0 1u
.TRAN 1u 5m
At each candidate time, the capacitor is replaced by a temporary companion model:
capacitor at this timestep ~= conductance geq + history current ihistory
At time 0, the capacitor may start near 0 V. When the pulse source jumps toward
5 V, the capacitor voltage cannot jump instantly. SpiceSharp advances time in steps:
t = 0 us V(out) = 0.00 V
t = 1 us V(out) = small increase
t = 2 us V(out) = larger increase
...
The exact timestep is not always the print step from .TRAN. The print step says when
you want output. The internal solver may use smaller or different timesteps to keep the
solution accurate.
Near the sharp PULSE edge, SpiceSharp may need smaller steps:
try step to 1.0 us:
edge is sharp, estimated error too high
reject
try step to 0.5 us:
still too high
reject
try step to 0.25 us:
acceptable
accept and commit capacitor history
Those numbers are illustrative. The key idea is that rejection is not a crash. It is the simulator protecting accuracy and convergence.
SpiceSharp exposes integration methods through IIntegrationMethod. Common methods are:
| Method | Character |
|---|---|
Trapezoidal |
Accurate for many circuits, but can show numerical ringing in stiff circuits. |
Gear |
More damping, often better for stiff circuits. |
FixedEuler |
Simple fixed-step method, useful for predictable stepping but less accurate. |
FixedTrapezoidal |
Fixed-step trapezoidal variant. |
SpiceSharpParser maps:
.OPTIONS method=trap
.OPTIONS method=trapezoidal
.OPTIONS method=gear
.OPTIONS method=euler
to the corresponding SpiceSharp time-parameter factory.
The integration method rewrites derivatives into algebraic companion models. For a capacitor:
i = C * dv/dt
becomes, for one timestep:
i ~= geq * v + ihistory
For an inductor:
v = L * di/dt
becomes:
v ~= req * i + vhistory
The exact coefficients depend on the method and timestep. The important concept is that the dynamic device becomes a matrix stamp plus RHS history source for the current solve.
This is the main trick of transient simulation:
hard time-dependent component
-> temporary simpler component for this timestep
Transient analysis distinguishes a candidate timestep from an accepted timestep.
This is like optimistic execution. SpiceSharp tries a step, checks whether it is good enough, and either commits it or rolls it back and tries a smaller step.
try timestep:
predict/prepare history
solve nonlinear MNA system
estimate truncation error
if acceptable:
accept solution
commit integration history
export point
else:
reject solution
reduce timestep
retry
IAcceptBehavior lets devices commit state after a successful point. ITruncatingBehavior
lets devices participate in timestep control by reporting how aggressively the timestep
should be limited.
Accepted means:
the solution converged
the estimated integration error is acceptable
device histories are committed
exports may be produced
the simulation time moves forward
Rejected means:
do not commit device histories
do not trust this candidate point
make the timestep smaller
try again from the last accepted time
This distinction matters for capacitors and inductors. Their history must only be updated when a timestep is accepted. If SpiceSharp committed history from a rejected point, the next solve would be based on a state that the simulator already decided was not reliable.
Small timeline example:
accepted time: 10.0 us
try 12.0 us:
Newton converges, but truncation error too high
reject 12.0 us
try 11.0 us:
Newton converges, error acceptable
accept 11.0 us
next start point is now 11.0 us
There may also be Newton rejection:
try 12.0 us:
Newton iteration does not converge
reject 12.0 us
retry with smaller step
That is why transient simulation can slow down near switching events, sharp waveform edges, or strongly nonlinear behavior. The simulator is doing extra inner solves to find a trustworthy next point.
Waveform sources and piecewise expressions can introduce discontinuities. The transient solver should take points at important breakpoints so it does not step over sharp source changes.
Examples:
- PULSE edge start/end times,
- PWL point times,
- delayed-source transition times,
- switching thresholds,
- transmission-line delayed events.
When a circuit has sharp discontinuities, the matrix can change abruptly. That often causes smaller timesteps and more Newton iterations near the event.
AC analysis is not a large-signal sinusoidal transient simulation. It is a small-signal linear analysis around a DC operating point.
"Small-signal" means the circuit is assumed to move only a tiny amount around the DC operating point. That lets nonlinear devices be replaced by linear approximations.
The flow is:
1. Solve DC operating point.
2. Freeze nonlinear devices at that bias point.
3. Convert nonlinear devices to small-signal linear equivalents.
4. For each frequency, solve a complex MNA system.
A diode in AC is not loaded as an exponential current equation. It is loaded as its small-signal conductance and capacitance at the operating point. A transistor becomes a network of conductances, transconductances, capacitances, and controlled sources.
In AC, the matrix values are complex:
s = j * 2 * pi * f
Typical dynamic stamps become:
| Device | Frequency-domain relation |
|---|---|
| Capacitor | Y = s*C |
| Inductor | Z = s*L, usually branch-equation form |
| Transmission line | frequency-dependent two-port relation |
| Laplace source | H(s) evaluated at current s |
The solve at each frequency is independent after the operating point is known:
for each frequency:
set complex frequency state
load complex matrix
solve
export magnitude/phase/real/imaginary data
Only AC source values drive the small-signal RHS. A source with DC value but no AC value biases the circuit but does not inject a small-signal AC excitation.
This is why a source line often looks like:
V1 IN 0 DC 5 AC 1
The DC 5 part affects the operating point. The AC 1 part drives the frequency
solve.
SPICE distinguishes component instances from models. In a netlist:
D1 OUT 0 DMOD
.MODEL DMOD D(IS=1e-14 N=1)
D1 is the device instance. DMOD is the model. The instance says where the diode is
connected; the model says which parameter set and equations are used.
Software analogy:
instance = object placed in the circuit
model = shared configuration/type data
SpiceSharp follows the same idea:
component instance
-> points to model name
-> owns instance parameters
model entity
-> owns model parameters
-> provides model behavior/parameter data
Many models are temperature dependent. Before the main solve, temperature behavior can derive effective parameters from nominal values, circuit temperature, and instance temperature.
You do not need to know the physics to understand the timing: temperature-adjusted values must be computed before the matrix is loaded, because the stamp uses those adjusted values.
Examples:
- diode saturation current changes with temperature,
- resistor model temperature coefficients alter resistance,
- BJT and MOSFET model quantities are adjusted for temperature,
- capacitance and charge equations may depend on thermal voltage.
This is why .TEMP, .OPTIONS temp=..., and .OPTIONS tnom=... are handled before
the numerical solve. Matrix stamps should use the effective temperature-adjusted
parameters, not just the raw netlist values.
A model is not usually a stamp by itself. It supplies equations and parameters used by the instance behavior. The instance behavior then loads the matrix.
For simple models, this is almost direct:
resistor model -> adjusted resistance -> conductance stamp
For semiconductor models, it is richer:
model parameters + terminal voltages + temperature
-> currents, charges, derivatives
-> Jacobian matrix terms + RHS residual terms
That is why the component atlas describes semiconductor stamps conceptually. The exact terms depend on model equations and operating region.
SpiceSharp stores parameters in parameter sets. A parameter set is a structured object with named properties such as resistance, capacitance, model coefficients, simulation tolerances, and integration settings.
For a developer, this is the bridge between text and strongly typed objects:
netlist text -> parsed value -> SpiceSharp parameter set
Important concepts:
| Concept | Role |
|---|---|
ParameterSet |
Default reflection-backed parameter container. |
IParameterized<T> |
Object that exposes a strongly typed parameter set. |
IImportParameterSet<T> |
Supports setting parameters by name. |
IExportPropertySet<T> |
Supports reading/exporting properties by name. |
ParameterSetCollection |
Groups multiple parameter sets. |
SpiceSharpParser uses this system when it translates netlist values into SpiceSharp objects. For example:
R1 in out 1k
-> Resistor entity
-> resistance parameter set to 1000
.OPTIONS reltol=1e-4
-> simulation biasing parameter set updated
SpiceSharpParser must evaluate SPICE expressions before it can set many SpiceSharp parameters. Some values are simple constants:
R1 in out 1k
Others depend on .PARAM, .FUNC, stochastic functions, .STEP, temperature, or
simulation context:
.PARAM rload=1k
R1 out 0 {rload}
The parser builds evaluation contexts and simulation preparation actions so parameters that depend on sweeps or simulation-specific values can be updated at the correct time.
The important bit: not every parameter is known once at parse time. Some values must be re-evaluated for each sweep, Monte Carlo run, temperature, or simulation.
Parameter names are not always the same as SPICE syntax tokens. The parser maps SPICE
syntax onto SpiceSharp parameter names. For example, a resistor's netlist value becomes
the entity's resistance parameter. .OPTIONS abstol becomes a biasing tolerance.
Exports use the reverse idea: user-facing names such as V(out) or I(V1) are mapped
to simulation variables or entity properties that can be read during export events.
SpiceSharp simulations produce data while they run. SpiceSharpParser exposes that data through exports and event handlers.
If the solver is the engine, exports are the dashboard. They read values from the current simulation state.
Typical usage:
var export = spiceModel.Exports.Find(e => e.Name == "V(OUT)");
sim.EventExportData += (sender, args) =>
{
Console.WriteLine(export.Extract());
};
sim.Execute(spiceModel.Circuit);The export is not just a stored number. It is an extractor that reads the current simulation state when the simulation fires an export event.
That is why you attach to EventExportData: the value is meaningful at specific points
in the simulation loop.
Different analyses export at different times:
| Analysis | Export timing |
|---|---|
.OP |
Once after the operating point is solved. |
.DC |
Once per sweep point. |
.AC |
Once per frequency point. |
.TRAN |
Once per accepted output/time point. |
.NOISE |
Once per frequency/noise point. |
The event model lets user code collect data without forcing the simulator to allocate one large result table for every possible use case.
SpiceSharpParser creates export objects for statements such as .SAVE, .PRINT,
.PLOT, .MEAS, and .FOUR.
Common export types:
- node voltage, such as
V(out), - differential voltage, such as
V(out,in), - source or branch current, such as
I(V1), - real/imaginary/magnitude/phase AC values,
- device or model properties,
- noise quantities,
- expression-based exports.
Measurements and plots are layered on top of simulation events. They observe exported values as the simulation runs and compute derived data after enough points are available.
SpiceSharp is designed as a library, not only as a fixed executable simulator. Custom components and models can participate in the same behavior and MNA system as built-in devices.
This is useful even if you never write a custom component. It explains why the built-in components are structured the way they are.
A custom component usually needs:
- An entity class with name, pins, and parameter sets.
- One or more behavior classes.
- Binding code that maps pins to variables and allocates matrix locations.
- Load code that stamps the matrix/RHS.
- Optional update, convergence, temperature, time, frequency, noise, accept, or truncation behavior.
Conceptually, a custom linear two-terminal conductance needs:
entity:
name
positive node
negative node
conductance parameter
binding:
get p and n voltage variables
cache Y[p,p], Y[p,n], Y[n,p], Y[n,n]
load:
add +g, -g, -g, +g
A nonlinear custom device adds:
update:
read solved voltages/currents
convergence:
compare previous and current values
load:
compute local Jacobian and RHS residual
A dynamic custom device adds:
time behavior:
register integration states
compute companion-model coefficients
accept behavior:
commit accepted history
truncating behavior:
limit timestep when local error is too high
For custom devices, the most important performance rule is the same as for built-in devices:
allocate structure during setup
change numbers during load
Do not perform expensive node-name lookups, matrix location discovery, or expression
parsing inside every Load() call if the result can be cached during binding or setup.
SpiceSharpParser maps these .OPTIONS values into SpiceSharp simulation settings:
| Option | Effect |
|---|---|
abstol |
Absolute tolerance for biasing convergence. |
reltol |
Relative tolerance for biasing convergence and selected integration methods. |
gmin |
Minimum conductance used by biasing algorithms. |
itl1 |
DC operating-point maximum iterations. |
itl2 |
DC sweep maximum iterations. |
itl4 |
Transient maximum iterations. |
method=trap or method=trapezoidal |
Use trapezoidal integration. |
method=gear |
Use Gear integration. |
method=euler |
Use fixed Euler integration. |
LTspice options such as pivrel, pivtol, gminsteps, and srcsteps are recognized
as behavior-changing solver options in compatibility mode, but this parser does not map
them yet.
This section shows the matrix contribution of each supported component category. For linear ideal components, the stamp is exact. For model-dependent devices, the section describes the local Jacobian and dynamic contribution rather than pretending that every model variant has the same closed-form matrix.
Do not try to memorize this section. Use it like a dictionary:
- Find the component type.
- Read the plain meaning.
- Look at which matrix/RHS entries it touches.
- Notice whether the stamp is exact or model-dependent.
When a stamp says Y[p,n] += -g, read it as:
in the equation for node p,
the voltage at node n contributes with coefficient -g
A resistor between p and n with resistance R has conductance:
g = 1 / R
Stamp:
| Location | Add |
|---|---|
Y[p,p] |
+g |
Y[p,n] |
-g |
Y[n,p] |
-g |
Y[n,n] |
+g |
This expresses:
I(p -> n) = g * (V(p) - V(n))
If either node is ground, its index is 0 and the corresponding ground entries are ignored by the real equation system.
Beginner meaning: a resistor connects two nodes and lets current flow based on the voltage difference. The four matrix entries are just the two-node KCL bookkeeping.
A DC current source from p to n contributes only to the RHS:
| Location | Add |
|---|---|
rhs[p] |
-I |
rhs[n] |
+I |
In AC, the AC magnitude and phase are used for the complex RHS. In transient analysis,
waveforms such as PULSE, SIN, PWL, SFFM, and AM compute a time-dependent
current and stamp that current into the RHS at each time point.
Beginner meaning: a current source pushes a known current into the circuit. Because the
current is already known, it goes on the known side (rhs) rather than creating a new
unknown.
An ideal voltage source cannot be represented as a simple conductance. MNA adds an
extra branch-current unknown b = I(Vsrc).
Unknowns:
V(p), V(n), I(Vsrc)
Stamp:
| Location | Add |
|---|---|
Y[p,b] |
+1 |
Y[n,b] |
-1 |
Y[b,p] |
+1 |
Y[b,n] |
-1 |
rhs[b] |
+Vsrc |
The node rows inject the branch current into KCL. The branch row enforces:
V(p) - V(n) = Vsrc
In transient analysis, waveform voltage sources update Vsrc at each time point. In AC,
the source uses its complex AC value.
Beginner meaning: a voltage source forces a voltage difference. The simulator must add one extra unknown because the source current is whatever it needs to be to enforce that voltage.
A capacitor has current:
i = C * d(v(p,n))/dt
Operating point:
open circuit, except initial-condition handling
AC:
y = s * C
The AC stamp is resistor-like with g replaced by complex admittance y:
| Location | Add |
|---|---|
Y[p,p] |
+s*C |
Y[p,n] |
-s*C |
Y[n,p] |
-s*C |
Y[n,n] |
+s*C |
Transient:
The integration method turns the capacitor into a companion conductance plus a history current source:
i(t) ~= geq * v(t) + ihistory
Stamp:
| Location | Add |
|---|---|
Y[p,p] |
+geq |
Y[p,n] |
-geq |
Y[n,p] |
-geq |
Y[n,n] |
+geq |
rhs[p] |
history-current contribution |
rhs[n] |
opposite history-current contribution |
geq and ihistory depend on the integration method and previous accepted capacitor
voltage. For backward Euler, conceptually:
geq = C / dt
ihistory = -geq * v(previous)
Trapezoidal and Gear use different coefficients and more history.
Beginner meaning: a capacitor remembers voltage history. In .OP, it mostly behaves
like an open circuit. In .AC, it becomes frequency-dependent. In .TRAN, the
integration method turns it into a temporary resistor-like stamp plus a history source.
An inductor has:
v = L * di/dt
MNA usually gives the inductor a branch-current unknown b = I(L).
DC operating point:
ideal short in steady state, represented through branch equations
AC:
V(p) - V(n) = s * L * I(L)
Stamp:
| Location | Add |
|---|---|
Y[p,b] |
+1 |
Y[n,b] |
-1 |
Y[b,p] |
+1 |
Y[b,n] |
-1 |
Y[b,b] |
-s*L |
The branch row expresses:
V(p) - V(n) - s*L*I(L) = 0
Transient:
The integration method builds an inductor companion relation:
v(t) ~= req * i(t) + vhistory
The branch equation stamps a branch-current column into node KCL, node-voltage terms
into the branch row, an equivalent resistance-like coefficient on Y[b,b], and a
history term into rhs[b]. Exact coefficients depend on the integration method.
Beginner meaning: an inductor remembers current history. Because its natural unknown is current, MNA often gives it a branch-current variable.
Mutual inductance couples two inductor branch currents:
M = k * sqrt(L1 * L2)
For inductor branch currents b1 and b2, the frequency-domain branch equations include
off-diagonal coupling:
V1 = s*L1*I1 + s*M*I2
V2 = s*M*I1 + s*L2*I2
Conceptual AC additions:
| Location | Add |
|---|---|
Y[b1,b2] |
-s*M |
Y[b2,b1] |
-s*M |
The self terms Y[b1,b1] and Y[b2,b2] come from the individual inductors. In
transient analysis, the same coupling is handled through integration-history terms and
equivalent coefficients.
Beginner meaning: mutual inductance says two inductors influence each other. That is why the matrix gets off-diagonal entries connecting one inductor's branch current to the other's equation.
A VCCS outputs current from p to n controlled by v(cp,cn):
i = gm * (V(cp) - V(cn))
Stamp:
| Location | Add |
|---|---|
Y[p,cp] |
+gm |
Y[p,cn] |
-gm |
Y[n,cp] |
-gm |
Y[n,cn] |
+gm |
No extra branch unknown is required because the output is a current source.
Beginner meaning: a VCCS is a current source whose value is controlled by a voltage somewhere else in the circuit.
A VCVS enforces:
V(p) - V(n) = gain * (V(cp) - V(cn))
It requires a branch-current unknown b.
Stamp:
| Location | Add |
|---|---|
Y[p,b] |
+1 |
Y[n,b] |
-1 |
Y[b,p] |
+1 |
Y[b,n] |
-1 |
Y[b,cp] |
-gain |
Y[b,cn] |
+gain |
The branch row expresses:
V(p) - V(n) - gain * (V(cp) - V(cn)) = 0
Beginner meaning: a VCVS is a voltage source whose voltage is controlled by another voltage. Because its output is a voltage source, it needs a branch-current unknown.
A CCCS outputs current from p to n controlled by another branch current I(bc):
i = gain * I(bc)
Stamp:
| Location | Add |
|---|---|
Y[p,bc] |
+gain |
Y[n,bc] |
-gain |
The controlling current is normally the current through a voltage source or another MNA branch-current variable.
Beginner meaning: a CCCS is a current source controlled by a measured current somewhere else.
A CCVS enforces:
V(p) - V(n) = rtrans * I(bc)
It requires its own branch-current unknown b.
Stamp:
| Location | Add |
|---|---|
Y[p,b] |
+1 |
Y[n,b] |
-1 |
Y[b,p] |
+1 |
Y[b,n] |
-1 |
Y[b,bc] |
-rtrans |
The branch row expresses:
V(p) - V(n) - rtrans * I(bc) = 0
Beginner meaning: a CCVS is a voltage source controlled by a current somewhere else. Because its output is voltage-defined, it needs its own branch-current unknown.
Behavioral sources can be voltage or current outputs with expressions. The expression can depend on voltages, currents, parameters, time, frequency, and functions supported by the parser and SpiceSharpBehavioral.
For a behavioral current source:
i = f(x)
At a Newton iteration, the engine can linearize the expression around the current solution:
i(x) ~= f(x0) + J * (x - x0)
where J contains partial derivatives with respect to referenced variables. The
Jacobian terms are stamped into Y; the residual/equivalent source term is stamped
into rhs.
For a behavioral voltage source, MNA also needs a branch-current unknown because the output is voltage-defined. The branch row enforces the expression result as a voltage constraint.
For expressions that are purely constant at a point, the stamp may reduce to an
ordinary independent source. For expressions with TIME, dynamic functions, table
lookups, or LAPLACE, the load can change at each timestep or frequency point.
Beginner meaning: behavioral sources are programmable sources. The expression is code that computes a voltage or current from circuit variables.
A diode is nonlinear:
i = Is * (exp(vd / (n*Vt)) - 1)
Newton iteration replaces it with a local conductance and equivalent current source:
gd = di/dv at current guess
ieq = i(vguess) - gd * vguess
i ~= gd * vd + ieq
Matrix stamp for gd is resistor-like:
| Location | Add |
|---|---|
Y[p,p] |
+gd |
Y[p,n] |
-gd |
Y[n,p] |
-gd |
Y[n,n] |
+gd |
RHS stamp for the equivalent diode current from p to n:
| Location | Add |
|---|---|
rhs[p] |
-ieq |
rhs[n] |
+ieq |
Junction capacitance and charge storage contribute additional dynamic stamps in AC and transient analyses.
Beginner meaning: a diode is not linear. SpiceSharp repeatedly replaces it with a local
"resistor plus current source" approximation until the answer stops changing too much.
The local resistor value is gd; the local current-source correction is ieq. On each
Newton iteration, both are recomputed from the latest diode voltage guess.
A BJT model is nonlinear and model-dependent. It is not one fixed stamp like a resistor. At each operating point guess, the model computes local small-signal quantities such as:
- base-emitter conductance,
- base-collector conductance,
- controlled collector current terms,
- output conductance,
- junction capacitances,
- charge-storage terms,
- equivalent RHS residual currents.
Those quantities form a local Jacobian over the transistor pins:
collector, base, emitter, optional substrate/internal nodes
The biasing stamp adds conductances and controlled-source terms to Y, plus equivalent
currents to rhs. AC uses the linearized small-signal model around the operating point.
Transient adds capacitance and charge companion terms through the integration method.
Beginner meaning: a BJT is a nonlinear multi-terminal device. Do not look for one simple stamp; the model computes many local derivatives and equivalent sources.
A JFET is also nonlinear and model-dependent. Its stamp is built from local derivatives of drain current and gate junction currents. Conceptually, each Newton iteration loads:
- channel conductance terms between drain and source,
- transconductance controlled by gate-source voltage,
- gate junction conductance and equivalent current,
- capacitance terms in AC/transient,
- residual current terms on the RHS.
The exact coefficients depend on the selected JFET model and current operating region.
Beginner meaning: a JFET is also model-dependent. Its stamp changes with the operating point.
MOSFETs have the richest model-dependent stamps. Depending on the model and operating region, the local Jacobian can include:
- drain-source output conductance,
- gate transconductance,
- body-effect transconductance,
- bulk diode conductances and equivalent currents,
- terminal capacitances,
- charge conservation terms,
- optional internal node terms.
The biasing matrix receives partial derivatives of terminal currents with respect to terminal voltages. The RHS receives residual currents so the linearized system matches the nonlinear equations at the current guess. AC uses the small-signal linearization. Transient uses charge/capacitance companion models.
Beginner meaning: a MOSFET is the most complex common device here. The matrix entries come from the model's derivatives, not from one universal four-entry stamp.
Voltage-controlled switches (S) and current-controlled switches (W) behave like a
conductance that changes based on a control value.
Conceptually:
g = 1 / Ron when on
g = 1 / Roff when off
Then the switch stamps like a resistor:
| Location | Add |
|---|---|
Y[p,p] |
+g |
Y[p,n] |
-g |
Y[n,p] |
-g |
Y[n,n] |
+g |
Real switch models usually smooth or limit transitions to help convergence. Abrupt state changes can make Newton iteration harder because the matrix changes sharply as the control crosses the threshold.
Beginner meaning: a switch is roughly a resistor that changes between Ron and Roff,
but the transition can make convergence harder.
A transmission line is not just a static resistor-like stamp. It relates present terminal behavior to delayed wave history:
delay = length / propagationVelocity
In transient analysis, the line uses history buffers and characteristic impedance to inject delayed wave contributions. It behaves like a dynamic two-port whose RHS and effective terminal relations depend on previous accepted time points.
In frequency-domain analysis, the line can be represented by frequency-dependent two-port relationships. The exact stamp is behavior-specific and depends on line parameters, delay, and characteristic impedance.
Beginner meaning: a transmission line has delay. It cannot be explained as one static resistor/capacitor/inductor stamp.
Laplace transfer sources implement:
output(s) = H(s) * input(s)
where H(s) is a rational transfer function:
H(s) = numerator(s) / denominator(s)
In AC, this is naturally frequency-domain: evaluate H(j*w) and stamp the equivalent
controlled-source relationship.
In transient analysis, the transfer function needs state realization or equivalent dynamic behavior. The source contributes extra internal state equations or companion terms so the time-domain output follows the transfer function. Delay options add history-buffer behavior.
The exact matrix shape depends on voltage-controlled versus current-controlled input and voltage versus current output:
| Kind | MNA shape |
|---|---|
| Voltage output | Needs a branch-current unknown, like a voltage source. |
| Current output | Stamps current into output node KCL rows. |
| Voltage input | Reads V(cp,cn) as the control variable. |
| Current input | Reads another branch-current variable as the control variable. |
Beginner meaning: a Laplace source is a dynamic filter block inside the circuit. In AC, it is evaluated as a frequency response. In transient analysis, it needs internal state.
A subcircuit does not have its own physical matrix stamp. It is a hierarchy and naming mechanism.
When subcircuits are expanded, internal devices receive generated names and internal nodes are mapped into the parent circuit namespace. Then each internal resistor, source, transistor, capacitor, and other component stamps the global matrix normally.
The important idea is:
X instance stamp = sum of stamps from expanded internal components
Beginner meaning: a subcircuit is like a function call or component macro. It does not solve separately; its inside parts are mapped into the parent circuit.
| Symptom | Typical meaning |
|---|---|
| Singular matrix | The equations do not define a unique solution. Check floating nodes and ideal source loops. |
| Invalid or weak pivot | The matrix is nearly singular or badly scaled. Check extreme component values. |
| DC non-convergence | Newton iteration did not settle. Check nonlinear models, initial conditions, and discontinuous behavioral expressions. |
| Timestep too small | Transient solver repeatedly reduced the step and still could not converge or meet truncation error. |
| Huge or NaN result | Look for zero-valued elements, unsupported model parameters, or unstable behavioral expressions. |
Useful debugging steps:
- Run
.OPbefore.TRAN. - Add DC paths for floating nodes, often large resistors to ground.
- Avoid ideal voltage-source loops and current-source cutsets.
- Start with simpler models, then add nonlinear or dynamic devices.
- Use
.NODESET,.IC,gmin, and tolerances carefully.