Developer Handoff Guide¶
Documentation Home | Core Solver Overview
This guide is the shortest route from “the project runs” to “I can change it without breaking another backend.”
Repository Map¶
| Path | Responsibility |
|---|---|
circuitopt/circuit_loader.py |
JSON loading and CircuitSpec construction |
circuitopt/topology.py |
Circuit graph and element definitions |
circuitopt/device_model.py |
Device-model interface and PDK registry |
circuitopt/device_factory.py |
Per-device model construction and binding |
circuitopt/dc_solver.py |
Nonlinear operating point |
circuitopt/ac_solver.py / ac_mna.py |
Small-signal MNA |
circuitopt/noise_solver.py |
Stationary noise |
circuitopt/transient_solver.py |
Native transient dispatch and integration |
circuitopt/pss_solver.py |
Shooting periodic steady state |
circuitopt/pac_solver.py |
Periodic small-signal conversion |
circuitopt/pnoise_solver.py |
Cyclostationary noise |
circuitopt/spice/ |
HSPICE library parser and elaborator |
circuitopt/compact_models/bsim4/ |
Native BSIM4 ABI, shared card cache, and implementation |
circuitopt/pdk/{freepdk45,sky130,tsmc28}/ |
PDK-specific model resolution and native device adapters |
circuitopt/ngspice_*.py |
Explicit external ngspice characterization and oracle paths |
circuitopt/dataset.py |
Dataset generation and provenance |
circuitopt/surrogate*.py / optimize.py |
Surrogate training and optimization |
circuitopt/service/ |
Optional local HTTP layer |
rust/crates/ |
Compiled core (co-core/co-bsim4/co-spice/co-pdk/co-py) — the sole compute engine as of v2.0.0; see Rust Core below |
examples/ |
Runnable circuits and scripts |
experiments/ |
Longer campaigns and comparison drivers |
tests/ |
Unit, regression, and backend tests |
calibration/ |
Archived Cadence/Spectre reference data |
Development Setup¶
Optional model toolchains are not required for the core test suite. Tests that need unavailable PDK payloads should skip with an actionable reason.
Test Strategy¶
Start with the narrowest relevant tests:
pytest -q tests/test_json_circuit.py
pytest -q tests/test_cli_subcommands.py
pytest -q tests/spice tests/test_tsmc28.py tests/test_tsmc28_5t_ota.py
Then run the full suite:
pytest -q runs everything — no marker is excluded. Each test that needs an
external payload self-skips without it: normal PDK tests exercise the
in-process C BSIM path and pass even when NGSPICE_BIN points to a nonexistent
executable, and the ngspice comparison workflows skip when no ngspice binary or
local PDK cards are present. On a fully provisioned machine the whole suite is
about three and a half minutes.
Two markers remain, for selecting a subset rather than skipping one:
pytest -q -m ngspice_oracle # ngspice comparison workflows (~81 s)
pytest -q -m heavy_e2e # complete SAR/ADC silicon conversions (~23 s)
Run those two by name when changing compact-model equations, card parsing, an
oracle adapter, the SAR/ADC workflow, the transient solver, or the native BSIM4
backend. The heavy_e2e inventory lives in tests/conftest.py.
Both were once excluded from the default run, back when the heavy conversions cost ~22 min. The compiled kernels have since taken them to the times above, so the exclusion bought nothing — and while it stood, two of these tests broke and stayed broken for days, because nothing (CI included) ran them. Keep them in the default gate.
Documentation:
Diagrams are ``mermaid fences, registered as a superfence inmkdocs.yml.
Material renders them by fetchingmermaidfrom unpkg at page-view time, so a
diagram is blank in a browser with no network —mkdocs build --strictcannot
catch a syntax error in one. Check a new or edited diagram in a browser, or parse
it offline withnpx -p mermaid -p jsdom node -e ...againstmermaid.parse`.
Use git diff --check before committing.
Rust Core¶
The rust/ workspace hosts the compiled core as five crates:
| Crate | Responsibility |
|---|---|
co-core |
Device, MNA, LTI, and transient solver kernels; the OTFT model and the periodic (PSS/PAC/PNoise) assembly |
co-bsim4 |
Berkeley BSIM4.5 host |
co-spice |
HSPICE parameter expression engine (lexer/parser/evaluator) |
co-pdk |
FreePDK45 / SKY130 / TSMC28 PDK compilers (numeric model cards) |
co-py |
The circuitopt_core PyO3 extension joining the above to Python |
co-bsim4 compiles the unmodified vendored Berkeley C (the same translation
units native.py builds, minus host.c) at build time via the cc crate and
reimplements the host.c adapter layer in Rust (parameter binding,
internal-node reduction, terminal I/G/Q/C extraction, noise combination);
bindgen derives the shared struct layouts so the port keeps an identical ABI
with the compiled C. As of v2.0.0 this core is the sole engine: OTFT/BSIM4
transient, AC/noise MNA, the periodic HB/PAC/PNoise assembly, PDK/HSPICE
compilation, and the no-GIL design-space campaign all run through it. SciPy
sparse/FFT orchestration and the DC basin/root-selection control plane remain
in Python.
The coarse PyO3 entry points accept read-only, C-contiguous NumPy arrays for frequency grids, source waveforms, states, and device grids. Rust borrows those buffers while the GIL is released and returns NumPy-owned matrices/waveforms; non-contiguous views are rejected instead of silently copied or reinterpreted. Topology dictionaries are converted once when an immutable problem object is built. Keep this boundary when adding a solver path: do not introduce a per-device or per-time-step Python callback.
Toolchain: stable Rust from rustup with the rustfmt and clippy components,
plus maturin for the Python bridge. Building co-bsim4 also needs a C
compiler (clang/gcc) and libclang for bindgen.
cd rust
cargo fmt --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace
# Build + install circuitopt_core into the active venv for local testing
maturin develop --release -m crates/co-py/Cargo.toml
Engine selection: as of v2.0.0 rust is the only compute engine. The
--engine flag and CIRCUIT_ENGINE environment variable are retained (§7
compatibility contract) but accept only rust; the former numba (JIT) and
python (pure-Python) engines — and the --no-numba / CIRCUIT_USE_NUMBA
switches — were removed and now raise a clear error that points at the
CHANGELOG. circuitopt.current_engine() reports the active engine (always
rust).
The pure-Python _impl kernels (numba_kernels.py) were removed in R7. The
OTFT root-selection recovery they powered lives in the compiled core as
OtftModel(..., reference=True), selected by
pmos_tft_model.otft_reference_mode; the frozen golden corpus
(tests/golden/engine_parity) is the reference oracle (D4).
CI lints the workspace (cargo fmt + clippy), installs circuitopt_core in
the test matrix so the solvers can run, and the release workflow builds and
publishes both distributions together (the circuit-optimization sdist/wheel
and per-OS circuitopt_core wheels). The per-OS wheels cover Linux, macOS, and
— starting with this release, CI-built as the first Windows build — Windows
(win_amd64, abi3-py310); the test matrix and the wheel matrix both gained a
windows-latest leg. That leg is provisional (continue-on-error) until a green
Windows runner confirms the MSVC build of the vendored BSIM4.5 C, which is wired
up by an out-of-vendor config shim in rust/crates/co-bsim4/build.rs (the vendor
tree itself is unchanged); promote it to a hard gate by removing the
experimental / continue-on-error markers.
BSIM4.5 backend selector¶
The native BSIM4.5 compact model still reads a backend selector,
CIRCUIT_BSIM4_BACKEND, on every evaluation (never baked at import,
mirroring ngspice_chain_enabled) — but as of v2.0.0 it mirrors
CIRCUIT_ENGINE's lockdown: rust (the co-bsim4 port, reached by loading
the compiled circuitopt_core extension and binding the identical
co_bsim4_* C ABI) is the only value, defaulted when the variable is unset.
CIRCUIT_BSIM4_BACKEND=cc — the v1.x path where native.py compiled the
vendored C at runtime with the system compiler and called it through
ctypes — is gone; setting it now raises a clear Bsim4NativeError
pointing at the v2.0.0 CHANGELOG entry, and any other value is likewise
rejected. If the compiled extension itself is missing, the error names the
build command instead:
maturin develop --release -m rust/crates/co-py/Cargo.toml
python -m pytest tests/compact_models/bsim4
Version Management¶
pyproject.toml is the canonical source for the project version. Do not edit
the frontend, npm lockfile, or Tauri versions by hand.
# Show or verify the current version
python tools/version.py show
python tools/version.py check
# Set one version everywhere
python tools/version.py set 1.4.0
# Prepare a release: set all versions and archive Unreleased changelog entries
python tools/version.py release 1.4.0
set synchronizes pyproject.toml, frontend/package.json,
frontend/package-lock.json, frontend/src-tauri/Cargo.toml,
frontend/src-tauri/tauri.conf.json, and rust/Cargo.toml (the
[workspace.package] version every Rust member crate inherits). release
also creates the dated changelog heading and comparison links. CI rejects version drift, and the
release workflow rejects a tag that does not match the canonical version.
Change Boundaries¶
Adding a JSON field¶
- Update
schemas/circuit.schema.json. - Update
circuitopt/circuit_loader.py. - Thread the field through
CircuitSpecor the relevant analysis configuration. - Add schema, loader, and behavior tests.
- Update both JSON format documents.
Adding an analysis option¶
- Define and validate it in
analysis_options.py. - Route it through
analysis_dispatch.py. - Add solver-level and CLI-level tests.
- Update the CLI and JSON references.
Adding a PDK¶
- Keep process-specific parsing and file resolution outside the numerical solver modules.
- Implement the
TransistorModelcontract and register model keys throughregister_pdk. - State whether the backend exposes terminal conductance, charge, capacitance, and correlated noise.
- Define corner normalization and fail on unknown corners.
- Add a small circuit benchmark before an OTA-scale benchmark.
- Document required local files without embedding machine paths or licensed parameters.
Changing a Solver¶
Check every backend capability path touched by the change:
- built-in AT4000TG;
- native SKY130 BSIM4;
- native FreePDK45 BSIM4;
- native TSMC28 BSIM4;
- explicit ngspice oracle helpers.
- Rust core (
co-core/co-bsim4). The production numerics live in Rust (engine=rust). A change to the OTFT scalar equations must keep the production and reference modes ofco-core::otftconsistent (the reference mode is the root-selection recovery oracle, D4), and every kernel change must be re-validated against the frozen golden corpus:python tools/freeze_engine_golden.py verify. Rebuild the core after Rust edits:maturin develop --release -m rust/crates/co-py/Cargo.toml.
A solver optimization must preserve numerical behavior within an explicit tolerance. Do not use benchmark speed as a substitute for a regression check.
Result and Cache Hygiene¶
- PDK payloads, generated parameter cards, virtual environments, and caches are local artifacts.
results/contains experiment outputs, not immutable truth. A design document must identify the exact CSV or fixture it summarizes.- Avoid writing foundry model parameters into logs, docs, fixtures, or committed caches.
- Keep path resolution in
circuitopt/toolchain.py; do not add per-machine absolute paths to examples.
Documentation Maintenance¶
The maintained public entry points are:
docs/README.mdanddocs/README_zh.md;- getting-started guides;
- CLI reference;
- JSON format;
- PDK support matrix;
- architecture overview;
- service API;
- TSMC adapter guide.
Design records and dated performance notes must carry a visible status. Completed implementation plans and speculative roadmaps should live in issue tracking or version history, not in the formal documentation navigation.
Release Checks¶
Before a release:
- Run
python tools/version.py release X.Y.Z, thenpython tools/version.py check --tag vX.Y.Z. - Run the full tests and lint.
- Build the documentation with
mkdocs build --strict. - Re-run representative passive, AT4000TG, and available silicon smoke tests.
- Verify that ignored PDK payloads are not staged.
- Mark partial experiment campaigns as partial.