Modified Nodal Analysis
Every SPICE simulator works the same way: it turns a circuit into a matrix equation
This chapter shows exactly how MNA works — how each component becomes entries in a matrix, and how solving that matrix gives you every voltage and current in the circuit.
The unknowns
The solution vector
- Node voltages — one for every node in the circuit (except ground, which is defined as 0V)
- Branch currents — one for every voltage source and every inductor
If a circuit has
The "Modified" in MNA refers to the addition of branch currents. Plain Nodal Analysis only solves for node voltages, which makes it unable to handle voltage sources directly. MNA adds extra equations for each voltage source, making the system slightly larger but much more general.
Node voltages
The solution vector in MNA starts with one unknown per circuit node — except ground. Understanding why ground is special, and how nodes get numbered, clarifies the entire matrix structure.
Ground is the reference, not a variable
Voltage is always measured between two points. To get absolute numbers, SPICE defines one node as the reference and fixes it at 0V. This is ground — node 0 in SPICE netlists.
Ground does not appear in the solution vector. It has no row and no column in the matrix. Every other node voltage is implicitly measured with respect to it. When we write
This is why every SPICE netlist must have a node 0. Without a reference, the system of equations is underdetermined — you can add any constant to all voltages and still satisfy KCL. Fixing ground removes that degree of freedom and makes the solution unique.
Node numbering
In a netlist, nodes have names: in, out, vdd, 0. Internally, the simulator assigns each non-ground node a sequential index starting at 1. This index is the row (and column) position in the MNA matrix.
Internal vs external nodes
Some devices create nodes that don't appear in the netlist. A MOSFET model, for example, adds internal nodes for the parasitic drain and source resistances. These nodes are real — they get indices, rows, and columns — but the user never names them.
In spice-rs (following ngspice), external nodes come first in the numbering, internal nodes are appended after. A circuit with 5 external nodes and 3 internal nodes has 8 node voltage unknowns, occupying rows 1 through 8 of the solution vector.
The solution vector
Putting it together, the full solution vector
The first
Conductance stamps
Each component contributes entries to the matrix
The resistor stamp is the atom of circuit simulation. Every other stamp — capacitors, diodes, transistors — is a variation on this pattern. Understand it once, and the rest follows.
Resistor
A resistor
Node
Node
These coefficients go into a 2x2 stamp pattern:
Positive on the diagonal, negative on the off-diagonal. Symmetric. This is the most important stamp in SPICE — it shows up everywhere because even nonlinear devices are linearized into an equivalent conductance at each iteration.
Here is how spice-rs implements it:
// From device/resistor.rs — the load() function
let g = self.conductance;
mna.stamp(self.pos_node, self.pos_node, g);
mna.stamp(self.neg_node, self.neg_node, g);
mna.stamp(self.pos_node, self.neg_node, -g);
mna.stamp(self.neg_node, self.pos_node, -g);
Four calls to stamp(). Each one adds a value to one matrix entry. That's all a resistor does.
Stamping two resistors
Take two resistors:
| Row 1 | ||
| Row 2 |
This is the key insight: each component stamps independently, and the stamps superpose by addition. The order doesn't matter. The components don't know about each other. The matrix assembles itself.
When one node is ground
When a resistor connects to ground (node 0), that node has no row or column. The 2x2 stamp degenerates: the row and column for ground are simply discarded. Only the diagonal entry at the other node survives. This is not a special case in the code — ground is just absent from the matrix, so stamps that reference it naturally contribute only their non-ground entries.
Current source
An independent current source
No matrix entries — a current source doesn't depend on any node voltage, so it contributes no conductance.
Voltage source
A voltage source
The first two rows express KCL: the branch current
This is why voltage sources add a row and column to the matrix — they introduce both a new unknown (the branch current) and a new equation (the voltage constraint).
Why the current comes out negative
When you simulate a simple circuit with a voltage source supplying power, the branch current
Building the full matrix
Let's assemble a complete MNA matrix from scratch. Every stamp, every entry, laid out so you can trace each number back to the component that put it there.
The voltage divider from Chapter 1:
Nodes: in = 1, mid = 2, ground = 0 (excluded). Branch variables:
Stamp R1 (1k between nodes 1 and 2):
Stamp R2 (1k between node 2 and ground):
Stamp V1 (10V source, positive at node 1, negative at ground): Branch current
The complete system:
Each row tells a story:
- Row 1 (KCL at
in):. The current through R1 plus the current from V1 sum to zero. - Row 2 (KCL at
mid):. Current into midfrom R1 equals current out through R2. - Row 3 (V1 constraint):
. The source forces the voltage.
Solving gives
Reactive stamp visualization
Here is a three-resistor network. Adjust the resistor values and voltage source — the MNA matrix entries update reactively, and the schematic re-renders with the simulated node voltages.
The MNA matrix for this circuit is 4x4 (3 nodes + 1 voltage source branch). Each resistor stamps its 2x2 conductance block; the voltage source adds its row and column.
Three stamps, three devices, one matrix. Each entry traces back to exactly one component (or in the case of shared-node diagonal entries, two components whose conductances add).
Solving the system
The matrix is built. Now what? SPICE needs to solve
LU factorization
The workhorse is LU factorization: decompose
- Forward substitution: solve
for (top to bottom, each equation has one new unknown) - Back substitution: solve
for (bottom to top, same idea)
For a dense
Sparsity
A 1000-node circuit has a
The sparse solver stores only the nonzero entries and operates only on those. Instead of
spice-rs uses a Markowitz-ordered LU factorization, faithfully ported from the KLU algorithm in SuiteSparse. Chapter 19 covers the sparse solver in full detail — the pivot ordering strategies, fill-in minimization, and the data structures that make it efficient.
From solution to results
After the solve, the solution vector
Node voltages occupy the first
Branch currents for voltage sources and inductors occupy positions
Other currents (through resistors, into transistor terminals) are computed after the fact from the node voltages. For a resistor:
What happens next
For a linear .OP analysis, the solve is done — one factorization, one forward/back substitution, and the answer is ready. But for nonlinear circuits, this is just one iteration of Newton-Raphson:
- Linearize all devices at the current operating point
- Stamp the linearized values into the matrix
- Solve
- Update the operating point
- Check convergence — if not converged, go to step 1
The matrix is re-stamped and re-factored at every iteration. Factorization dominates the runtime, which is why the sparse solver matters so much — it's the innermost loop of the entire simulation.
The stamp pattern
Every component in SPICE reduces to the same operation: stamp values into the matrix and RHS. This is true for resistors, capacitors, diodes, MOSFETs, transmission lines — everything.
For linear components (resistors, linear capacitors at a given frequency), the stamps are constant. For nonlinear components (diodes, transistors), the stamps change at each Newton-Raphson iteration as the linearization point updates. But the mechanism is identical: call mna.stamp() with a row, column, and value.
This uniformity is the key insight of MNA — it reduces the entire problem of circuit simulation to: fill a matrix, solve it, repeat.
The next chapter shows what happens when the stamps aren't constant — when Newton-Raphson iteration is needed to handle nonlinear devices.