SPICE Verification¶
ngspice post-sizing verification of op-amp performance metrics.
Re-simulates a sized circuit (W/L from
SizingResult) in ngspice using the
same technology as initial sizing, to cross-check the closed-form metrics
from _evaluate_metrics. For the card-less generic tech a Level-1 model
is synthesised from mu_cox/vth/lam (so SPICE ≈ the analytical
Level-1 formulas); for PTM nodes the BSIM4 .pm card is included (so the
delta reflects the Level-1-vs-device gap).
This is best-effort verification, not sign-off: each metric is measured by
an independent testbench and any that fails to converge/parse returns None
(printed as n/a) instead of raising.
- Package layout:
deck— model emission, netlist parsing/sizing, ngspice runnersrig— port classification and the shared testbench rigmeasure— per-metric testbenchesop— operating-point reading + DC bias-soundness verdictsimulate— thesimulate_metrics()entry point
- circuitgenome.sizer.shared.spice.check_bias_soundness(netlist_text, result, tech, spec)[source]¶
SPICE-grounded DC bias verdict:
(sound, reason).Runs the feedback-biased
.op(read_op_operating_point()) — which converges reliably, unlike the open-loop AC rig — and condemns the bias only on positive evidence: the operating point rails (no usable mid-rail bias) or a device is starved/triode. Conservative by design: returns(True, None)when it cannot check (ngspice absent, or a fully-differential topology the SE.oprig doesn’t support), so it only ever downgrades a feasible verdict.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
- circuitgenome.sizer.shared.spice.ngspice_available()[source]¶
True if the
ngspicebinary is on PATH.- Return type:
- circuitgenome.sizer.shared.spice.read_op_operating_point(netlist_text, result, tech, spec)[source]¶
Return
{ref: {'id','vds','vdsat'}}from a feedback-biased.op.Biases the sized circuit in unity feedback (
Lfb/Cfbrig, polarity auto-detected via the settled output), then reads each MOSFET’s actual operating point through@m.xdut.<ref>[...]. Single-ended only; returnsNonewhen ngspice is unavailable, the topology is fully-differential, or the bias doesn’t settle.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
- circuitgenome.sizer.shared.spice.simulate_metrics(netlist_text, result, tech, spec, corner=None)[source]¶
Return SPICE-measured metrics, mirroring
_evaluate_metricskeys.Keys:
power_w,gain_db,gbw_hz,phase_margin_deg,slew_rate_vps,output_swing_max_v,output_swing_min_v,cmrr_db,psrr_db. Missing/failed measurements areNone(slew rate and output swing are single-ended-only; CMRR/PSRR need a measured differential gain first).corneroverrides the PDK library corner (foundry techs only);Noneuses the tech’s nominal corner.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
corner (str | None)
- Return type:
- circuitgenome.sizer.shared.spice.sized_netlist(netlist_text, result)[source]¶
Return
netlist_textwith the sized values fromresultinjected.Each MOSFET in the single
.subcktblock gets itsW=/L=, sized load resistors get their ohm value and the compensation cap(s) getcc_pf/cc2_pf— the same injection the verification decks use. Any lines before the.subckt(title/comments) are preserved, so the output is a standalone flat netlist ready for SPICE.- Raises:
ValueError – when a MOSFET has no sizing entry — an exported design must be fully sized (an unsized device would silently run at the simulator’s default W/L).
- Parameters:
netlist_text (str)
result (SizingResult)
- Return type:
Deck building¶
SPICE deck building blocks: models, netlist parsing/sizing, ngspice runs.
Everything here is measurement-agnostic: emitting the technology’s model
block, parsing the DUT .subckt, injecting sized W/L/R/Cc values, and the
two ngspice runners (table output via wrdata, text output via print).
- circuitgenome.sizer.shared.spice.deck.ngspice_available()[source]¶
True if the
ngspicebinary is on PATH.- Return type:
- circuitgenome.sizer.shared.spice.deck._emit_model(tech, corner=None)[source]¶
Return the SPICE
.model/.include/.libblock fortech.spice_lib(foundry PDK, e.g. GF180MCU):.includethe global design file (if any) then.lib "<file>" <corner>— devices are subcircuits.spice_model(PTM): flat.includeof.model nmos/pmoscards.neither: synthesise a Level-1
.modelfrommu_cox/vth/lam.
- Parameters:
tech (TechParams)
corner (str | None)
- Return type:
- circuitgenome.sizer.shared.spice.deck._xref(ref)[source]¶
Subcircuit instance name for a MOSFET
ref(m1_in→x1_in).
- circuitgenome.sizer.shared.spice.deck._dev_prefix(tech, ref)[source]¶
ngspice operating-point handle prefix for device
refinsideXdut.PTM/generic instantiate
.modelMOSFETs (flat@m.xdut.<ref>); a PDK with adevice_mapinstantiates subcircuits, so the BSIM4 device sits one level deeper at the subckt’s internalm0(@m.xdut.<xref>.m0).- Parameters:
tech (TechParams)
ref (str)
- Return type:
- circuitgenome.sizer.shared.spice.deck._emit_body(tech, body)[source]¶
Rewrite generic
m… nmos/pmoslines as PDK subcircuitx…instances.No-op unless
tech.device_mapis set. Maps the model token via the map (nmos→nmos_3p3) and lowercasesW=/L=to the subckt params.- Parameters:
tech (TechParams)
- Return type:
- circuitgenome.sizer.shared.spice.deck._parse_subckt(netlist_text)[source]¶
Return
(name, ports, body_lines)for the single.subcktblock.- Parameters:
netlist_text (str)
- circuitgenome.sizer.shared.spice.deck.sized_netlist(netlist_text, result)[source]¶
Return
netlist_textwith the sized values fromresultinjected.Each MOSFET in the single
.subcktblock gets itsW=/L=, sized load resistors get their ohm value and the compensation cap(s) getcc_pf/cc2_pf— the same injection the verification decks use. Any lines before the.subckt(title/comments) are preserved, so the output is a standalone flat netlist ready for SPICE.- Raises:
ValueError – when a MOSFET has no sizing entry — an exported design must be fully sized (an unsized device would silently run at the simulator’s default W/L).
- Parameters:
netlist_text (str)
result (SizingResult)
- Return type:
- circuitgenome.sizer.shared.spice.deck.unsized_mos_refs(body, result)[source]¶
MOSFET refs in
bodythat_inject_sizes()would leave unsized.- Parameters:
result (SizingResult)
- Return type:
- circuitgenome.sizer.shared.spice.deck._inject_sizes(body, result)[source]¶
Set sized W/L (MOSFETs), Cc (comp caps) and R (sized load resistors).
- Parameters:
result (SizingResult)
- Return type:
- circuitgenome.sizer.shared.spice.deck._dut(tech, name, body, corner=None)[source]¶
Title line + model block + the sized
.subcktdefinition.The leading comment is mandatory: SPICE always treats the first deck line as the title and ignores it, which would otherwise swallow the first
.model. For a PDK tech the device lines are rewritten to subcircuit instances.
Testbench rig¶
Shared testbench rig: port classification, supplies, DUT instantiation.
Every measurement wraps the same sized DUT block in a small rig; the helpers
here remove the per-testbench boilerplate: the port→net map for a chosen input
polarity (_fb_netmap()), and full-deck assembly (_deck()). The
input polarity — which of (in1, in2) is the non-inverting input — is detected
once by the AC testbench (via the DC-settle check) and reused by the slew,
swing, CMRR and PSRR benches.
The bias-current source direction is not fixed: it follows the DUT’s ibias
reference-diode orientation (_iref_sink()), because an NMOS-referenced
bias generator needs the reference current pushed into the pin while a
PMOS-referenced one needs it pulled out.
- circuitgenome.sizer.shared.spice.rig._xline(name, ports, netmap)[source]¶
X-instance wiring DUT ports to testbench nets via
netmap.
- circuitgenome.sizer.shared.spice.rig._iref_sink(dut_lines)[source]¶
True when the external bias source must pull current out of
ibias.The DUT’s reference is the diode-connected MOSFET on the
ibiasnet (d == g == ibias). With its source atgnd!(NMOS reference, e.g.diode_connected_mosfet_bias) the diode sinks an injected current; with its source atvdd!(PMOS reference, e.g.magic_battery_biasorresistor_bias) the diode conducts from the supply out of the pin, so the external source must sink the current to ground — injecting instead floats the pin above VDD and leaves the whole bias generator dead.Works on raw and PDK-rewritten device lines alike (both keep the
ref d g s b modeltoken order). No reference diode, or conflicting diodes on both rails, falls back to inject (the historical behavior).- Return type:
- circuitgenome.sizer.shared.spice.rig._rig(vdd, ibias, sup_ac=False, sink=False)[source]¶
Supply + bias-current sources;
sup_acrides 1 V AC on VDD (PSRR).sinkpulls the reference current out of theibiaspin instead of injecting it — required by PMOS-referenced bias generators (see_iref_sink()).
- circuitgenome.sizer.shared.spice.rig._fb_netmap(topo, inp, inn)[source]¶
Port→net map for a feedback testbench with the given input polarity.
- circuitgenome.sizer.shared.spice.rig._deck(name, ports, body_dut, vdd, ibias, fb, netmap, control, sup_ac=False)[source]¶
Assemble a full ngspice deck: DUT + supplies + testbench
fb+ control.The bias-current direction adapts to the DUT block’s reference diode (
_iref_sink()).
Metric testbenches¶
Per-metric ngspice testbenches.
Each testbench builds an independent deck around the sized DUT block and is
best-effort: a measurement that fails to converge/parse returns None
instead of raising.
The AC bench detects the input polarity — which of (in1, in2) is the non-inverting input — via a DC-settle check, and reports it so the slew, swing, CMRR and PSRR benches reuse it instead of re-detecting.
Feedback rigs (all AC-coupled through huge L/C so the loop only sets the DC operating point):
SE loop (AC/CMRR/PSRR):
Lfbcloses out→inn at DC,CfbAC-grounds the inverting input to thecmnode.FD loop (AC/CMRR/PSRR):
L1/L2close outp→inn / outn→inp at DC.Unity buffer (slew/swing): out→inn direct — the large-signal rigs.
- circuitgenome.sizer.shared.spice.measure._pols(polarity)[source]¶
Polarities to try, the AC-detected one first.
The AC polarity is only a hint: ngspice’s
.opalso converges on the unstable equilibrium of a positive-feedback loop, so the AC bench cannot always tell the two assignments apart (their small-signal magnitudes are symmetric for a differential pair). The large-signal benches keep their own validity check and fall back to the other polarity when the hint rails.
- circuitgenome.sizer.shared.spice.measure._lf_mag(name, ports, body_dut, vdd, ibias, fb, netmap, outexpr, sup_ac=False)[source]¶
Low-frequency (1 mHz, matching the gain bench’s first AC point) magnitude of
outexprfor a 1 V AC stimulus.- Parameters:
sup_ac (bool)
- circuitgenome.sizer.shared.spice.measure._measure_power(name, ports, body_dut, topo, vdd, ibias, vcm)[source]¶
DC operating point → quiescent power.
- circuitgenome.sizer.shared.spice.measure._pm_plausible(pm)[source]¶
True unless
pmis outside the physical(0°, 180°]range.A stable amplifier’s phase at the 0-dB crossing lies below its DC phase, so
PM = 180° + phaselands in(0°, 180°]. A value above 180° (phase lead on a falling gain) is a non-minimum-phase — right-half-plane — response: a genuinely mis-compensated circuit, e.g. Miller-family compensation wrapped around a non-inverting second stage. A value ≤ 0° means the crossing came from a corrupted sweep — typically the wrong feedback polarity settling into a measurable but meaningless response, or a phase-unwrap glitch. Neither is a usable gain/GBW/PM measurement.None(no crossing found) carries no such evidence.
- circuitgenome.sizer.shared.spice.measure._loop_fb(topo, vcm, drive)[source]¶
The AC-coupled feedback loop + output expression, with
drivelines.SE:
Lfb(DC feedback) +Cfb(AC ground tocm). FD:L1/L2cross-feedback with the output CM pinned atocm.
- circuitgenome.sizer.shared.spice.measure._measure_ac(name, ports, body_dut, topo, vdd, ibias, vcm)[source]¶
Open-loop AC: returns
(gain_db, gbw_hz, pm_deg, reason, polarity).AC-coupled feedback: huge L closes the loop at DC (sets bias ≈ CM), huge C AC-grounds the inverting input; a 1 V AC source drives the non-inverting input. The (in1,in2)->(non-inv,inv) assignment is auto-detected via the DC operating point (output must settle near CM, not a rail), and among the branches that settle the one with a plausible phase margin wins (higher gain breaks ties): a corrupted branch shows an implausible PM and must not outrank the honest measurement just because its gain reads higher. The winning
polarityis returned for the other benches to reuse (Nonewhen nothing usable settled).The measured low-frequency gain is reported even when it is ≤ 0 dB (a mis-biased circuit that does not amplify) —
gbw/pmare thenNone(no 0-dB crossing) andreasonexplains why. When every settled branch is corrupt (PM outside(0°, 180°]) the whole extraction is discarded: all three values areNoneandreasonsays so.reasonisNoneon a normal (positive-gain) measurement.
- circuitgenome.sizer.shared.spice.measure._edge_slew(t, vo, vdd)[source]¶
Standard 20%-80% slew of one output transition (robust to edge spikes): the average slope across the central 60% of the swing.
- Return type:
float | None
- circuitgenome.sizer.shared.spice.measure._measure_sr(name, ports, body_dut, topo, vdd, ibias, vcm, polarity=None, sr_hint=None)[source]¶
Unity-gain large-signal pulse → slew rate (V/s), the min of the rising and falling edges. SE only (best-effort).
The transient starts from the DC operating point (no
uic— a zero-state start would measure the power-up transient instead of the step response), and the pulse width is scaled so a design slewing at ≥ 1/3 of the spec target (sr_hint) completes its transition inside the window.- Parameters:
sr_hint (float | None)
- circuitgenome.sizer.shared.spice.measure._measure_swing(name, ports, body_dut, topo, vdd, ibias, vcm, polarity=None)[source]¶
Inverting −1 DC sweep →
(swing_max, swing_min)in V. SE only.The output is driven across the supply through an inverting gain −1 network while the non-inverting input stays at CM, so the input stage never leaves its common-mode range: the sweep measures output swing, not the intersection of output swing and ICMR (a unity buffer, whose input rides with the output, conflates the two — issue #126). Large feedback resistors keep the DUT’s output unloaded.
The output is read over the contiguous tracking region around CM (small-signal slope ≤ −0.7 — outside it the output stage has saturated and the output no longer follows). The region’s edge values are the reachable output extremes.
- circuitgenome.sizer.shared.spice.measure._measure_cmrr(name, ports, body_dut, topo, vdd, ibias, vcm, polarity, adm_db)[source]¶
Common-mode rejection:
CMRR = Adm − Acmin dB.Same DC feedback loop as the gain bench, but the 1 V AC rides on the input common mode: SE — the
cmnode itself carries the AC (the inverting input receives it throughCfb, the non-inverting one through the 0 VVid); FD — both gates are AC-coupled to one source, and the common-mode → differential conversion is read at the output.
- circuitgenome.sizer.shared.spice.measure._measure_psrr(name, ports, body_dut, topo, vdd, ibias, vcm, polarity, adm_db)[source]¶
Positive-supply rejection:
PSRR+ = Adm − Avddin dB.Same DC feedback loop as the gain bench with a quiet input; the 1 V AC rides on VDD and the supply-to-output gain is read at low frequency.
Operating point & bias soundness¶
Feedback-biased operating-point reading and the DC bias-soundness verdict.
- circuitgenome.sizer.shared.spice.op.read_op_operating_point(netlist_text, result, tech, spec)[source]¶
Return
{ref: {'id','vds','vdsat'}}from a feedback-biased.op.Biases the sized circuit in unity feedback (
Lfb/Cfbrig, polarity auto-detected via the settled output), then reads each MOSFET’s actual operating point through@m.xdut.<ref>[...]. Single-ended only; returnsNonewhen ngspice is unavailable, the topology is fully-differential, or the bias doesn’t settle.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
- circuitgenome.sizer.shared.spice.op._read_op(netlist_text, result, tech, spec)[source]¶
read_op_operating_point()plus the failure kind when it isNone.The failure kind separates “the simulation ran and the output railed at both polarities” (
"railed") from “ngspice never produced a usable run” ("sim-failed"— crash, non-convergence, or nothing probed); it isNonewhen an operating point is returned.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
- circuitgenome.sizer.shared.spice.op._op_bias_problems(op)[source]¶
Return
(triode_refs, starved_refs)from an operating-point dict.Starved: drain current below 0.1 µA (device effectively off). Triode:
|Vds| < |Vdsat|(a current source/amplifier device pushed out of saturation).
- circuitgenome.sizer.shared.spice.op.check_bias_soundness(netlist_text, result, tech, spec)[source]¶
SPICE-grounded DC bias verdict:
(sound, reason).Runs the feedback-biased
.op(read_op_operating_point()) — which converges reliably, unlike the open-loop AC rig — and condemns the bias only on positive evidence: the operating point rails (no usable mid-rail bias) or a device is starved/triode. Conservative by design: returns(True, None)when it cannot check (ngspice absent, or a fully-differential topology the SE.oprig doesn’t support), so it only ever downgrades a feasible verdict.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
- circuitgenome.sizer.shared.spice.op._bias_diagnostic(netlist_text, result, tech, spec)[source]¶
One-line summary of devices in triode / starved, when AC found no gain.
Reuses the feedback-biased
.opreader to explain why a circuit doesn’t amplify (the usual cause: stacked devices don’t fit the supply headroom).- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- Return type:
str | None
Entry point¶
Public entry point: run every metric testbench on a sized circuit.
- circuitgenome.sizer.shared.spice.simulate.simulate_metrics(netlist_text, result, tech, spec, corner=None)[source]¶
Return SPICE-measured metrics, mirroring
_evaluate_metricskeys.Keys:
power_w,gain_db,gbw_hz,phase_margin_deg,slew_rate_vps,output_swing_max_v,output_swing_min_v,cmrr_db,psrr_db. Missing/failed measurements areNone(slew rate and output swing are single-ended-only; CMRR/PSRR need a measured differential gain first).corneroverrides the PDK library corner (foundry techs only);Noneuses the tech’s nominal corner.- Parameters:
netlist_text (str)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
corner (str | None)
- Return type: