Core Solver Overview¶
Project overview | 中文说明 | 中文版
Status: maintained architecture reference. Module responsibilities and data flow are maintained; benchmark and calibration numbers near the end are dated snapshots and should be reproduced before use in a new report.
This document introduces the current circuitopt/ solver stack. It is a local
analog-circuit simulation and optimization framework spanning the AT4000TG
analytic model and native BSIM4-backed SKY130, FreePDK45, and TSMC28 flows.
Scope¶
The current solver stack covers:
- DC operating-point solving.
- AC small-signal gain and bandwidth analysis.
- Noise analysis, including flicker noise and thermal noise.
- Transient response simulation.
- Periodic steady-state (PSS) shooting for periodic transient orbits.
- PSS-assisted PAC (periodic AC) via analytic-adjoint harmonic balance (default), opt-in time-domain Floquet shooting, or finite-difference shooting.
- Periodic PNoise, with harmonic-balance and time-domain Floquet-adjoint paths plus cyclostationary noise folding.
- Process-corner and per-device mismatch perturbations.
- Cadence/Spectre-oriented validation for operating point, AC, noise, transient, PSS, PAC, and PNoise behavior.
The implementation is intentionally small and self-contained. Its modules include
__init__.py, the CLI entry __main__.py,
calibration/PSF/Cadence-netlist helpers, shared diagnostics/profiling
modules, the main solver stack, an ML-surrogate layer (dataset builder, surrogate
training, screen-and-verify optimizer), three silicon PDKs — SKY130 (via bundled
resolved cards and native Berkeley BSIM4.5), FreePDK45 (via a flat-card loader and native Berkeley
BSIM4.5), and TSMC28HPC+ (via the internal HSPICE parser and the same native backend) — plugged into the
same TransistorModel interface as the original AT4000TG OTFT model, and an
optional local HTTP service layer (circuitopt/service/) over the whole stack.
File Structure¶
circuitopt/
topology.py Circuit topology source of truth.
compiled_topology.py Runtime-compiled topology/index/stamp metadata.
circuit_loader.py JSON circuit description loader.
device_model.py TransistorModel ABC + OtftParams + model factory/registry + PDK/polarity layer.
device_factory.py Device build/resolve layer (build_devices, get_ss_params) + corner routing
(OTFT CORNERS, silicon apply_silicon_corner). Leaf module: depends
only on device_model.
pmos_tft_model.py AT4000TG PMOS-OTFT compact-model implementation.
ac_mna.py MNA stamping primitives.
ac_solver.py Pure AC small-signal solver: DC operating point + AC response (ac_solve).
dc_solver.py DC solve fallback (bounded least squares) + AFE-specific symmetric DC
seeding/continuation heuristics.
noise_solver.py Noise propagation and input-referred noise analysis.
transient_solver.py Time-domain transient solver.
transient_profile.py Shared transient/chopper analysis counter slots.
pss_solver.py Transient-shooting periodic steady-state solver.
pac_solver.py Generic PSS-assisted PAC solver.
pnoise_solver.py Generic PNoise solver (HB + TD adjoint).
adaptive_config.py Shared adaptive-step configuration types and helpers.
analysis_dispatch.py JSON analysis-configuration dispatch entry point.
analysis_options.py Central solver-option registry for JSON dispatch.
diagnostics.py Thread-safe solver-fallback observer (counters + logging).
psf.py PSFASCII parser for Spectre reference data.
calibration.py Local-vs-Cadence calibration comparison helpers.
cadence_netlist.py Spectre netlist generation helpers for validation runs.
chopper.py Ideal and PMOS-switch differential chopper analyses.
explore.py Design-space exploration / optimization driver.
corners.py Process corners, mismatch MC, and latch detection.
dataset.py Labeled surrogate-training dataset builder (provenance + failure-retaining).
surrogate.py Baseline metric surrogate (GBT via optional scikit-learn) + region-of-interest filtering.
surrogate_torch.py Differentiable surrogate (torch/MPS) + gradient-based design optimization.
optimize.py Screen-with-surrogate / Pareto-select / verify-with-solver optimization loop.
sky130_model.py SKY130 compatibility exports + explicit ngspice card-extraction utility.
ngspice_char.py Model-card evaluator: batch ngspice .dc/.noise characterization → cached (Vsb,Vds,Vgs) grid.
ngspice_device.py TransistorModel over a cached ngspice grid (interpolated Id/gm/gds/caps/noise; extract_w + temperature).
ngspice_process.py Process-adapter protocol: deck preamble, instance syntax, op vectors, simulator flags.
freepdk45_model.py FreePDK45 compatibility exports + explicit ngspice oracle registration.
pdk/freepdk45/ Flat-card loader and native FreePDK45 nmos/pmos adapter.
pdk/sky130/ Bundled resolved-card loader and native SKY130 nmos/pmos adapter.
compact_models/bsim4/ Native Berkeley BSIM4 host, ABI, and the ctypes bridge into the compiled Rust core.
tsmc28_model.py TSMC28HPC+ core nmos/pmos binding: nch_mac/pch_mac + HSPICE library closure.
service/ Optional local FastAPI HTTP service layer (the `serve` extra) — see below.
__init__.py Re-exports CLI glue only; never imports fastapi (import circuitopt stays fastapi-free).
app.py create_app() — /api/v1 routes (health/capabilities/validate/solve/jobs/*); thin adapter, no numerics.
operations.py Transport-neutral capabilities/validate/solve operations shared by HTTP and MCP.
jobs.py Extensible in-process thread-pool jobs with progress queue + cooperative cancel.
serialize.py to_jsonable()/serialize_results() — numpy/complex/NaN → strict-JSON conventions.
cli.py add_cli_args()/run_cli() — shared `serve` subcommand argument wiring (lazy fastapi/uvicorn import).
mcp/ Optional stdio/Streamable HTTP MCP adapter (the `mcp` extra).
server.py MCPServer tools/resources, bounded results, and background signoff adapter.
workspace.py Workspace-relative input validation and atomic result artifacts.
cli.py Shared `mcp` subcommand and standalone entry-point arguments.
Import Relationship¶
Module-level imports form a directed acyclic graph ten layers deep. The diagram groups those layers; the exhaustive edge list follows it.
flowchart TB
subgraph BASE["L0 — no internal dependency"]
direction LR
B1["topology<br/>compiled_topology"]
B2["device_model"]
B3["run_contract · analysis_options<br/>adaptive_config · diagnostics"]
B4["_rust_lti · _rust_transient<br/>_rust_periodic"]
end
subgraph DESC["L1–L2 — description and devices"]
direction LR
D1["circuit_loader"]
D2["device_factory"]
D3["pdk/* · compact_models/bsim4<br/>spice/*"]
end
subgraph SOLV["L2–L4 — solvers"]
direction LR
S1["dc_solver"] --> S2["ac_solver"]
S2 --> S3["noise_solver"]
S2 --> S4["transient_solver"]
end
subgraph PER["L5–L6 — periodic family"]
direction LR
P1["pss_solver"] --> P2["pac_solver"] --> P3["pnoise_solver"]
end
DISP["L7 — analysis_dispatch"]
subgraph WF["L5–L7 — workflows"]
direction LR
W1["explore · corners"]
W2["dataset · optimize"]
W3["sar · sar_mc · sar_explore"]
W4["chopper · calibration"]
end
CAMP["L8 — signoff_campaign<br/>L9 — engine_crosscheck"]
subgraph NG["ngspice oracle"]
direction LR
N1["ngspice_render"] --> N2["ngspice_ac<br/>ngspice_transient"]
end
subgraph SVC["L9 — services, consumer leaves"]
direction LR
V1["service/*"]
V2["mcp/*"]
end
BASE --> DESC
DESC --> SOLV
DESC --> NG
SOLV --> PER
SOLV --> DISP
PER --> DISP
SOLV --> WF
PER --> WF
DISP --> CAMP
DISP --> SVC
WF --> SVC
CAMP --> SVC
Two properties of this graph are worth stating, because both are load-bearing:
explore,corners,datasetandchopperreach the solvers directly, not throughanalysis_dispatch. Dispatch drives one circuit through a configured analysis suite; those four drive a candidate matrix and need the solvers raw.service/andmcp/are pure consumer leaves. Nothing outside them imports back in, andcircuitopt/__init__.pynever imports either, so plainimport circuitoptstays fastapi-free even with theserveextra installed.
The exhaustive list below records module-level imports only. Two pairs —
run_contract/dc_measurements and sar/sar_rust — also import each other from
inside functions; that is the deliberate way to break an import cycle in Python and
is why the module-level graph stays acyclic.
topology.py <- no internal dependency
compiled_topology.py <- no internal dependency; consumes Topology-like objects at runtime
device_model.py <- no internal dependency (abc, dataclasses only)
ac_mna.py <- no internal dependency
run_contract.py <- no internal dependency (dc_measurements imported lazily)
analysis_options.py <- no internal dependency (registry)
adaptive_config.py <- no internal dependency (dataclass only)
transient_profile.py <- no internal dependency (counter slot constants)
diagnostics.py <- no internal dependency (thread-safe counters)
psf.py <- no internal dependency
adc.py <- no internal dependency
frequency_metrics.py <- no internal dependency
passive_bom.py <- no internal dependency
toolchain.py <- no internal dependency
surrogate.py <- no internal dependency; optional scikit-learn/joblib at runtime
_engine.py <- no internal dependency
_parallel.py <- no internal dependency
_rust_lti.py <- no internal dependency (serialization bridge)
_rust_transient.py <- no internal dependency (serialization bridge)
_rust_periodic.py <- no internal dependency (serialization bridge)
spice/parser.py <- no internal dependency
spice/expressions.py <- no internal dependency
spice/elaborator.py <- spice/parser, spice/expressions
circuit_loader.py <- topology, device_model
device_factory.py <- device_model, run_contract (leaf device layer; no solver/workflow imports)
dc_measurements.py <- run_contract
pmos_tft_model.py <- device_model (equations in circuitopt_core.OtftModel)
cadence_netlist.py <- topology
_campaign_sweep.py <- _engine
compact_models/bsim4/abi.py <- no internal dependency
compact_models/bsim4/card_cache.py <- no internal dependency
compact_models/bsim4/native.py <- compact_models/bsim4/abi
compact_models/bsim4/rust_transient.py <- _rust_transient, compiled_topology, bsim4/native
compact_models/bsim4/transient.py <- adaptive_config, compiled_topology, device_factory
pdk/<name>/library.py <- compact_models/bsim4, spice, toolchain
pdk/<name>/device.py <- compact_models/bsim4, device_model, pdk/<name>/library
sky130_model.py <- pdk/sky130, toolchain
freepdk45_model.py <- pdk/freepdk45, device_model, ngspice_device
tsmc28_model.py <- device_model, ngspice_device, ngspice_process, toolchain
ngspice_char.py <- toolchain; ngspice subprocess + numpy only
ngspice_process.py <- device_model
ngspice_device.py <- device_model, ngspice_char; optional scipy at runtime
ngspice_render.py <- device_factory, freepdk45_model, ngspice_process, toolchain
ngspice_ac.py <- device_factory, ngspice_char, ngspice_render
ngspice_transient.py <- device_factory, ngspice_char, ngspice_render
dc_solver.py <- device_factory, topology
ac_solver.py <- compiled_topology, dc_solver, device_factory, run_contract, topology
noise_solver.py <- ac_solver, compiled_topology, device_factory, device_model, run_contract, topology
transient_solver.py <- ac_solver, adaptive_config, compiled_topology, device_factory, topology, transient_profile
pss_solver.py <- ac_mna, ac_solver, adaptive_config, device_factory, topology, transient_solver
pac_solver.py <- ac_mna, ac_solver, device_factory, topology, transient_solver
pnoise_solver.py <- ac_mna, device_factory, noise_solver, pac_solver, run_contract
analysis_dispatch.py <- ac_solver, noise_solver, transient_solver, pss_solver, pac_solver,
pnoise_solver, circuit_loader, analysis_options, adaptive_config, run_contract
calibration.py <- ac_solver, adaptive_config, noise_solver
chopper.py <- ac_solver, dc_solver, device_factory, device_model, adaptive_config,
noise_solver, pss_solver, pac_solver, pnoise_solver, topology, transient_solver
explore.py <- ac_solver, circuit_loader, device_factory, frequency_metrics, noise_solver, run_contract
corners.py <- _campaign_sweep, _parallel, ac_solver, circuit_loader, device_factory,
noise_solver, run_contract, topology
dataset.py <- circuit_loader, device_factory, device_model, explore, run_contract, transient_solver
optimize.py <- circuit_loader, dataset, device_factory, explore
surrogate_torch.py <- surrogate (dataset lazily, CLI only); optional torch at runtime
_rust_campaign.py <- compiled_topology, device_factory, topology
sar.py <- _parallel, adc, circuit_loader, transient_solver
sar_mc.py <- _parallel, adc, circuit_loader, sar
sar_rust.py <- _rust_transient, compiled_topology, device_factory, sar, transient_solver
sar_explore.py <- adc, circuit_loader, explore, sar, sar_mc
signoff_campaign.py <- _parallel, analysis_dispatch, circuit_loader, compact_models/bsim4, run_contract
engine_crosscheck.py <- analysis_dispatch, circuit_loader, compact_models/bsim4, device_model, signoff_campaign
service/serialize.py <- no internal dependency; numpy only
service/jobs.py <- corners, explore, service/serialize; no fastapi (pure threading/queue)
service/operations.py <- analysis_dispatch, analysis_options, circuit_loader, device_factory,
device_model, freepdk45_model, run_contract, service/jobs, service/serialize
service/app.py <- service/operations, service/jobs; optional fastapi/pydantic at import time
service/cli.py <- service/app (lazy); optional uvicorn at runtime
mcp/workspace.py <- no internal dependency
mcp/server.py <- mcp/workspace, service/operations, service/jobs, service/serialize,
signoff_campaign; optional mcp SDK at import time
mcp/cli.py <- mcp/server (lazy)
Main Components¶
pmos_tft_model.py¶
Implements the AT4000TG PMOS-OTFT compact model in Python. It provides:
- Terminal current evaluation through
get_Idc. - Drain-current noise PSD through
get_noise_psd. - Bias-dependent terminal capacitances through
get_capacitances. - Geometry area calculation through
g_area. - Process and mismatch parameters such as
pvt0,mvt0,pbeta0, andmbeta0. - A warm-started internal-node operating point solve.
- Compiled scalar kernels: the device equations evaluate in
circuitopt_core.OtftModel(the sole engine as of v2.0.0).
For AC and noise analysis, the solver extracts terminal gm and gds by finite-differencing get_Idc, matching the terminal behavior used by the circuit solver.
PMOS_TFT inherits from :class:~device_model.TransistorModel, the abstract
base class consumed by all solvers. It also provides get_otft_params() for the
transient solver’s compiled inner loop and a get_ss_params() override backed by
the compiled Rust core’s analytic terminal derivatives.
device_model.py¶
Defines the abstract device‑model interface that decouples solvers from concrete transistor implementations:
TransistorModel(ABC) — seven abstract methods (get_Idc,get_op,get_capacitances,get_capacitance_charges_from_op,get_capacitance_branch_terms_from_op,get_noise_psd,get_otft_params);get_ss_paramsprovides a finite‑difference default that subclasses can override.OtftParams(frozen dataclass) — the 16 scalar parameters extracted once per device and passed to the compiled transient kernels.- Backend-capability class attributes — generic solvers dispatch on capabilities, never on a concrete backend type.
HAS_TERMINAL_LINEARIZATION(defaultFalse) advertises the full quasi-static 4×4 terminal(G, C)stamp used by AC/PAC/PNoise; native BSIM devices set it toTrue.TRANSIENT_BACKEND(defaultNone, meaning the selected engine's generic OTFT transient path) names a specialised integrator such as"bsim4_native"or an explicit external oracle backend. register_model()/create_device()+ PDK/polarity layer — the factory and registry described below.
Registry keys. Each (pdk, polarity) pair registers under a structured key
"<pdk>.<polarity>", for example "at4000tg.pmos". register_pdk() groups one
process's polarities and marks the default. "pmos_tft" stays a back-compat alias.
Single switch point. Solver files call create_device(get_default_model_type(), …)
instead of hardcoding a model name, so a new process or an nmos polarity slots in
with one register_pdk call and no solver edits.
Read access. get_model_class(model_type) is a public read-only accessor, so a
solver can inspect a model's capability flags without importing a concrete backend
class. registered_models() returns a read-only {model_type: "module.QualName"}
snapshot of the whole registry in insertion order, for callers that need to
enumerate rather than look up one entry; the service layer's
GET /api/v1/capabilities uses it to list every selectable model key.
Out of scope. Generic elements — resistors, capacitors, ideal voltage and current sources, controlled sources — are process-independent topology primitives and are not in this registry, so every PDK reuses them unchanged.
Collisions. register_model() replaces an existing entry on re-registration, which
keeps an intentional swap-in such as a test stub working silently. A genuine collision
— a different class, compared by __module__.__qualname__, taking over an occupied
name, for instance two PDK modules racing for the same alias — emits a RuntimeWarning
before overwriting. A repeat import or importlib.reload of the same class stays
silent.
device_factory.py¶
The leaf device-build layer: it depends only on device_model (never on any solver or workflow
module), so every solver can import it without risking a cycle.
build_devices(sizes, *, nf=None, corner=None, topo, model_types=None, device_kwargs=None)/get_ss_params(...)— turn the per-device inputs a solver already carries (sizes, NF, corner,model_types,device_kwargs) into concreteTransistorModelinstances, migrated here fromac_solver.pyunchanged.dev_corner/dev_nf/is_per_device_cornerare the small per-device corner/NF resolution helpers behind them.CORNERS— the OTFT continuous-PVT global process shift dict (typical/slow/fastaspvt0/pbeta0), migrated here fromcorners.py;corners.pynow imports it from here instead of defining it.SKY130_CORNERS/SILICON_CORNERS/apply_silicon_corner(model_types, device_kwargs, corner)— the silicon discrete-corner routing that stamps a corner name (tt/ss/ff/sf/fsfor SKY130, plusnomfor FreePDK45) onto extracted silicon device cards, migrated here fromexplore.py;explore.pyanddataset.pynow import it from here instead of defining it.CircuitBinding— a frozen dataclass bundling the six per-circuit inputs every solver used to thread by hand:topo/model_types/device_kwargs/nf/corner/dc_seed. It exists to close a bug class: droppingmodel_types/device_kwargson the way into a solver silently reverted the circuit to the default OTFT PDK. A caller now passesbinding=once instead of re-plumbing the cluster. Build one fromCircuitSpec.binding()(seecircuit_loader.py).binding.build(sizes)materializes{name: TransistorModel}for those sizes;binding.at_corner(corner)returns a binding routed to a corner — a silicon corner is baked ontodevice_kwargs(solver corner cleared, viaapply_silicon_corner), an OTFT corner stays onbinding.corner, andNonereturnsself. Resolution priority (viaresolve_binding): an explicit non-Nonekeyword to a solver always wins; otherwise the binding field supplies the default (binding.dc_seedbacksx0_guess); withbinding=Nonethe legacy kwargs path is byte-identical. All six solver entry points (ac_solve/noise_analysis/transient/pss_solve/pac_solve/pnoise_solve) acceptbinding=, and the internal workflows —analysis_dispatch.run_analysis_suite,explore,dataset,optimize— thread one binding rather than re-forwarding the model cluster to each branch.
topology.py¶
Defines the circuit topology as the single source of truth. The topology contains the transistor list, solved node list, rail/bias nodes, outputs, AC input drives, load capacitors, transient input mapping, DC guesses, and DC aliases. Solver runtime metadata is derived from this topology instead of being hand-written separately in each solver.
Alongside the transistors it also carries passive and source elements. None of them touch the transistor model machinery.
| Element | Fields | Semantics |
|---|---|---|
resistors |
a, b, R | Resistance in ohms. |
capacitors |
a, b, C | Capacitance in farads. |
isources |
nplus, nminus, I | Ideal DC current source, I flows nplus → nminus. |
vsources |
p, q, value | Ideal voltage source, true MNA. |
vccs |
p, q, ctrl_p, ctrl_n, gm | Voltage-controlled current source. |
vcvs |
p, q, cp, cn, mu | Voltage-controlled voltage source, Vp−Vq = μ(Vcp−Vcn). |
cccs |
p, q, ctrl_name, beta | Current-controlled current source, Iout = β·Ictrl. |
ccvs |
p, q, ctrl_name, gamma | Current-controlled voltage source, Vp−Vq = γ·Ictrl. |
Each vsource, VCVS and CCVS adds one branch-current unknown and one constraint row,
growing the system from n to n_aug = n + m. CCCS and CCVS can cascade: either may
control on the branch current of any vsource, VCVS or CCVS.
The elements flow through every analysis:
- DC — resistor branch currents and current-source injections enter the KCL.
- AC — resistors stamp as
1/R, capacitors asjωC, VCCS asgm*(Vcp-Vcn), VCVS/CCVS/vsource as a bordered[[Y,B],[B^T,0]]block with their constraint rows, and CCCS as a coupling into the KCL rows. Current sources are open circuits in the small-signal system. - Noise — resistors add
4kT/Rthermal noise. All controlled sources and ideal voltage sources are noiseless. - Transient — resistor conductances, capacitor companions, constant/VCCS/CCCS source currents, and VCVS/CCVS/vsource branch-current unknowns with their constraint equations.
The default topology is AFE_TOPO, a 10-transistor fully differential AFE core with tail current device, input pair, output stage, and cross-coupled positive-feedback level shifting devices.
compiled_topology.py¶
Builds a runtime plan from a declarative Topology and a bias/input context. It resolves node names once into compact terminal tokens and exposes shared metadata for DC, AC/noise, and transient:
- solved-node indices and rail values;
- per-device drain/gate/source terminal tokens;
- resistor, capacitor, current-source, VCCS, VCVS, CCCS, and CCVS stamp metadata;
- AC/noise
("n", idx)/("v", value)terminal tables; - transient input and
node_inputsmappings.
This keeps AC, noise, and transient on the same indexing/stamping convention while preserving the ability to swap in a different JSON-defined circuit.
It also hosts the small marshalling helpers term_arrays() (splits (kind, ref_or_value) terminal
tokens into parallel kind/ref/value int/float arrays) and index_array() (packs optional
integer indices into an int64 array, None → -1). The generic and native-BSIM
transient marshals build the same stamp-ready arrays from these, so the
helpers live with the topology tokens they share instead of being duplicated per backend.
circuit_loader.py¶
Loads JSON circuit descriptions and returns a CircuitSpec containing:
topologysizesbiasnf
This lets new circuits be added through JSON files such as examples/single_stage.json without editing solver source code. CircuitSpec.binding() bundles the spec's topology, model_types, device_kwargs, nf, and default DC seed (its first dict dc_guess) into a CircuitBinding (see device_factory.py), so a workflow can pass binding= to the solvers instead of threading the whole cluster.
numba_kernels.py (removed in v2.0.0/R7)¶
Historical: this module carried the pure-Python/numba _impl scalar and
transient kernels. The numba JIT went away in R6; in R7 the whole module was
deleted after its load-bearing part — the OTFT root-selection recovery oracle —
was ported bit-exactly into the compiled core as
circuitopt_core.OtftModel(..., reference=True) (selected by
pmos_tft_model.otft_reference_mode). The frozen golden corpus
(tests/golden/engine_parity) is the numerical reference oracle.
analysis_options.py / analysis_dispatch.py¶
analysis_options.py is the central per-analysis option registry that
analysis_dispatch.py's run_analysis_suite (and the JSON schema regression
test) both derive from, so solver kwargs/defaults/schema cannot silently drift
apart. validate_analysis_cfg(analysis, cfg) rejects residual keys in a JSON
analyses block: known_keys(analysis) unions the solver's option registry
with DISPATCH_KEYS (the handful of keys — e.g. freqs/corner/band for
ac/noise, signed_devices for transient — that run_analysis_suite
reads directly out of cfg rather than forwarding into solver_kwargs); any
key outside that union raises ValueError naming the analysis, the offending
key(s), and the sorted list of valid keys. This turns a typo (e.g.
max_sidebands for max_sideband) into an immediate error instead of a
silently-ignored option running with its default.
ANALYSIS_ORDER = ("ac", "noise", "transient", "pss", "pac", "pnoise") in
analysis_dispatch.py is the canonical analysis-name tuple and execution order;
run_analysis_suite iterates it, and the service layer's GET
/api/v1/capabilities builds its analyses map by iterating the same tuple, so
the two surfaces list identical analysis names with no separate hardcoded list.
psf.py¶
provenance(path)["fundamental"] reads the PSF HEADER's "fundamental
frequency" key (falling back to the bare "fundamental" spelling for any
non-standard writer) — periodic analyses (PAC/PNoise/PSS) report their real
drive frequency; DC/AC/noise/tran carry neither key and read back None.
parse_noise(path)'s per-device noise arrays are ragged: the column count
follows each device's TYPE-declared struct width (a MOSFET's (flicker,
thermal, total) struct is width 3; a resistor's (rn, total) struct is width
2), so callers must read the last column ([:, -1]) for the total
contribution and check .shape[1] before slicing a specific field — never
assume width 3.
ac_mna.py¶
Provides the low-level MNA stamping primitives used by the small-signal solvers:
- Admittance stamping.
- VCCS, VCVS, CCCS, and CCVS stamping.
- Ideal voltage source stamping (bordered MNA).
- MOS small-signal stamping.
ac_solver.py¶
Solves the DC operating point and AC response:
ac_solve(sizes, bias, freqs, corner=None, x0_guess=None, topo=AFE_TOPO, nf=None)- Uses
scipy.fsolvefor the DC node equations. - Returns gain, bandwidth, node operating point, and extracted small-signal parameters.
- Supports both global process corners and per-device mismatch maps.
- Uses topology metadata for output sensing, load capacitance, and AC input drive.
The DC solve includes robustness handling for physical branch selection, symmetric operating points,
and rail-bounded node solutions — implemented in dc_solver.py and called from here. ac_solver.py
itself is now a pure AC small-signal module: ac_solve plus the bw_from_gain bandwidth helper. It
no longer carries the device-factory or DC-seeding code it used to.
dc_solver.py¶
DC operating-point solving support that used to live inline in ac_solver.py, split out because it
mixes two different concerns:
bounded_least_squares_dc(...)/dc_residual_ok(...)— a generic last-resort DC solve: a bounded least-squares fallback used when the primary Newton/fsolvepath fails to converge, plus the residual-acceptance check that gates it.symmetric_seed(...)/symmetric_continuation(...)/is_afe_topology(...)/is_pairwise_symmetric_afe(...)/_AFE_SYMMETRIC_PAIRS— a circuit-specific seeding heuristic for the AFE topology only, not general solver logic. It selects the physical (Spectre-matching) symmetric power-up branch for that one circuit. Keeping it in its own module keepsac_solver.py's generic solve free of per-circuit branches.
noise_solver.py¶
Performs noise propagation on the same topology-derived MNA system used by AC analysis. Each transistor drain-current noise source is injected between drain and source, propagated to the configured output, and divided by the signal gain to obtain input-referred noise.
The noise flow supports the same topology-derived terminal mapping and corner/mismatch parameter passing as the AC solver. In JSON dispatch, when AC and noise use the same frequency grid, noise reuses the prior AC dc_op, small-signal parameters, and gains instead of running DC/AC again; with different grids it still uses the AC dc_op as a warm seed.
chopper.py¶
Computes gain, bandwidth, and baseband noise for chopper variants around the AFE:
chopper_analysis(...)is the ideal synchronized differential chopper model. It treats the eight-switch commutator as +/-1 square-wave multipliers at the input and output, then folds sideband gain/noise back to baseband with odd harmonic coefficients. This is the linear periodically time-varying (LPTV) frequency-domain path for ideal chopping and flicker-noise movement.build_afe_pmos_chopper(...)inserts the eight switches as realPMOS_TFTpass devices around the AFE input/output ports.pmos_chopper_analysis(...)runs static phase A/B AC and noise on that PMOS switch topology and averages the phases. It includes switch Ron loading, nonlinear capacitance, and PMOS switch noise.finite_edge_clock_pair(...)andfinite_edge_chopper_harmonics(...)model finite clock edge time and break-before-make dead time in the chopper waveform.pmos_chopper_lptv_analysis(...)folds the PMOS-switch sideband response/noise with those finite-edge harmonic weights. It is a fast first-order quasi-static estimate and underestimates the baseband gain by ~10% (it omits the higher-order LPTV conversion). For Cadence-grade gain/noise use the constant-free harmonic- balance path (pmos_chopper_pss→pmos_chopper_pac/pmos_chopper_pnoise). The old Cadence-fit conversion-phase / noise-PSD-scale constants were retired; theconversion_phase_rad/periodic_noise_psd_scaleargs remain as manual knobs.pmos_chopper_transient(...)drives the eight-PMOS topology with finite-edge clocks. The default clock follows Spectretype=pulsetiming (delay=T/2,width=T/2, finiterise/fall), while the older centered phase waveform is still available withclock_style="phase"for dead-time experiments. Clock feedthrough comes from the PDKCgss/Cgdd * ddt()terms and the long-timescaleR_cap2gate-leak terms stamped by the transient solver; optional charge-injection pulses are estimated from the same PDK capacitance equations and stamped as time-varying current sources. The helper refines the internal time grid around clock edges, uses signed terminal currents for the eight bidirectional pass switches, and keeps a tight residual tolerance so slow common-mode charge balance is not lost.pmos_chopper_pss(...)wraps that same hard-switched topology in the generic shooting PSS solver and returns one periodic orbit for the selected clock period. This is the foundation for native PAC/PNoise. The wrapper defaults tocap_mode="average"for the orbit: a trapezoidal0.5*(C_n+C_{n-1})*dVdiscretization that matches Cadence's commutation feedthrough on the chopper's high-impedance internal nodes. Generic transient/PSS calls still default to the charge-conservative Q-stamp.pmos_chopper_pac(...)is a compatibility wrapper over the genericcircuitopt.pac_solver.pac_solve(...). The generic solver still defaults to the analytic-adjoint HB path, but the chopper wrapper defaults to the time-domain Floquet path (method="pss_time_domain"): it builds the one-period monodromy once in time domain, then solves a small quasi-periodic boundary system per frequency. For PMOS_TFT periodic conversion it retains each device's hiddengate1small-signal state (R_cap,R_cap2,Cgs,Cgd) instead of collapsing to terminal{gm,gds,Cgs,Cgd}at every orbit sample. The periodic conversion linearization uses the Verilog-A-styleC(V)*ddt(V)operator that Spectre PAC folds, not necessarily the transient companion operator used to generate the large-signal orbit. When every PMOS device exposes thegate1network, the gate1-retained PAC linearization is assembled by the compiled Rust kernel (circuitopt_core.PeriodicLinearizationProblem); topologies it does not cover (controlled sources, bordered or vsource-driven systems) return early so the caller continues to the next path. Settime_domain=Falsefor the analytic-adjoint HB comparison path, oranalytic=Falsefor the original finite-difference shooting path. Static PSS orbits automatically reduce to ordinaryac_solve, avoiding PAC transient runs.pmos_chopper_pnoise(...)is a compatibility wrapper over the genericcircuitopt.pnoise_solver.pnoise_solve(...). For chopper verification it defaults to the time-domain Floquet adjoint: solve the sparse periodic adjoint BVP directly, then reuse the existing cyclostationary device/resistor noise fold. This removes the HB-adjoint sideband-truncation error. The harmonic-balance PNoise path remains available withtime_domain=False: sample periodic small-signal G(t)/C(t), FFT to Fourier coefficients, assembleY[kr,kc] = G_{kr-kc} + jω·C_{kr-kc}, solve one adjoint system per baseband frequency, and fold noise to the baseband output. Unlikepmos_chopper_lptv_analysis, neither path needs a Cadence calibration factor.
The PMOS-switch sideband path was initially validated with
pmos_chopper_lptv_analysis paired with Cadence calibration constants. The
native pmos_chopper_pac and pmos_chopper_pnoise now replace those
calibration-dependent paths with first-principles periodic small-signal and
noise solves. The finite-edge transient path has been checked against Spectre
tran. For the D3 chop_tb_d3 slow-corner PSS/PAC/PNoise reference, native
default time-domain PAC is about +0.03%, and native TD PNoise IRN is about
+0.02%. The old HB-K32 PNoise IRN errors across slow/typical/fast were
+1.81% / +1.05% / +0.66%; the TD-adjoint errors are +0.02% / -0.00% / +0.57%.
This closes the earlier false comfort from sideband truncation while remaining
first-principles.
pss_solver.py¶
Solves periodic steady state by shooting on top of the existing transient engine:
pss_solve(sizes, bias, period, topo=..., tgrid=..., inputs=..., node_inputs=...)- Integrates one period with
transient(...)and solvesx(T)-x(0)=0. - Uses the DC operating point as the default seed, with optional stabilization periods before shooting.
- Shooting Jacobian: By default (
analytic_jacobian=True), the first Jacobian is built analytically in one orbit pass: sample the small-signal G(t)/C(t) stamps at each converged trajectory step, form the per-step map A_m = (G_m + C_m/h)^{-1} · (C_m/h), and accumulate the monodromy matrix Φ = ∏ A_m. The shooting Jacobian is then Φ - I — O(1) orbit pass instead ofn_statefinite-difference period runs. Falls back to finite differences on failure. Setanalytic_jacobian=Falseto use the original finite-difference path. - After the first Jacobian build (analytic or FD), the solver reuses it with a
Broyden secant update. This removes repeated Jacobian builds on later shooting
iterations while still recomputing the true one-period residual for every
accepted step. Use
jacobian_reuse=Falseto rebuild every iteration, or setjacobian_rebuild_intervalfor periodic rebuilds. - Returns the one-period trajectory,
x0,x_end, residual vector/norm, convergence flag, iteration history, and performance counters such asshooting_period_runs,shooting_jacobian_evals, andshooting_jacobian_reuses. The history records the Jacobian kind used ("analytic_monodromy"or"finite_difference"). - PMOS chopper wrappers default to the compiled Rust grid transient path
(
fallback_least_squares=False), one stabilization period, and the chopper-onlycap_mode="average"orbit when they build a PSS internally for PAC/PNoise. This preserves the residual/nfail convergence checks while avoiding Python fallback reruns on every period. - Chopper PSS auto-seeding caches the bare-AFE DC seed for identical size/bias/corner inputs. Repeated analyses reuse only the initial guess; the real shooting residual is still recomputed, so convergence accuracy is unchanged.
This PSS orbit can feed the generic pac_solve and pnoise_solve kernels
directly. pmos_chopper_pac / pmos_chopper_pnoise are wrappers that map the
chopper's differential input to input_drive={"vip": 0.5, "vin": -0.5}.
pac_solver.py¶
pac_solve(sizes, bias, freqs, pss_result=..., input_drive=...)- Circuit-generic: it only requires the PSS result to carry
topology,t,nodes,x0,x_end,output, and periodic input-waveform metadata. input_drivemaps small-signal complex amplitudes to transient input keys, e.g.{"vip": 0.5, "vin": -0.5}for differential input or{"vin": 1.0}for single-ended input.- Four performance paths, tried in order:
- LTI fast path — static PSS orbits reduce to ordinary
ac_solve. - Time-domain Floquet PAC (
time_domain=True; chopper wrapper default) — samples periodic G(t)/C(t) and input coupling on a uniform orbit grid, builds the frequency-independent monodromy once, then solves(exp(jωT)I - Ψ)x0 = gper frequency. This avoids HB sideband truncation and the large(2K+1)nconversion matrix. PMOS_TFT devices are expanded with their internalgate1small-signal states during periodic conversion. The all-PMOS gate1 case uses the compiled Rust linearization kernel; unsupported mixed, bordered, or vsource-driven cases returnNoneand continue to the next path. - Analytic-adjoint (generic default,
analytic=True) — samples periodic G(t)/C(t) and input-coupling columns G_in(t)/C_in(t) on the PSS orbit, FFTs to harmonic coefficients G_k/C_k, builds the harmonic-balance conversion matrix Y_HB(f), and reads sideband-0 gain from a single adjoint linear solve per frequency. Cost is O(1) per frequency with zero extra transient runs. Controlled byn_period_samples(time resolution) andmax_sideband(sideband count). - Finite-difference shooting (
analytic=False) — finite-differences the state transition matrix Φ and the complex input forcing around the PSS orbit, then solves(Φ-γI)dx0=-b. Costsn_state+2transient runs per frequency. Cached onpss_resultfor repeated PAC/PNoise calls. - Results include counters such as
pac_period_runs,pac_state_cache_hit,pac_input_cache_hits,pac_td_setup_time_s, andmethod("pss_time_domain","pss_analytic_adjoint", or"pss_fd_shooting"). PAC condition diagnostics are off by default and are enabled only byprofile=True,debug=True, or explicitcompute_condition=True; the diagnostic does an SVD per frequency and does not affect gain/BW/noise.
pnoise_solver.py¶
pnoise_solve(sizes, bias, freqs, pss_result=..., fundamental=...)- Uses generic
Topologydevice/resistor/capacitor stamps. PMOS device noise is sampled along the PSS orbit; resistor thermal noise is folded as a stationary source. - Static PSS orbits use the same LTI
noise_analysispath as normal noise analysis. True LPTV runs cache sampledG(t)/C(t), HB blocks, and identical-frequency adjoint solves onpss_resultwhere the HB path is used. - With
time_domain=True, PNoise replaces the K-truncated HB adjoint solve with a sparse Floquet adjoint BVP (pnoise_time_domain_used=True). This is exact in the conversion sideband index; its remaining numerical error is the time-grid discretization, so default-liken_period_samples < 640is raised to 768. The returnedmethodispss_time_domain_floquet_adjointon this path andpss_harmonic_balance_conversion_matrixon the HB fallback path. - HB adjoint solves support
hb_solver="auto" | "dense" | "sparse" | "iterative". The default keeps small systems on dense BLAS/LAPACK and switches large, very sparse HB matrices to SciPy sparse direct solves. Forcediterativeuses block-Jacobi preconditioned GMRES, with per-harmonic diagonal block LU factors, and falls back to sparse direct if convergence fails. - HB block assembly, scalar
freq × source × sideband²noise folding, OTFT orbit linearization, retainedgate1dynamics, and generic four-terminal silicon G/C stamping run in the compiled Rust core (circuitopt_core) — the sole engine as of v2.0.0 (seecircuitopt/_engine.py). Result flagspnoise_rust_hb_used/pnoise_rust_fold_usedreport whether the Rust fast path applied to a given call. Compiled code mainly helps the matrix-fill and noise-fold loops; HB linear solves are dominated by BLAS/LAPACK, SuperLU, or GMRES rather than Python loop overhead. - If
gainsorpac_resultare not provided, pass the sameinput_driveand the function will call genericpac_solvefor input-referred noise.
transient_solver.py¶
Solves the time-domain response of the topology-defined system using backward Euler (default) or variable-step BDF2/gear2 integration:
transient(sizes, bias, tgrid, vip=None, vin=None, nf=None, V0=None, topo=AFE_TOPO, inputs=None, node_inputs=None, integration_method="be", adaptive=False)- Supports legacy AFE
vip/vininputs and genericinputs={name: waveform}driven throughtopo.transient_inputs. node_inputs={node: input_key}drives a (rail) NODE with a waveform — used by a front-end testbench where the stimulus enters at source nodes and propagates through a passive network, rather than driving device gates directly.current_inputs=[{"p": node_a, "q": node_b, "input": key}]stamps a time-varying ideal current source flowingp -> q; the PMOS chopper helper uses this for charge-injection pulses.cap_mode/cap_mode_idare per-call capacitance-operator overrides. Production paths only supportcharge/id 0 andaverage/id 1.Noneuses the module/environment default (charge), while chopper PSS passes theaverageoperator explicitly for Cadence-matched clock feedthrough. This override affects only the transient/PSS orbit, not PAC/PNoise conversion linearization.adaptive=Trueis an opt-in LTE-controlled gear2 path. It treats the suppliedtgridas the input-sampling grid and[t0, tstop]boundary, then returns a self-chosen non-uniform accepted grid. It is valid only withintegration_method="gear2". Public callers may still passadaptive_reltol,adaptive_vabstol,adaptive_iabstol,adaptive_max_steps, andadaptive_h0; internally these are normalized intoAdaptiveConfigand shared by transient/PSS/chopper paths. PSS additionally usesadaptive_freeze_factorfrom the same config. The adaptive LTE policy constants and Python helpers live incircuitopt/adaptive_config.py; the compiled Rust core (co-core) keeps a matching mirror, with tests checking the two implementations agree. Newton failure rejects now shrink the candidate step, while zero-error accepted steps may grow it.max_step,max_retry_subdivisions,fallback_full_jacobian, andfallback_least_squaresprovide controlled step refinement and bounded fallback solving for switched transient steps.- Includes topology-defined load capacitances (and capacitor elements), plus resistor and ideal-current-source branches.
- Re-evaluates nonlinear capacitances during Newton iterations, and includes the
PMOS
R_cap2source/drain-to-gate leakage branch used by the PDK Verilog-A model. - Supports
signed_devicesfor bidirectional pass switches. The default AFE path keeps the historicalabs(Idc)convention that matches the calibrated DC/AC/noise solvers, while switch devices can keep physical drain-current sign when source/drain voltages reverse. - Uses the DC operating point from
ac_solveas the default initial condition. - Uses the compiled Rust transient Newton kernel (
circuitopt_core, built via_rust_transient.build_otft_transient_problem). The compiled path evaluates PMOS operating points/capacitances, stamps the residual/Jacobian, solves the dense Newton step, and steps substep-to-substep entirely inside Rust; Python owns marshalling, dispatch, and result assembly only (transient_solver.pycarries no per-step numeric loop of its own). - With
profile=True, native BSIM transient runs returnresult["transient_profile"]populated by counters inside the Rust hot loop: total Newton iterations, native BSIM evaluations and batch calls, solver/inserted step counts, failed-step count and expanded-grid indices, and measured solver wall time. Each Newton iteration resolves every MOS terminal tuple and callsco_bsim4::eval_batch_intoonce. Its persistentEvalBatchWorkspacereuses handle and status storage for the whole transient; the solver likewise reuses terminal rows and its I/G/Q/C evaluation slots. Rayon workers write those disjoint solver slots directly, with no temporary batch-result vector or adapter copy. The dedicated pool defaults to at most 10 workers and can be reduced withCIRCUITOPT_BSIM_BATCH_THREADS=1..10. Repeated cached handles are grouped onto one worker and evaluated serially; distinct handles remain parallel, so mutable compact-model state is never entered concurrently. A transient lease or campaign worker owns each such handle for the complete solve, so this hot path calls an internal lock-free evaluator. Public scalar/C ABI entries keep the per-handle mutex, and repeated handles in a batch stay serialized. Stamping remains deterministic. The pool is process-wide, so parallelism belongs to the outermost level: a driver already running solves concurrently (signoff PVT points, SAR conversions and Monte-Carlo trials, corner/PVT slices) has its workers evaluate batches inline and leaves the cores to that level. (SAR exploration no longer runs candidates concurrently — itsworkersgo to each candidate's own conversion sweep, which lands on the "SAR conversions" case above.) This applies only when the outer work is at least as wide as the worker count, matching the compiled campaign's own axis rule, so a single solve keeps the pool.CIRCUITOPT_BSIM_NESTED_POOL=1restores the previous scheduling; results are unaffected either way. Every evaluation clears only the rows and columns of the fixed BSIM matrix frame throughCKTmaxEqNum, which is the whole region the vendor stamps or reads;CIRCUITOPT_BSIM_FULL_FRAME_CLEAR=1forces the old full-frame write. Profiling disabled leaves these counters and the result field absent. The originaleval_batchABI remains as a compatibility wrapper. - A native transient without an explicit
V0first tries the topology's declared DC guesses through the same compiled topology, device wrappers, and leased handles used by the transient. A state is accepted only when Rust DC Newton converges to a finite residual withindc_toland, when requested by the topology, remains inside its voltage box. Difficult points retain the completeac_solvefallback. This avoids constructing the device set twice and avoids an unused 1 Hz small-signal reduction on the normal path. - Accepted-state device-current and charge history reuses the final converged
Newton
Evaluationarray. The convergence branch does not apply its sub-tolerance correction, so those I/G/Q/C values already match the accepted state. A failed step still refreshes the batch after its last state update. Consequently, a successful transient uses one initial-history batch plus one batch per Newton iteration, with no per-step accepted-state replay. - Gear2 uses a guarded variable-step state predictor before each native BSIM
Newton solve. Fixed grids use the two-state linear extrapolation
x[n] + h[n+1]/h[n] * (x[n] - x[n-1]); adaptive Gear2 upgrades to the three-state variable-step quadratic extrapolation once that history exists and falls back to linear after a restart. Prediction rejects step growth above 4x and input-slope discontinuities, keeping clock and DAC edges on the previous accepted-state seed while accelerating smooth settling.gear2_predictor_stepsrecords actual use in the transient profile;CIRCUITOPT_BSIM_GEAR2_PREDICTOR=0disables both modes for regression A/B. - Native BSIM dispatch also implements
adaptive=Trueentirely in Rust. Each trial performs one nonlinear Gear2 solve. A variable-step BDF3-minus-BDF2 defect is formed from the accepted BSIM terminal-charge history and projected through the converged BDF2 Jacobian with one linear solve. That projection reuses the final Newton LU factors and solves only a second right-hand side; it does not stamp or factor the same Jacobian again. A PI controller shrinks on LTE or Newton rejection, caps growth at 2x, suppresses growth immediately after rejection, and restarts conservatively at input-slope breakpoints. LTE is formed over dynamic node voltages; ideal-source MNA branch currents are algebraic multipliers and are deliberately excluded. The returnedtis the accepted non-uniform grid, and profiles addaccepted_steps,rejected_steps,trial_solves,lte_estimates,lte_linear_solves,lte_rejections, andnewton_rejections. - For PSS-style non-robust runs (
fallback_least_squares=Falseandfallback_full_jacobian=False), the compiled grid solver stays in Rust across the full period. Failed substeps are counted as failed intervals and the trajectory continues from the last accepted state, matching the non-throwing transient behavior without rerunning the whole period. - Robust modes (least-squares or full finite-difference Jacobian recovery) are
also compiled-core options passed in as call flags, applied only when
requested — not a separate Python numeric path.
result["rust_grid_solver"]/result["rust_adaptive_solver"]report which compiled path served a given call (the retirednumba_grid_solverresult key no longer exists; tests assert its absence rather than aFalsevalue). - Uses implicit differentiation of the PMOS internal nodes for faster transient Jacobians, with finite-difference fallback.
Gear2/BDF2 integration (integration_method="gear2"): The transient solver
also supports variable-step BDF2 (second-order, stiffly stable). Key properties:
- Uses the stable charge-mode capacitor companion
i_n = (α0·Q_n + α1·Q_{n-1} + α2·Q_{n-2})/h_n, same as BE's(Q_n − Q_{n-1})/hbut with two-step history. A per-call cap-mode override exists for calibrated orbit generation; the PMOS chopper PSS wrapper uses theaverageoperator to match Spectre feedthrough, while generic stiff circuits keep the charge default. - Step-ratio clamp ρ≤2 guarantees zero-stability on non-uniform grids.
- BE self-start on the first step of every interval.
- A compiled Rust gear2 grid solver handles periodic PSS/PAC/PNoise orbits and
raw-transient
max_step,flat_max_step, andmax_retry_subdivisions; the analytic gear2 monodromy (augmented 2n-state) feeds the PSS shooting Jacobian. - The adaptive gear2 path uses a step-doubling LTE estimate and freezes the PSS
grid near convergence before the final fixed-grid orbit/monodromy. The compiled
core (
_solve_adaptive_rust, the sole adaptive driver) accelerates it. - PSS callers that need a reproducible PAC/PNoise orbit can pass
final_tgrid. Adaptive Gear2 then performs stabilization only and never freezes its accepted grid. Shooting, analytic monodromy, fallback stabilization, profiling, and the returned trajectory all use the supplied deterministic grid. Analysis JSON exposes this mode asfinal_n_points; periodic pulse/square edge boundaries are inserted into both grids. - Raw
transient(integration_method="gear2")keeps the BE default opt-in boundary, but whenmax_retry_subdivisionsormax_stepasks for robustness it stays in the compiled Rust gear2 grid. The grid updates rolling two-step BDF2 history after every accepted internal substep and retries failed substeps with fixed2**max_retry_subdivisionsbisection. There is no separate Python numeric fallback intransient_solver.py; a rejected robust step raises instead. - Chopper PSS/PAC/PNoise default to gear2 — PAC baseband errors drop from BE's −2.5% (typ/fast) to <1% across all three corners.
- Raw
transient()still defaults to BE to preserve the established raw transient regression surface and first-order damping behavior. The default BE hard-switched chopper transient hot path is also fully compiled (Rust) now; normal runs no longer enter a Python tail or SciPyleast_squares.
Front-end stimulus (ac_drives)¶
For a testbench, the small-signal AC stimulus can be applied at NODES via Topology.ac_drives (e.g. {"VINP": +0.5, "VINN": -0.5}) instead of at device gates. The drive propagates through the passive front-end network to the (now solved) amplifier-input nodes, and the gain is normalized by the differential stimulus. In noise analysis these drives are treated as AC ground (inputs carry no signal). examples/afe_testbench.py builds the dry-electrode + AC-coupling front end (R_EL∥C_EL, C_AC series, R_AC to VCM) in front of the AFE core and runs AC (bandpass ≈ 0.05 Hz–few-hundred Hz), input-referred noise (including R_EL/R_AC thermal noise), and an in-band transient. Because the AC-coupled input makes the bare AFE DC multistable, the testbench seeds its DC solve from the robust bare-AFE operating point (dc_seed).
explore.py¶
Design-space exploration / optimization driver built on top of the AC and noise
solvers — the "optimization" the project is named for. Given a circuit plus an
explore configuration (design variables with ranges, feasibility constraints,
and one or more objectives), it samples candidates, evaluates each through the
solvers, filters by constraints, and Pareto-selects the trade-off front.
explore(topo, base_sizes, base_bias, nf, cfg, n=, seed=, method=, corner=, workers=)— run a sweep.cornerapplies a process shift (e.g.CORNERS["slow"]) to every evaluation, enabling corner-aware search without modifying the config. Eligible BSIM4 circuits use the topology-drivenCompiledCampaign: arbitrary MOS connectivity, passive elements, independent/controlled sources, and augmented MNA branches are compiled once, then geometry/NF/bias candidates execute in cancellable chunks under one GIL-free Rayon pool. A multistable OTFT AFE uses the compiled path only whenseed_fnsupplies a consistent DC seed; cold AFE exploration stays on the scalar reference to avoid changing physical roots.evaluate(topo, sizes, bias, nf, freqs, band, x0_guess=None, corner=None)— single-candidate solver evaluation, now with optional corner/mismatch argument. Duringexplore, evaluation is AC-first: gain/BW/power/area are computed before noise, failed candidates are rejected immediately, and noise runs only whenirn_uVis required by a surviving candidate's constraints or objectives. The compiled BSIM path prepares DC, operating-point device linearization, the assembled MNA system, and the forward AC response once. A reusablePreparedCampaignthen runs only the noise continuation for survivor indices; it does not repeat DC or forward AC. Native BSIM handles remain call-local and are re-biased at the retained operating point, so no C pointer crosses Python calls or Rayon workers.load_explore_json(path)— read anexploreblock from a full circuit JSON. The topology, device sizes, bias, and optional NF data are all loaded through the same JSON path; legacybuiltin_topologyconfigs are no longer accepted in the exploration layer.- Sampling is
lhs(Latin hypercube) orrandom, with a seeded RNG for repeatability. - Metrics:
gain_dB,bw_Hz,irn_uV,power_uW(the sum of solved source branch voltage-current products), andarea(sum of per-deviceg_area). - A variable's
targetscan drive several keys at once, keeping matched pairs (M7=M8, ...) identical so the AFE's symmetric DC continuation stays on the physical branch. - Results export to CSV and JSONL; a CLI runs
python -m circuitopt.explore <config.json>. - Silicon corner routing (
SKY130_CORNERS/SILICON_CORNERS/apply_silicon_corner) now lives indevice_factory.py;explore.pyimports it from there rather than defining it. add_cli_args(parser)/run_cli(args)are the single source of the CLI argument definitions — both thepython -m circuitopt exploresubcommand and the standalonepython -m circuitopt.exploreentry point call the same two functions, so the two surfaces cannot drift apart (seecli_reference.md).explore_from_dict(data, n=, seed=, method=, corner=, workers=, progress=None, should_stop=None)— the single shared entry point for theexploresubcommand and the service layer'sPOST /api/v1/jobs/explore: parses theexploreblock, binds any siliconmodels, and callsexplore().progress(done, total)/should_stop()are optional hooks threaded straight through toexplore()for a caller that needs live progress or cooperative cancellation (e.g. the service's background-job manager, seeservice/jobs.py); both default toNone, in which case behavior is deterministic. Scalar runs check cancellation before every candidate; compiled runs check between bounded native chunks. On early stop,results["stopped_early"]andresults["summary"]["stopped_early"]areTrueandsummary["evaluated"]records how many candidates actually ran (summary["n"]stays the originally requested count).
Example configs: examples/afe_explore.json and examples/single_stage.json;
both are full circuit JSON files with an explore block.
corners.py¶
Single source of truth for process-corner and robustness work — the pieces that
otherwise get re-derived in every sweep. The CORNERS data itself (global process shifts
typical / slow / fast as pvt0/pbeta0, from the PDK monte.scs sections; e.g.
slow = {"pvt0": -0.2259, "pbeta0": -0.54}) now lives in device_factory.py, the shared
leaf device layer; corners.py imports it from there (from .device_factory import CORNERS)
so existing from circuitopt.corners import CORNERS call sites keep working unchanged. What stays
in corners.py is the mismatch/latch machinery built on top of it:
mismatch_corner(rng, devices, base)— per-device randommvt0/mbeta0on top of a process corner.metrics(...)— one design at one corner →gain_peak_dB,bw_Hz,irn_uV, andlatch_dV(|out+ - out-|at the DC op; large ⇒ the cross-coupled positive feedback has latched).corner_table(...)— metrics across typ/slow/fast.latch_screen(...)— deterministic worst-case latch screen: pushes each symmetric pair ±kσ apart over ALL sign patterns and returns the largest output imbalance. A single fixed kick has false negatives (the latching sign pattern is design-dependent), so the screen scans patterns; cheap enough to use inside a search instead of a full MC. It skips noise because only the DC/AC operating point and latch imbalance are needed.mismatch_mc(...)— per-device mismatch MC at one corner, seeded from the nominal op; returns per-metric arrays, a latched mask, and a summary (latch rate + non-latched mean/std/P5/P95). Each sample is evaluated AC-first; IRN is computed only for non-latched samples included in the final noise statistics. Accepts optionalprogress(i, n, partial)/should_stop()hooks (defaultNone, byte-identical result to the pre-hook code path):progressfires after each sample with a lightweight running summary,should_stopis checked before each sample and, if it returnsTrue, ends the run early with"stopped_early": Trueon both the top level andsummary— the count actually evaluated issummary["n"](this path has no separate "requested n" field; unlikeexplore'ssummary["evaluated"],mismatch_mc'ssummary["n"]always reflects samples actually run).mismatch_mc_from_dict(data, n=, seed=, corner="typical", progress=None, should_stop=None)— the shared entry point for themcsubcommand and the service layer'sPOST /api/v1/jobs/mc; parses the circuit and callsmismatch_mc()so the two surfaces can't drift.
ac_solve / noise_analysis accept the same corner argument (a flat process dict or a
per-device mismatch map). The driver examples/mc_mismatch.py wraps this into a corner
table + 3-corner MC figure.
ML surrogate layer (dataset.py / surrogate.py / surrogate_torch.py / optimize.py)¶
Turns the validated solvers into a full build dataset → train surrogate → optimize → verify loop. The solvers stay the ground truth throughout; the surrogate only accelerates the screening of a large candidate pool.
dataset.py— likeexplore.py's sampling/evaluation but with no constraint or Pareto filtering and always evaluates noise, so every sample (including DC failures) becomes a labeled training row. Writes.jsonl(human-debuggable rows) +.manifest.json(provenance: schema version, solver git commit/dirty flag, topology hash, PDK,modelsbinding, corner, sampling seed/method, variable ranges — so a consumer can reject out-of-domain designs) +.npz(denseX/Ymatrices, NaN where a label is missing) + optional.parquet. Label groups (--labels, opt-in beyond the defaultac_noise):transient(stimulus-agnostic waveform features from the config's validated periodic transient),pss(periodic-steady-state quality + orbit output),pac(baseband conversion gain + PAC-grid −3 dB corner) andpnoise(band-integrated output / input-referred periodic noise — the chopper figures of merit). Thepss/pac/pnoisegroups run one sharedrun_analysis_suitechain per candidate, so the config's validatedanalysessolver settings (time_domain, drive, band, shooting tolerances) apply exactly;pac/pnoiserequire theiranalysesblocks. Design-axis grammar extends beyondDEV.W/.L/.NF/bias:<Cap>.C/<Res>.R(named passive values — structural, rebuilds the circuit per candidate viacandidate_circuit()),periodic.frequency(clock), andpvt0/pbeta0(continuous global process shift — sampling this turns the discrete corner sweep into a continuous-PVT training axis). Likeexplore.py,dataset.pyimports silicon corner routing (SKY130_CORNERS,apply_silicon_corner) fromdevice_factory.pyrather than defining it, and exposes the sameadd_cli_args(parser)/run_cli(args)pair so thepython -m circuitopt datasetsubcommand and the standalonepython -m circuitopt.datasetentry point share one argument definition.surrogate.py—HistGradientBoostingRegressor(optionalscikit-learndependency) trained per label, with automatic log-space fitting for labels spanning multiple decades (e.g. IRN).filter_rows()/ CLI--filter label:lo:hirestricts training to a region of interest (e.g. drop railed/collapsed designs whose extreme labels would dominate a squared-error fit, and which get screened out by a constraint anyway).score()reports median/P95 relative error and R² per label.surrogate_torch.py— a differentiable MLP surrogate (optionaltorchdependency; MPS-capable on Apple Silicon) for gradient-based multi-objective design optimization with constraint penalties, plus a--verifypass back through the solver.optimize.py— the screen-and-verify payoff: predict a large pool (µs/candidate) with the surrogate, take the constrained Pareto front, then re-evaluate the top-K on the actual calibrated solver. Usesdataset.candidate_circuit()/split_variables()so every variable kind (including structural cap/resistor/clock axes) is honored in the verify pass, not just size/bias.
A key operational lesson: no single surrogate is both a
precise region-of-interest model and a good failure-region-aware screener — training
on the operating region (via --filter) gives the tightest metric accuracy but makes the
surrogate blind to railed designs during screening. The screen-and-verify architecture
tolerates this by design: the solver, not the surrogate, has the final word on feasibility.
Silicon PDK and compact-model backends¶
The three silicon PDK adapters expose BSIM4 through the same TransistorModel
interface as the AT4000TG OTFT model. SKY130, FreePDK45, and TSMC28HPC+ all use
the in-process native Berkeley BSIM4 backend for normal DC, AC, noise, transient,
PSS, PAC, and PNoise operation.
compact_models/bsim4/card_cache.py provides their shared immutable-card cache.
The bounded, thread-safe LRU keys complete PDK/model/section/bin bindings,
geometry, NF/multiplicity, temperature, corner, mismatch, source fingerprint,
and adapter-specific instance fields. It caches only PDK/model/instance cards,
never mutable device objects or runtime terminal biases. Per-key single-flight
construction preserves parallel PVT work while preventing duplicate cold-card
elaboration; path/mtime/size fingerprints invalidate entries when a model source
is replaced.
compact_models/bsim4/native.py separately owns a bounded pool of
setup-complete mutable native handles. Each immutable card key may have several
handle slots so matched MOS instances can be evaluated concurrently without
sharing internal-node or limiting history. A solver leases every handle
exclusively for its complete trajectory and returns it afterward; the next
sequential solve can reuse the setup cost and warm history. Active handles are
never evicted. BSIM4_DEVICE_CACHE_SIZE limits the total number of handle slots
per PDK namespace (default 128), not the number of distinct card keys.
Immutable cards precompute their normalized, sorted parameter tuples at
construction, so repeated handle leases do not re-sort the full model card.
This changes only cache-key construction and is shared by SKY130, FreePDK45,
and TSMC28.
The TSMC28 MDAC signoff manifest deliberately sets newton_vtol=3e-8 V only
for its six adaptive residue/code-transition cases. The solver-wide default
remains 1e-8 V; other circuits must opt in after their own trajectory and
signoff comparison. Those six cases also set
bsim_model_bypass_tolerance=3e-9 V, enabling the compact model's bounded
standard bypass during Newton. This option defaults to zero, may not exceed
newton_vtol, and is restored after every exclusive transient evaluation;
scalar analyses and regenerative SAR remain on the exact path.
pdk/sky130/library.py/device.py— loads the bundled geometry-resolved BSIM4.5 cards and registers nativesky130.nmos/sky130.pmos.extract_wselects a reference-width card while the instance keeps its actual geometry. Missing geometries fail clearly rather than launching an external simulator.sky130_model.py— compatibility exports plus the explicitextract_sky130_card()ngspice preparation utility. Normal simulation uses the bundled cards and does not call this extraction path.
A third PDK, FreePDK45, now uses the same native Berkeley BSIM4.5 kernel as
TSMC28HPC+. pdk/freepdk45/library.py parses each flat level-54 VTG card into a
numeric model card; pdk/freepdk45/device.py exposes four-terminal current,
conductance, charge, capacitance, and correlated noise through
freepdk45.nmos / .pmos. The source cards declare version=4.0; the bundled
Berkeley source treats that field as metadata, and regression tests compare
single-device operating points/noise plus a 5T OTA against ngspice.
The native host reduces the complete FreePDK45 internal drain/source, gate, and body-resistance network with a small pivoted linear solve. One Schur factorization serves all four external-terminal right-hand sides instead of refactoring the same internal matrix for every row and column. Four-terminal charge aggregation includes distributed body-junction charge and normalizes PMOS charge signs, so AC capacitance and finite-difference charge derivatives agree for both polarities. DC, AC, noise, transient, PSS, PAC, and PNoise therefore share one in-process compact-model path.
Full evaluations feed
acLoad from the charge and small-signal fields produced by their final
floating-point model load, avoiding a duplicate MODEINITSMSIG load; set
CIRCUITOPT_BSIM_REUSE_SMSIG_LOAD=0 to restore the legacy sequence.
The experimental CIRCUITOPT_BSIM_REUSE_FINAL_LOAD=1 mode also lets a private-node
linearization whose Newton update is below 1 pV supply terminal reduction
directly. It remains opt-in because isolated scalar PDK points can move beyond
the public bit-level golden contract. Independently of that experimental mode,
the host always omits the reload when the solved private-node voltages are
bit-identical to those used by the current load (or no private nodes exist).
The external/internal node layout is cached after setup, so both optimizations
preserve the public bit-level result.
The compiled SAR single-trial path partitions input conversions into exactly
min(workers, inputs) balanced contiguous ranges; this prevents campaign Auto
from selecting its serial axis when ceil-sized chunks happen to produce fewer
ranges than workers. Within one conversion, the full stimulus is rendered once.
Each later bit updates only the newly active trial rows and, when needed, the
previous cleared-bit rows. Code-only sweeps omit terminal history, while detailed
conversions retain the complete accepted state/current/charge trajectory.
The native host also exports a versioned conserved-evaluation ABI, an all-void *
entry point, and a batch evaluator. co_bsim4_set_card applies a whole model or
instance card in one crossing, and the vendor keyword tables are indexed once per
process instead of scanned per parameter; a silicon card names hundreds of
parameters, so the two together are what keeps handle construction off the
critical path. The single-value co_bsim4_set_model / co_bsim4_set_instance
entries remain, and a parity test pins the whole-card path to them.
The fixed-grid BSIM4 transient runs matrix
assembly, Newton iteration, and time stepping inside the compiled Rust core
(co-core calling co-bsim4 in-process); compact_models/bsim4/native.py's
ctypes binding is the Python-facing entry point onto the same compiled library
for op-point/AC/noise device calls. There is no separate Python numeric
implementation in production as of v2.0.0.
pdk/freepdk45/library.py— portable card resolution, strict binding, corner and polarity validation, numeric parsing, and shared immutable-card caching.pdk/freepdk45/device.py— nativeTransistorModeladapter and defaultfreepdk45.*registration.freepdk45_model.py— compatibility exports and the optional historicalfreepdk45_ngspice.*grid aliases.ngspice_char.py/ngspice_device.py/ngspice_transient.py— retained external regression-oracle infrastructure, not the default FreePDK45 runtime.
TSMC28HPC+ native adapter (spice/ / compact_models/bsim4/ / pdk/tsmc28/)¶
The internal HSPICE frontend resolves nested .lib/.include closures, parameters,
expressions, subcircuits, foundry MOS macros, and geometry bins. The TSMC28 library
layer selects the 0.9 V nch_mac / pch_mac core model for each instance and sends
the expanded model and instance parameters to the native Berkeley BSIM4.5 backend.
The native device exposes four-terminal currents, charges, conductance, capacitance,
and correlated noise. DC, AC, noise, transient, PSS, PAC, and PNoise therefore run
without an ngspice subprocess. The old process adapter remains registered only under
the explicit tsmc28hpcp_ngspice.* model names as an independent regression oracle.
The default portable model entry is
PDK/tsmc28hpcp/models/hspice/cln28hpcp_1d8_elk_v1d0_2p2.l, which is Git-ignored.
Resolution priority is TSMC28_MODEL_DIR, TSMC28_PDK_ROOT, that project-local
entry, then PDK_ROOT/tsmc28hpcp. See TSMC28HPC+ Local Adapter.
circuitopt/circuit_loader.py requires every MOS in JSON to carry an explicit
models entry (pdk, model, section, and bin). A mixed OTFT/silicon
(or all-silicon) circuit is therefore just configuration; see
JSON Circuit Description. Two complete fully-differential
OTA design walkthroughs: SKY130 FD-OTA,
FreePDK45 FD-OTA.
Local service layer (service/app.py / jobs.py / serialize.py / cli.py)¶
An optional local FastAPI HTTP layer over the whole solver stack, gated on the
serve extra (pip install -e ".[serve]"). It is a thin adapter — every route hands
a request to the transport-neutral operations in service/operations.py and
carries no numerical logic of its own. Full endpoint reference:
Service API.
app.py—create_app(job_workers=1) -> FastAPIbuilds the/api/v1app:GET health/capabilities,POST validate/solve(synchronous, callingcircuit_from_dict/validate_analysis_cfg/run_analysis_suitedirectly), and thejobs/*background-task routes (POST jobs/explore/jobs/mc,GET jobs/jobs/{id},DELETE jobs/{id},WS jobs/{id}/events) backed byjobs.JobManager. CORS is restricted tolocalhost/127.0.0.1on any port.pydanticrequest models (SolveRequest/ExploreJobRequest/McJobRequest) wrapcircuitas an opaquedict— the circuit schema's single source of truth stayscircuit_from_dict, never re-described here.jobs.py—JobManager/Job: an in-processThreadPoolExecutor-backed background-job table for the two long-running drivers,explore_from_dictandmismatch_mc_from_dict. State machinequeued -> running -> {done, failed, cancelled}; progress is pushed onto a per-jobqueue.Queue(drained by the WebSocket route) and also cached onJob.progressfor polling; cancellation sets athreading.Eventconsumed as theshould_stopcallback passed into the core driver (cooperative — an in-flight candidate/sample always finishes first). Retains at mostMAX_JOBS(50) jobs, evicting the oldest already-terminal one first. Imports nofastapi— pure threading/queue plumbing, unit-testable standalone.serialize.py—to_jsonable()/serialize_results(): the numpy/complex/NaN → strict-JSON conventions shared by every response (both the synchronous endpoints and the job/WebSocket payloads). NaN/±Inf →null,complex→{"re", "im"},numpy.ndarray→ nestedlist,_-prefixed dict keys and callables dropped.cli.py—add_cli_args(parser)/run_cli(args), the single source of theservesubcommand's argument wiring (--host/--port/--reload/--job-workers), shared by thecircuit-opt servesubcommand and the standalonepython -m circuitopt.serviceentry point, mirroring theexplore/datasetsingle-source CLI pattern. Importsfastapi/uvicornlazily insiderun_cli, so importingcircuitopt.service(whichcircuitopt/__main__.pydoes eagerly for subcommand registration) never requires the extra to be installed.
MCP layer (mcp/server.py / workspace.py / cli.py)¶
The optional MCP adapter is gated on pip install -e ".[mcp]" and exposes
stdio plus loopback-only Streamable HTTP transports. It calls the same
service.operations functions as FastAPI, adds workspace-confined relative
paths, compacts long vectors in direct tool responses, and writes complete
artifacts under results/mcp. Exploration, mismatch MC, and signoff are
cancellable JobManager jobs. Full tool and resource reference:
MCP Server.
Quick Example¶
import numpy as np
from circuitopt.ac_solver import ac_solve
from circuitopt.noise_solver import noise_analysis, band_rms
from circuitopt.transient_solver import transient
sizes = {
"M6": (2264, 78),
"M7": (61365, 61),
"M8": (61365, 61),
"M9": (3175, 468),
"M10": (3175, 468),
"M11": (465, 66),
"M12": (894, 85),
"M13": (894, 85),
"M14": (5224, 46),
"M15": (5224, 46),
}
bias = {
"VDD": 40.0,
"VCM": 30.65,
"VB": 9.84,
"VC": 16.0,
}
freqs = np.logspace(-2, 4, 121)
ac = ac_solve(sizes, bias, freqs)
noise = noise_analysis(sizes, bias, freqs)
irn_uv = band_rms(freqs, noise["irn_psd"], 0.05, 100) * 1e6
t = np.linspace(0, 4e-3, 400)
vip = np.where(t >= 0.5e-3, bias["VCM"] + 0.5e-3, bias["VCM"])
vin = np.where(t >= 0.5e-3, bias["VCM"] - 0.5e-3, bias["VCM"])
tran = transient(sizes, bias, t, vip, vin)
JSON Circuit Example¶
New circuits can be loaded from JSON. The field-level format is documented in JSON 电路描述格式.
import numpy as np
from circuitopt.circuit_loader import load_circuit_json
from circuitopt.ac_solver import ac_solve
from circuitopt.transient_solver import transient
spec = load_circuit_json("examples/single_stage.json")
freqs = np.logspace(0, 4, 121)
ac = ac_solve(spec.sizes, spec.bias, freqs, topo=spec.topology, nf=spec.nf)
t = np.linspace(0, 1e-3, 100)
vin = np.full_like(t, spec.bias["VIN"])
tran = transient(spec.sizes, spec.bias, t, topo=spec.topology,
nf=spec.nf, inputs={"vin": vin})
Benchmarks¶
Five benchmarks live under benchmarks/:
# Full-AFE benchmark (ac121 / noise121 / tran200)
python3 -m benchmarks.bench_afe --warm-runs 3
# Single-device PMOS_TFT micro-benchmark (7 hot-path ops × 3 bias regions)
python3 -m benchmarks.bench_model --warm-runs 3
# Chopper analysis benchmark (harmonics / ideal / pmos_static / pmos_lptv / pmos_tran)
python3 -m benchmarks.bench_chopper --warm-runs 3
python3 -m benchmarks.bench_chopper --skip-tran --warm-runs 3
# Periodic PSS / PAC / PNoise benchmark
python3 -m benchmarks.bench_periodic --warm-runs 3
# Batch sweep benchmark (N × AC / AC+noise, explore-layer workload)
python3 -m benchmarks.bench_sweep --n-candidates 200 --warm-runs 3
bench_afe.py reports cold and warm timings for the three canonical full-AFE
workloads. bench_model.py measures individual device operations (DC OP, Idc,
capacitances, noise PSD, Cadence metrics) across saturation, subthreshold, and
linear bias regions. bench_chopper.py covers the five chopper analysis levels
at f_chop=225 Hz — from fast finite-edge harmonic math (~1 ms) through ideal
LPTV folding, PMOS static-phase, quasi-static PMOS sideband folding, and the
heavy hard-switched PMOS chopper transient. bench_sweep.py measures batch
throughput of AC and AC+noise evaluation across randomly perturbed design
candidates, simulating the explore layer's per-candidate workload. As of
v2.0.0 CIRCUIT_ENGINE accepts only rust; there is no other engine left to
compare against (see docs/environment_performance.md for the retired v1.x
numba-vs-interpreted baselines).
The old UI chopper full-flow bottleneck was the portable HB PAC frequency solve:
explicit PSS+PAC(HB)+PNoise (time_domain=False) takes about 25.6 s for
61 frequency points (PSS≈0.35 s, PAC≈24.7 s, PNoise≈0.55 s) and about 48.9 s for
121 points (PSS≈0.44 s, PAC≈47.6 s, PNoise≈0.93 s). The default chopper
time-domain PAC path keeps the PMOS gate1 states and uses the selected engine's
compiled conversion assembly; it takes about 1.4 s for 61 points on the
same PSS orbit. A non-chopper AFE DC+AC+Noise 121-point run
is about 1.8 ms when noise reuses the AC result.
Calibration Status¶
The current solver stack was calibrated against Cadence Spectre 24.1 for the AT4000TG AFE use case. The observed agreement in the original project included:
- Typical and corner AC behavior within approximately 0.01 dB for gain.
- Input-referred noise within a few percent across validated cases.
- Per-device mismatch Monte Carlo mean and standard deviation matching Cadence trends.
- Transient step and sinusoidal response closely matching Cadence
tranbehavior. - PMOS eight-switch chopper transient, using UI locked sizes,
f_chop=225 Hz, switchW/L=5000/30, andrise/fall=20 us, now matches the Spectre finite-edge transient scale with the defaultedge_time/10internal step: last-period output mean about-10.76 mVvs Spectre-10.62 mV, output21.11 mVppvs21.46 mVpp, input common-mode swing5.14 Vppvs5.43 Vpp, andnfail=0. - PMOS eight-switch chopper PSS/PAC/PNoise, using the same UI locked case, is
matched by
pmos_chopper_lptv_analysis(...): gain21.370 dBvs Spectre21.369 dB, bandwidth738.6 Hzvs721.9 Hz, and IRN12.592 uVrmsvs12.591 uVrms. - Native
pmos_chopper_pacandpmos_chopper_pnoise(first-principles, no calibration constants) match the D3chop_tb_d3slow-corner Spectre PSS/PAC/PNoise reference atf_chop=200 Hz: default time-domain PAC is about +0.03%, and TD-adjoint PNoise IRN is about +0.02%. Across slow/typical/fast, the old HB-K32 IRN errors were +1.81% / +1.05% / +0.66%; TD PNoise gives +0.02% / -0.00% / +0.57%. - SC-LPF calibration uses adaptive Gear2 only for stabilization, then completes
shooting and returns the PAC/PNoise orbit on a deterministic 3201-point base
grid augmented with every clock edge. It keeps
cap_mode="average"and PNoise resampling at 512 samples /max_sideband=20. Against the archived Spectre reference, PAC gain, bandwidth, and integrated output noise are within about0.5%,1.2%, and2.3%, respectively. - Final locked design around 22.9 dB gain, 549 Hz bandwidth, and 37 uVrms input-referred noise.
These numbers describe the current AT4000TG validation case. Future PDKs or topologies should be recalibrated against their own simulator references.