This tutorial explains what the project does, why the circuit equations look the way they do, and where to start reading the code. You need basic C# skills, but you do not need prior simulator experience.
The shortest useful description is:
SpiceSharpParser.CustomComponentsis an optional translation and simulation layer for useful SPICE forms that the standard SpiceSharp components do not directly represent.
It does not replace the parser, the circuit, or the simulator.
A simulation starts as text and ends as numbers:
SPICE text
|
| parse words, numbers, expressions, and line positions
v
parsed netlist model
|
| read each parsed statement and select a generator
v
SpiceSharp Circuit + Simulations
|
| create analysis-specific behaviors
v
solver equations
|
| solve DC, transient, or AC problem
v
voltages, currents, measurements, and plots
This package participates in the middle two steps:
- It installs custom-aware reader generators for
A,C,D, andLnetlist lines. - It either creates a custom SpiceSharp component or expands a portable subcircuit.
There are three implementation routes. Learning this table removes most of the apparent complexity:
| Netlist form | What the package creates | Where its science lives |
|---|---|---|
C ... Q=<expression> or L ... Flux=<expression> |
A native custom component | C# Biasing, Time, and Frequency behaviors |
Ideal-diode .MODEL D(...) |
A native custom diode and model | IdealDiodeEquation plus C# behaviors |
| LTspice A-device or public digital/analog helper | Ordinary entities from a .lib subcircuit |
The embedded SPICE macromodel |
An ordinary line such as C1 out 0 1u still takes the standard SpiceSharp
route. Enabling custom components does not turn every capacitor, diode, or
inductor into a custom one.
The opt-in point is UseCustomComponents():
using System;
using SpiceSharpParser;
using SpiceSharpParser.CustomComponents;
string netlist = string.Join(
Environment.NewLine,
"Nonlinear capacitor tutorial",
"V1 in 0 1",
"R1 in out 1k",
"C1 out 0 Q={1u*x + 100n*x*x*x}",
".tran 10u 5m",
".end");
var parser = new SpiceNetlistParser();
var parseResult = parser.ParseNetlist(netlist);
var reader = new SpiceSharpReader();
reader.Settings.UseCustomComponents();
var model = reader.Read(parseResult.FinalModel);
foreach (var simulation in model.Simulations)
{
simulation.Execute(model.Circuit);
}Keep the two jobs separate in your head:
SpiceNetlistParserunderstands the text.SpiceSharpReaderturns the parsed description into executable objects.
UseCustomComponents() changes reader mappings, so it must be called before
Read.
A node is a named electrical connection. Voltage is always a difference:
V(a, b) = voltage at a - voltage at b
Node 0 is the global reference, usually called ground. V(out) is shorthand
for V(out, 0).
The order of a two-terminal component matters because it defines the positive voltage and current directions. For:
C1 positive negative Q=1u*x
the package uses:
V = V(positive) - V(negative)
Kirchhoff's current law, or KCL, says that currents at a node balance. A simulator builds one large system of equations from all component contributions. Adding a contribution to that system is called stamping.
A resistor with conductance G = 1/R obeys:
I = G * (Vpositive - Vnegative)
That relation contributes four signed values to the node matrix. The custom components eventually do the same thing, even when their equations start as charge, flux, or a smoothed diode curve.
Linear equations have constant slopes. Nonlinear equations do not. The solver therefore works around a current guess and replaces a nonlinear curve with its local tangent:
f(x) is approximated near x0 by:
f(x0) + f'(x0) * (x - x0)
This is the core idea behind Newton iteration. In the code, the derivative usually becomes a matrix coefficient and the remaining correction becomes a right-hand-side value.
The derivative is not an optimization detail. It is the local electrical value seen by the solver.
| Analysis | Question | Custom behavior |
|---|---|---|
| DC operating point | Where does the circuit settle with no time change? | Biasing |
| Transient | How does the circuit change over time? | Time plus Biasing |
| AC small signal | How does a tiny sine-wave disturbance behave around the DC point? | Frequency plus Biasing |
The same entity can therefore create different behavior objects for different analyses.
A normal capacitor is often introduced as:
Q = C * V
The physical law used in time is:
I = dQ/dt
For a constant capacitor, differentiating Q = C*V gives the familiar
I = C*dV/dt. A nonlinear capacitor generalizes this by allowing any supported
charge curve Q(V).
Consider:
C1 out 0 Q={1u*x + 100n*x*x*x}
For a capacitor, x means its terminal voltage. The expression is:
Q(V) = 1e-6*V + 100e-9*V^3 coulombs
The local or incremental capacitance is:
Cinc(V) = dQ/dV
= 1e-6 + 300e-9*V^2 farads
At V = 0, the local capacitance is 1 uF. At V = 2 V, it is
2.2 uF. The component gets "larger" as the magnitude of voltage rises.
The code follows the same steps:
NonlinearPassiveGeneratornoticesQ=.- It creates
NonlinearCapacitorinstead of the standard capacitor. SingleVariableExpressionparses bothQ(x)and its symbolic derivative.NonlinearCapacitors.Biasingevaluates charge anddQ/dV.NonlinearCapacitors.Timeasks the integration method to turndQ/dtinto a matrix companion model.NonlinearCapacitors.Frequencyusess*Cincat the operating point.
At DC, an ideal capacitor is an open circuit. Its biasing behavior still evaluates the derivative so exports and later AC setup know the operating-point capacitance.
For an inductor, the stored quantity is flux linkage:
Phi = L * I
V = dPhi/dt
A nonlinear inductor accepts Flux=<expression>, and x now means branch
current:
L1 in out Flux={2m*tanh(x)}
Its incremental inductance is:
Linc(I) = dPhi/dI
The code mirrors the capacitor implementation, with one important topology
difference: an inductor needs a branch-current variable. Node voltages alone
cannot directly express the voltage-source-like branch equation
V = dPhi/dt.
Read these files side by side:
NonlinearCapacitors/Biasing.csNonlinearInductors/Biasing.csNonlinearCapacitors/Time.csNonlinearInductors/Time.cs
The symmetry is intentional. It exposes what is common and highlights why the inductor has an extra solver unknown.
An ideal verbal diode is simple:
- forward-biased: conduct strongly;
- reverse-biased: block current;
- optionally clamp reverse voltage or current.
A perfectly abrupt switch is difficult for Newton iteration because its slope changes discontinuously. The custom diode supports resistances, thresholds, smoothing, reverse behavior, and current limits that form a numerically usable piecewise curve.
The ideal-diode area is deliberately layered:
netlist parameters
|
v
IdealDiodeParserSupport
|
v
IdealDiodeParameterResolver
|
v
effective IdealDiodeParameters
|
v
IdealDiodeEquation.Evaluate(voltage)
|
+--> current
`--> conductance = dI/dV
|
v
solver behaviors
This separation is valuable:
- the equation can be understood without parser types;
- the parser can handle model selection without matrix knowledge;
BiasingandFrequencyadapt the same effective equation to their analyses.
When debugging a numerical diode result, first inspect
IdealDiodeEquation. When debugging why a .MODEL value was not selected,
start in IdealDiodeGenerator and IdealDiodeParameterResolver.
The digital and analog helpers use embedded .lib files. A subcircuit is a
named circuit template with ordered external nodes and optional parameters.
For example:
var digital = DigitalSubcircuitLibrary.LoadBuiltIn();
digital.AddBinaryGate(
circuit,
DigitalGateKind.Nand2,
instanceName: "XU1",
firstInputNode: "a",
secondInputNode: "b",
outputNode: "y",
positiveSupplyNode: "vdd",
negativeSupplyNode: "0");The C# facade contributes:
- discoverable method and parameter names;
- argument validation;
- a stable API;
- the ordered node list expected by the subcircuit.
The .lib file contributes the functional electrical model. After expansion,
SpiceSharp sees ordinary resistors, capacitors, behavioral sources, and other
normal entities. There is no special "NAND solver".
These are functional macromodels. They aim to reproduce useful input/output behavior, not every transistor inside a physical chip.
Every supported native A-device has eight terminal positions, a model name, and parameters:
A<name> T1 T2 T3 T4 T5 T6 T7 T8 <model> [name=value ...]
The refactored code makes the translation stages explicit.
LTspiceADeviceInstanceReader verifies the eight terminals, model, flags, and
parameter assignments. Its output, LTspiceADeviceInstance, contains raw
parameter expressions and source locations for good diagnostics.
No circuit entities are added at this stage.
LTspiceADeviceCatalog is the central translation table. One definition keeps
together:
- portable subcircuit name;
- native-terminal-to-subcircuit-node order;
- output terminals;
- native-to-portable parameter names;
- whether digital power rails must be generated;
- zero-delay and compatibility policies;
- small model-specific parameter adjustments.
For a counter, the important part reads conceptually as:
native model: COUNTER
portable model: DIG_COUNTER
portable nodes: T1, T2, T7, T6, generated-high, generated-low
native parameters: cycles -> CYCLES
duty -> DUTY
rout -> ROUT
The definition is the first file to edit or review when terminal wiring looks wrong.
LTspiceADeviceValidator checks facts such as:
COUNTER Cyclesis an integer of at least two;Dutyis between zero and one;- resistances are positive;
- OTA source and sink current limits have the correct signs;
- required modulator frequencies exist.
Validation happens before circuit expansion so bad input does not leave a half-created macromodel.
LTspice uses terminal 8 as common. Repeating that same node in another terminal position means "unused".
There is an important trap: if terminal 8 is not global ground, literal node
0 is a real connection to global ground. It is not unused.
LTspiceADeviceConnections records which terminals were unused. Unused inputs
may remain at common. Unused outputs are moved to private nodes so a portable
output stage does not accidentally drive common.
LTspiceADeviceExpander:
- evaluates and maps parameters;
- creates private digital high/low rails when required;
- applies small compatibility defaults;
- chooses a regular or zero-delay subcircuit;
- builds the ordered node list from the definition;
- adds the subcircuit's entities.
LTspiceADeviceGenerator is now only the coordinator for these stages. It also
takes a circuit snapshot and removes newly added entities if expansion throws.
To the reader, a multi-entity expansion is therefore one operation.
Do not start by reading every file alphabetically. Use this path:
-
Parser/CustomComponentReaderExtensions.csSee exactly which four netlist letters are intercepted. -
Parser/NonlinearPassiveGenerator.csSee a small custom-or-fallback decision. -
NonlinearCapacitor.csandNonlinearCapacitors/Biasing.csLearn entity versus behavior. -
NonlinearCapacitors/Time.csConnectQ(V)toI=dQ/dtand the matrix stamp. -
IdealDiodes/IdealDiodeEquation.csSee a numerical law isolated from parser and topology concerns. -
Parser/LTspiceADeviceGenerator.csRead the five-stage coordinator. -
Parser/LTspiceADeviceDefinition.csInspect all A-device wiring and parameter maps in one place. -
Parser/LTspiceADeviceExpander.csSee how a definition becomes circuit entities. -
Digital/Netlists/standard-digital.liborAnalog/Netlists/standard-analog.libRead one public facade method and then find its matching.SUBCKT.
When a custom netlist does not behave as expected, ask these questions in order.
- Was
UseCustomComponents()called beforeRead? - Is the form actually custom (
Q=,Flux=, ideal-diode parameters, orA)? - Did an ordinary component correctly take the standard fallback?
Inspect model.ValidationResult. A parser/reader error is usually more useful
than a later simulation exception because it retains source line information.
Inspect model.Circuit:
- nonlinear passive: expect
NonlinearCapacitororNonlinearInductor; - ideal diode: expect
IdealDiode; - A-device: expect several entities with a generated
X...prefix, not one A-device entity.
- Wrong charge/flux curve: inspect the expression and symbolic derivative.
- Wrong diode curve: inspect
IdealDiodeEquation. - Wrong diode parameter precedence: inspect
IdealDiodeParameterResolver. - Wrong A-device wiring: inspect
LTspiceADeviceCatalog. - Wrong A-device electrical behavior: inspect the matching
.SUBCKT.
A component may be correct in DC and wrong in transient or AC. Go directly to the behavior for that analysis. Check units and signs before changing solver tolerances.
- Add or identify the portable
.SUBCKT. - Add its definition to
LTspiceADeviceCatalog. - Add model-specific ranges to
LTspiceADeviceValidator. - Add a parameter adjustment only if a declarative map is insufficient.
- Add reader tests for valid syntax, invalid parameters, terminal order, and unused outputs.
- Add transient or AC behavior tests for the macromodel.
Avoid putting another large model switch back into the generator. Wiring data belongs in the catalog; generic mechanics belong in the expander.
Use the nonlinear passives as the template:
- Entity class and parameter set.
- Solver-variable layout.
- Biasing behavior.
- Time and/or frequency behavior.
- Reader generator with an explicit standard fallback where applicable.
- Equation-focused tests plus an end-to-end netlist test.
Keep numerical equations out of the parser generator. A generator decides what to create; a behavior decides what equation to stamp.
"CustomComponents is a second parser." No. It plugs additional generators into the existing reader after parsing.
"Q= is a voltage-dependent capacitance expression."
No. It is total charge. Capacitance is its derivative dQ/dV.
"Flux= is an inductance expression."
No. It is total flux linkage. Inductance is its derivative dPhi/dI.
"An A-device becomes one custom SpiceSharp entity." No. It expands into multiple ordinary entities from a portable subcircuit.
"Terminal 8 is always ground." No. It is the A-device's common node and may be any node.
"An unused output can stay connected to common." No. The portable output stage must be detached onto a private node.
"AC analysis follows the whole nonlinear curve." No. Small-signal AC uses the local derivative at the DC operating point.
- Entity: an object placed in a
Circuit. - Parameters: configuration shared by an entity's behaviors.
- Behavior: the analysis-specific implementation of an entity.
- Biasing: DC and Newton-iteration behavior.
- Stamp: matrix and right-hand-side contributions from a component.
- Jacobian: matrix of local derivatives used by Newton iteration.
- Incremental value: a local slope, such as
dQ/dV. - Branch variable: an extra solver unknown, commonly a component current.
- Companion model: an equivalent algebraic model for a dynamic component at one timestep.
- Subcircuit: a reusable parameterized circuit template.
- Macromodel: a functional circuit approximation of a more complicated device.
- Generator: reader code that turns parsed netlist data into entities.
- Fallback: delegation to the standard generator when syntax is ordinary.
If you remember only five ideas, remember these:
- The package is an opt-in reader and simulation extension.
- Native custom behaviors and
.libmacromodels are different mechanisms. - Nonlinear solvers need local derivatives.
- Generators choose objects; behaviors own equations.
- A-device wiring belongs in the catalog, while expansion mechanics belong in the expander.
With that map, every folder has a specific job and the project becomes much smaller than it first appears.