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 DC
.op(read_op_operating_point()— SE in unity feedback, FD with inputs/vcm_refat Vcm, issue #162) and condemns the bias only on positive evidence: the operating point rails (no usable mid-rail bias; for FD also a split output CM) or, single-ended, a device is starved/triode. FD skips the per-device verdicts: with inputs at Vcm the CMFB amp’s tail (its inputs also sit at Vcm) and the main tail run in marginal triode by design at low supplies — degraded, still functional, and universal to the family — so a device-level condemnation would reject every low-voltage CMFB variant that measurably amplifies at this very operating point; a dead FD circuit rails/splits its outputs instead, which the.opverdict already catches (the benches quantify any marginality). Conservative by design: returns(True, None)when it cannot check (ngspice absent), 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.pdk_netlist(sized_text, tech, base_dir=None)[source]¶
Rewrite a sized generic netlist into the PDK-native, simulatable form.
The generic
*_sized.cktstays the canonical interchange format (the recognizer parses only genericnmos/pmosM-lines); this sibling is for running directly against the PDK: the corner.libheader plus the same rewrite the verification decks use (m…→x…,device_mapsubcircuit names, native W/L units viawl_units). Header paths are emitted relative tobase_dir(the export directory) when given, absolute otherwise — either way they are resolved on the machine that exported the file.- Raises:
ValueError – for a tech without
spice_lib+device_map— there is no PDK form to export.- Parameters:
sized_text (str)
tech (TechParams)
- Return type:
- circuitgenome.sizer.shared.spice.read_op_operating_point(netlist_text, result, tech, spec)[source]¶
Return
{ref: {'id','vds','vdsat'}}from a DC.op.Single-ended: biases the sized circuit in unity feedback (
Lfb/Cfbrig, polarity auto-detected via the settled output). Fully differential (issue #162): both inputs andvcm_refsit at Vcm with no feedback loop — the CMFB / loads own the output CM, which is exactly the DC state the metric benches run at. Either way each MOSFET’s actual operating point is read through@m.xdut.<ref>[...]; returnsNonewhen ngspice is unavailable 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, model)[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 internal instance —m0by default (GF180), ortech.device_handle[model]when the PDK names it per cell (sky130’smsky130_fd_pr__nfet_01v8).modelis the generic device type ofref("nmos"/"pmos").- Parameters:
tech (TechParams)
ref (str)
model (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. For awl_units="um"PDK (sky130) the SI micro suffix is dropped —w=10.0u→w=10.0— because the library’s.option scale=1.0ualready interprets bare instance W/L as microns.- 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.pdk_netlist(sized_text, tech, base_dir=None)[source]¶
Rewrite a sized generic netlist into the PDK-native, simulatable form.
The generic
*_sized.cktstays the canonical interchange format (the recognizer parses only genericnmos/pmosM-lines); this sibling is for running directly against the PDK: the corner.libheader plus the same rewrite the verification decks use (m…→x…,device_mapsubcircuit names, native W/L units viawl_units). Header paths are emitted relative tobase_dir(the export directory) when given, absolute otherwise — either way they are resolved on the machine that exported the file.- Raises:
ValueError – for a tech without
spice_lib+device_map— there is no PDK form to export.- Parameters:
sized_text (str)
tech (TechParams)
- 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 DC-bias network + output expression, with
drivelines.SE:
Lfb(DC feedback) +Cfb(AC ground tocm). FD (issue #165): inputs DC-anchored at Vcm through huge inductors (DC shorts, AC opens) with the CMFB owning the output CM — the standard FD bench. The old cross-feedback ties (outp``→``inn/outn``→``inp) were a pre-#165 crutch that invented an output CM for unregulated circuits; against a real CM loop they form a bistable DC network whose degenerate all-off solution ngspice picks at tight headroom (sky130: healthy circuits measured −79 dB), and they made the FD DC sanity check vacuous (ties forceoutp == outn). With free outputs that check is meaningful again.
- 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¶
DC operating-point reading and the bias-soundness verdict (SE and FD).
- circuitgenome.sizer.shared.spice.op.read_op_operating_point(netlist_text, result, tech, spec)[source]¶
Return
{ref: {'id','vds','vdsat'}}from a DC.op.Single-ended: biases the sized circuit in unity feedback (
Lfb/Cfbrig, polarity auto-detected via the settled output). Fully differential (issue #162): both inputs andvcm_refsit at Vcm with no feedback loop — the CMFB / loads own the output CM, which is exactly the DC state the metric benches run at. Either way each MOSFET’s actual operating point is read through@m.xdut.<ref>[...]; returnsNonewhen ngspice is unavailable 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” (
"railed"— SE: at both feedback polarities; FD: an output at a rail or a split output CM) 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._parse_probes(prefixes, txt)[source]¶
Collect
{ref: {param: value}}from printed@m...[param]probes.
- circuitgenome.sizer.shared.spice.op._FD_SPLIT_FRAC = 0.2¶
Largest |V(outp) − V(outn)| (fraction of the supply) an FD
.opmay show at zero differential input before the verdict is “railed”: a split output CM is the signature of an unregulated output stage latching apart.
- circuitgenome.sizer.shared.spice.op._read_op_fd(name, ports, body, topo, result, tech, spec)[source]¶
FD
.opin the metric benches’ DC state (#162, rig per #165).Inputs and
vcm_refsit at Vcm with no feedback ties — post-#165 the CMFB owns the output CM, and this is exactly the DC state the FD benches measure in (_loop_fb’s FD branch anchors the inputs the same way). The old bench-tie network (outp``→``inn/outn``→``inp) is bistable against a real CM loop and converges to its degenerate all-off solution at tight headroom, falsely condemning healthy circuits. Verdict"railed"when either output leaves the0.1–0.9·Vddwindow or the outputs split apart at zero differential input; otherwise the per-device operating points feed the usual starved/triode checks.- Parameters:
topo (_Topo)
result (SizingResult)
tech (TechParams)
spec (SizingSpec)
- 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 DC
.op(read_op_operating_point()— SE in unity feedback, FD with inputs/vcm_refat Vcm, issue #162) and condemns the bias only on positive evidence: the operating point rails (no usable mid-rail bias; for FD also a split output CM) or, single-ended, a device is starved/triode. FD skips the per-device verdicts: with inputs at Vcm the CMFB amp’s tail (its inputs also sit at Vcm) and the main tail run in marginal triode by design at low supplies — degraded, still functional, and universal to the family — so a device-level condemnation would reject every low-voltage CMFB variant that measurably amplifies at this very operating point; a dead FD circuit rails/splits its outputs instead, which the.opverdict already catches (the benches quantify any marginality). Conservative by design: returns(True, None)when it cannot check (ngspice absent), 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: