Imported from freudenthal/skidl-eda (
skills/design-circuit/SKILL.md). Install upstream withnpx skills add freudenthal/skidl-eda --skill design-circuit. Copyright stays with the author.
design-circuit — iterative circuit design loop (skidl authoring)
You are designing a circuit with skidl + skidl-eda. Work in numbered
iterations (max 5). Keep an append-only design_log.md in the project root;
after every iteration append: iteration number, what changed, generation result,
simulation measurements, PASS/FAIL per criterion, and the next action.
The authoring DSL is skidl (Part / Net / @subcircuit); the loop
orchestration, gates, sim entry, and sourcing come from skidl-eda. The design's
source of truth is the Python file — every downstream step (generation,
simulation, sourcing/BOM) reads it.
Two modes: NEW design vs EDIT existing
- NEW design (no matching
.pyyet, or the user asks for a fresh circuit): run the full loop below starting at Phase 0. - EDIT an existing design (the user asks to change/tweak/fix a circuit that
already has a
*.pyin this project — "make R3 4.7k", "add a bypass cap on VOUT", "raise the cutoff to 10 kHz"): jump to the "Editing an existing design" section near the end. The short version: change the Python source and regenerate — do not hand-edit the generated.kicad_sch; the next regeneration overwrites it.
Phase 0 — SETUP (once)
- Read the project's example
*.pyif one exists — it is a known-good API pattern. Thecanaries/sipm_tia/sipm_tia_skidl.pyin skidl-eda is a faithful reference (single-stage op-amp TIA with sim intent). - Bind KiCad-10 libraries first, every run: call
skidl_eda.setup_kicad10()before building anyPart. Do NOT use the rawlib_search_paths["kicad10"] = ["."] + default_lib_paths()recipe — under a checkout carrying skidl'stests/test_datait silently shadows KiCad-10 symbols with bundled KiCad-6 ones (KiCad-10-only parts vanish). - Run commands with the interpreter that has
skidl_edainstalled. A scaffolded project shipsrun.ps1/run.shpinned to it (./run.sh <file>.py); for in-repo work use<repo>/.venv-skidl314/Scripts/pythondirectly. Plainuv run python <file>.pyonly works if the project has its own pyproject wired to the stack — a fresh project dir has none, so it fails withModuleNotFoundError: skidl_eda. Sanity-check the interpreter once per session:<python> -c "import skidl_eda". - Windows note: run commands with UTF-8 mode, e.g. in bash
PYTHONUTF8=1 <python> ...(emoji prints crash captured output otherwise); the scaffold's run scripts already set it.
Phase 1 — THINK
- Restate the user's request as a spec: topology, inputs, outputs, constraints.
- Derive measurable acceptance criteria — concrete node voltages/currents with tolerances (default ±5 % unless the user specified). Example: "VOUT_3V3 = 3.30 V ± 5 % with VIN_5V = 5.0 V". If the request has nothing measurable, define at minimum: schematic generates, ERC-relevant connectivity is sane, expected component count.
- List every component with its intended KiCad symbol (
Lib,Name) andfootprint=id. For a multi-sheet design (see Phase 3), group the component list by sheet (### Sheet: psu,### Sheet: amp, ...) so the hierarchy is visible in the log. - Write all of this into
design_log.mdunder## Iteration N — plan.
Phase 2 — DISCOVER (symbol/footprint resolution)
- NEVER guess lib ids. Verify each one:
PYTHONUTF8=1 <python> -m skidl_eda.sourcing.find_symbol "<query>"(the skidl-eda interpreter — the scaffold's run script or.venv-skidl314; add--footprintsfor footprints). Common:Device:R,Device:C,Device:LED,Regulator_Linear:AMS1117-3.3. In skidl a symbol isPart("Device", "R")— theLib:Nameid splits into the first twoPart(...)args. - Derived symbols (
(extends ...)) can carry the parent's pin count/pinout. A KiCad symbol defined as(extends "OtherPart")inherits the parent's pins — e.g.Amplifier_Operational:ADA4807-2ARMextends the 8-pinLM2904, but the ADA4807-2ARMZ part is MSOP-10. Before wiring a derived symbol, verify its pin count matches the package you intend (read the.kicad_sym, or use the MCPget_component_pins); a mismatch silently mis-maps the extra pins. - A symbol name is not an MPN.
Part("Diode", "BAT54")fails — the library carries variants (BAT54A/BAT54C/BAT54J…), not a bareBAT54. Use a generic symbol with the MPN invalue(Part("Device", "D_Schottky", value="BAT54")), and read pins by skidl introspection (Part("Device", "D_Schottky", dest="TEMPLATE").pins) — never by regexing the.kicad_sym(an(extends …)symbol keeps its pins in the parent, so a regex finds zero). - If the kicad-sch-api MCP server is connected (see
.mcp.json), you can confirm pin numbering for unfamiliar parts with its tools —get_component_pins,find_pins_by_name,find_pins_by_type. Optional: if the server is not connected, rely onfind_symboland the reference.pyinstead — a missing MCP server must not stop the loop. - SPICE model availability (OPTIONAL — when the design will be simulated and a
part needs a real vendor model). Check whether the KiCad-Spice-Library corpus
has a model for a candidate part with
python -m skidl_eda.sourcing.find_spice_model "<NAME>" --type <kind>— model availability can inform part choice. See Phase 5 for attaching the model. Zeners have no built-in card — for a gate/rail clamp search--type zener(permissive corpus hits exist, e.g.DI_1N4742A) or use a generic diode withSim_Params="BV=<Vz>". - De-risk a vendor driver/controller/IC subckt in an isolated harness FIRST.
Before committing a design to a behavioral driver/controller subckt, build the
smallest isolated harness that drives it with the design's real stimulus
(amplitude, rails) and confirm the outputs actually toggle/regulate.
--verifyis a single-device load + op-point check — it cannot see logic thresholds, UVLO, or an unasserted enable. Worked example (HV LLC resonator E2E): theIR2104half-bridge driver has a ~5 V VCC-independent logic threshold, so a 3.3 V PWM never switched it — caught only by an isolated_drv_test.pyharness, and fixed with a 2N7000 level-shift inverter. When a subckt's node order differs from your symbol's pin numbering, mapSim_Pinsby name withfind_spice_model "<NAME>" --symbol <Lib:Sym>. Budget one iteration for this harness; it is far cheaper than debugging a dead full design. - Sourcing / availability (OPTIONAL — only when the user asks for real parts,
a BOM, or sourcing, or supplies MPNs). Check real stock/price with
skidl_eda.sourcing.check_availability("<query>"). JLCPCB works without any credentials (via the keyless tscircuit JLCSearch mirror, rows taggedjlcpcb:jlcsearch); DigiKey needsDIGIKEY_CLIENT_ID/_SECRETand is skipped otherwise. It never returns fake data and reports askipped: <source> -- <reason>for any source it could not query.- Record a
### Sourcingtable in the iteration-plan block with columns| ref | MPN | source | stock | price | note |. - Honesty rule: if a source was skipped (no credentials / network error), write "not checked — no credentials" in the note; never invent stock or prices. No creds at all → say sourcing was not verified and move on; this must not block the design.
- Attach the chosen part's identity to its
Partas plain KiCad properties via kwargs — the curated sourcing setMPN/Manufacturer/Distributor/DistributorPNpass through by name, e.g.Part("Device", "Q", MPN="2N7000", Manufacturer="onsemi", Distributor="DigiKey"). For any other property usefields={"AnyName": "val"}. All of these become hidden schematic properties and appear as BOM columns (generate()exports MPN/Manufacturer/ Distributor by default; override withbom_fields=); no schema change needed.
- Record a
Phase 3 — WRITE
-
Create/modify
<snake_case_name>.py. The skidl pattern:skidl_eda.setup_kicad10()once at the top of the build.Part("Lib", "Name", ref=..., value=..., footprint=..., **fields)per component (circuit-synth'sComponent(symbol="Lib:Name", ...)becomesPart("Lib", "Name", ...)).- Prefer human-readable value strings (
value="22u","100n","10k") over floats/str(float)— a bare float renders as2.2e-05in the BOM/schematic. (The renderer now engineering-formats bare floats as a backstop, but an exact string is always shown verbatim.) Give refs a trailing number (ref="CO1"not"CO"); an unnumbered ref reads as unannotated (CO?) in KiCad, so the renderer finalizes it toCO1itself. Net("NAME")for connections;part[pin] += netto wire (pin numbers or named pins —u1[3] += ninv,u1["OUT"] += vout).- Power nets: create the net and set
net.drive = POWER(importPOWERfrom skidl) so the renderer emits proper KiCad power symbols forGND/VCC*. - Build inside an explicit circuit so you can hand it to the generator:
from skidl import Circuit, Net, Part, POWER from skidl_eda import setup_kicad10, generate, summarize def build(): setup_kicad10() ckt = Circuit(name="rc_filter") with ckt: # components/nets register to ckt vin = Net("VIN") vout = Net("VOUT") gnd = Net("GND"); gnd.drive = POWER r = Part("Device", "R", ref="R1", value="1k", footprint="Resistor_SMD:R_0603_1608Metric") c = Part("Device", "C", ref="C1", value="160nF", footprint="Capacitor_SMD:C_0603_1608Metric") r[1] += vin; r[2] += vout c[1] += vout; c[2] += gnd return ckt if __name__ == "__main__": result = generate(build(), "RC_Filter", output_dir=".") print(summarize(result)) -
Readable output = hierarchy first, renderer second. A flat sheet with more than ~20 parts is unreadable no matter how it is placed or routed — the structural fix is hierarchy, not the renderer. Decompose the design into functional-block sub-circuits (below); aim for one block ≈ one sheet ≈ ~≤20 parts ≈ one readable page.
-
Multi-sheet / hierarchical designs. Split into sheets when the design has distinct functional blocks (power, MCU, analog front-end, ...), the user asks for it, or it exceeds ~15–20 components. Pattern: write one
@subcircuitfunction per block, and a top builder that creates the shared nets and calls each block, passing the sameNetobjects into the blocks that must connect:from skidl import subcircuit, Net, Part, POWER @subcircuit def psu(vin, v5, gnd): ... # components here land on the psu sheet @subcircuit def amp(v5, gnd, sig_in, sig_out): ... def build(): setup_kicad10() ckt = Circuit(name="main") with ckt: vin, v5 = Net("VIN_9V"), Net("V5") gnd = Net("GND"); gnd.drive = POWER sig_in, sig_out = Net("SIG_IN"), Net("SIG_OUT") psu(vin, v5, gnd) # auto-registered as a child sheet amp(v5, gnd, sig_in, sig_out) # V5/GND shared by object identity return cktGeneration emits one
.kicad_schper@subcircuitplus the root; a net shared between two or more blocks becomes a sheet pin on each (e.g.V5), while power nets (drive = POWER,GND/VCC*) use global power symbols, not pins. A net used inside only one block stays local to that sheet — so to expose a block's I/O, share that net with the top or another block. -
Simulation flattens the hierarchy automatically; measure nodes by net name as usual (
result.get_voltage("V5")) — no special handling needed.
Authoring rules (enforced downstream — follow them to stay green)
| Rule | Why / enforcing gate |
|---|---|
Every Part gets a footprint=. |
A footprint-less part is dropped from the board and counted in missing_refs; generate()'s footprint check warns, and plan_pcb(strict_footprints=True) fails the write. The only legitimate exception is a Simulation_SPICE source (auto-stripped from the board). |
Verify a footprint id exists before using it — Phase 2's find_symbol --footprints; generate() runs check_footprints and warns on any absent id. |
A hallucinated footprint silently vanishes from the board. Verify, don't trust a remembered name: a plain SOIC-8 is Package_SO:SOIC-8_3.9x4.9mm_P1.27mm (not SOIC-8_3.9x4.9mm), and Package_LGA does exist in stock KiCad (the bme280 canary routes on it) — so check the library, never assume a package is missing. |
Decoupling caps use value="100nF" (or "0.1uF") — never "104", "0.1µF" (micro sign), or "100000pF". |
The decap classifier keys on the value string (^(100n|0.1u)), and only a canonical spelling classifies. A mis-spelled value silently loses the decoupling_cap role, so the cap is no longer placed next to its IC's power pin and drops out of the decap-distance checks. 100nF is the safe canonical form. |
A part whose pins you are unsure of gets looked up (Phase 2 introspection / MCP get_component_pins) — never hand-declared via Part(tool=SKIDL, pins=[...]). |
Hand-declaring pins trades a loud "symbol not found" failure for silent ERC noise and mis-mapped nets. The harvest record is 1 symbol substitution across 8 boards — the stock libraries cover what we need; look it up. |
Phase 4 — GENERATE
- Run:
./run.ps1 <name>.py(PowerShell) or./run.sh <name>.py(bash) — the scaffold's run scripts, pinned to the skidl-eda interpreter. In-repo projects:PYTHONUTF8=1 <repo>/.venv-skidl314/Scripts/python <name>.py. (NOTuv run python— no pyproject in a fresh project dir; see Phase 0 note.) skidl_eda.generate(circuit, project_name, output_dir=...)renders with the fork KiCad-10 renderer (netlist + hierarchical.kicad_sch), writes the.kicad_proscaffold that makes the project openable, then runs the gate pipeline (footprint check → ERC + PWR_FLAG autofix → save-crash gate → BOM + PDF) and returns a result dict.print(summarize(result))gives a per-step PASS/WARN/FAIL line; paste it intodesign_log.md.- The top
.kicad_schis named aftertop_name(defaults toproject_name) and lives in<output_dir>/<project_name>/. Point ERC / sim paths at<project_name>/<project_name>.kicad_sch. result["ok"]is the openability contract: generation succeeded AND the save-crash gate did not hard-fail. ERC is report-only (see Phase 4.5).
- The top
- Then verify the output is real, not an empty shell: the emitted
.kicad_schmust contain a(symbolblock per component and a(property "Reference" "<ref>"for each expected reference. A ~1 KB schematic with only a text box means every component silently failed. - Default render path = hierarchy + constructive placement + A* wiring.
generatedefaults torenderer_options={"seed_placement": True, "auto_stub": False, "hierarchical_sheet_pins": True, "power_stubs": True}— constructive seed placement followed by A* routing that draws real wires (not label stubs), the true KiCad hierarchical interconnect, and power symbols pulled off the pin onto short stub wires. Author the design hierarchically (@subcircuit, ~5–15 parts/sheet) so each sheet stays routable; this is the path a new build should try first. Verify it with thedrawing_connectivitygate below. The render is now byte-reproducible (fixed default seed; run it twice and diff to confirm) and handles power + hierarchy structurally:- Power nets never enter the router. Every power net (
drive=POWERor a stock power name) renders as apower:*symbol at each pin; a non-stock rail name (e.g.VBIAS_28V) gets a cloned in-file(power)symbol. Each undriven rail (fed by a header/connector, noPWROUTpin) gets exactly ONEPWR_FLAGproject-wide, sopower_pin_not_drivenis cleared structurally, across sheets — you do NOT need to add manualPWR_FLAGparts, and the ERC autofix finds nothing to do. - Cross-sheet nets connect by name via a
global_labelon each pin (no dangling hierarchical labels / sheet pins). A 2-sheet differential-amp hierarchy now renders ERC 0 /equiv=True. - Split routed nets self-heal. If the router boxes a pin in, a net-aware emission audit drops a unifying name label so the drawing still matches the netlist.
- True KiCad hierarchical sheet pins (default ON,
hierarchical_sheet_pins). Each boundary net gets ahierarchical_labelon the net inside the child sheet, paired with a sheet pin (wired out to a same-named label) on the parent's sheet symbol; a transit net threads through part-less intermediate sheets. ERC-clean and byte-reproducible on the SiPM 8-sheet bench (drawing_connectivitymatches). Setrenderer_options={"hierarchical_sheet_pins": False}to fall back to plain cross-sheetglobal_labels. - Power symbols on stub wires (default ON,
power_stubs). Each power symbol is pulled one grid step off its pin onto a short stub wire (the classic KiCad look, symbols clear of the body). Setrenderer_options= {"power_stubs": False}to place symbols directly on the pin instead. - Constructive relaxation (default ON in deconflict mode,
constructive_relax). The constructive seed + the per-sheet occupancy registry deconflict every power stub and signal stub against each other, so the force-directed refiner is retired on this path — placement is the pin-face constructive arrangement with deterministic spacing, and the render is byte-identical across runs andPYTHONHASHSEEDs (no more dense-sheet placement jitter). On a very dense sheet where the refiner-free placement boxes the router in and splits a net,generateself-heals by re-rendering that project once with the force refiner (constructive_relax=False), so correctness always wins. Passrenderer_options={"constructive_relax": False}to force the refiner path.
- Power nets never enter the router. Every power net (
- Fall back to
auto_stubonly if a dense sheet still won't clear. At very high per-sheet density the A* router may still fail; if ERC won't clear ordrawing_connectivityreportsequiv=False, passrenderer_options={"auto_stub": True}(stubs power/high-fanout nets to labels before routing — a robust label-only path), and/or split a too-dense sheet. drawing_connectivitygate (insidegenerate, report-only): it exports a netlist from the rendered schematic and compares it to the logical.net.result["steps"]["drawing_connectivity"]["equiv"]must beTrue;equiv=Falsemeans the drawing doesn't connect a pin the circuit does (a routing-fallback or placement gap on the wired path) — treat it like an ERC error and route back to Phase 3/4 (fall back toauto_stub, or split into sheets). Passdrawing_must_match=Trueto make it gateresult["ok"].- Multi-unit parts (dual/quad op-amps with a dedicated power unit) render as
one shared reference
U1with(unit N)— connect the power unit'sV+/V-pins like any pin and they place correctly. References may contain_/.(J_PWR), so standardR1/U3naming is recommended but not required.
Error routing table:
| Symptom | Route |
|---|---|
KeyError/FileNotFoundError on a lib id / symbol not found |
Phase 2 — fix that lib id (or you skipped setup_kicad10()) |
| Schematic missing components / tiny file | Read the run log for footprint/part failures; Phase 2 or 3 |
| Python exception in your file | Phase 3 — fix the code |
UnicodeEncodeError |
You forgot PYTHONUTF8=1; rerun |
| Gerber/PCB errors | Ignore — unavailable feature; generate emits no PCB |
Phase 4.5 — ERC (connectivity gate, runs inside generate)
generateruns the ERC gate by default (run_erc_gate=True,erc_autofix= True): it shellskicad-cli sch erc, then applies the net-aware PWR_FLAG autofix — for each net flaggedpower_pin_not_driven(a power pin with no driver, including a real part's power rails) it adds apower:PWR_FLAGwired to the net's real driving pin, iterating a few times with revert-on-regression (it never leaves the schematic worse). The residual report is onresult["steps"]["erc"](errors,warnings,autofixes_applied,non_autofixable_errors) andresult["erc_clean"].- Paste the ERC line from
summarize()intodesign_log.md. Treat remaining errors (non_autofixable_errors) as FAIL → route back to Phase 3 (fix the connection). Warnings likeisolated_pin_labelon an I/O net terminated by a single label are normal — note them, don't chase them. If kicad-cli is absent the gate skips; if kicad-sch-api is absent the autofix is a no-op (ERC still reports). To make remaining ERC errors fail the project, passgenerate(..., erc_must_be_clean=True). - Save-crash gate (also inside
generate, stepsave_gate): KiCad's GUI can segfault-on-save a schematic thatkicad-cliERC/netlist/PDF all load fine. The gate reproduces it headlessly — copy the.kicad_sch,kicad-cli sch upgrade --forcethe copy, and requirerc == 0ANDsize > 0AND it reloads. Asave_gateFAIL is a real corruption bug, not a warning; it makesresult["ok"]false.
Recurring real-part ERC errors you must fix in the Python (the autofix won't)
The PWR_FLAG autofix only handles power_pin_not_driven. The errors below are
wiring decisions on real ICs — the autofix (correctly) won't guess them, so
every real-part design must fix them in the Python source:
- Exposed pads (
EP/EPAD/PADpins) → connect explicitly: GND for regulators/converters, the V− rail for op-amps. Left floating they trippin_not_connected/power_pin_not_driven. - Unused symbol pins (op-amp offset-null 1/5, IC
NC/OSC, spare gates) → mark each as a deliberate no-connect, or they throwpin_not_connectedandpin_not_driven(an Input-typed unused pin trips both). The mechanism:from skidl import NCNetthenpart[pin] += NCNet()for each unused pin — the renderer emits a KiCad(no_connect)flag and ERC goes clean. (A freshNCNet()per pin. The eval grade does not penalize these — intentional no-connects are excluded from the floating/naming checks.) - Open-collector status pins (
PGOOD,/FAULT, …) → pull up to a rail (a resistor), or add a deliberate no-connect with a note. - Passive utility pins with no obvious net (charge-pump
CP/C+/C-, noise-reductionNR,BYP) → bypass cap per datasheet; flag "datasheet-verify" in the design log. - Bonded output+feedback pins (ADA4817-style
FB+OUT, both typed Output) → put the feedback network onFBand the load onOUT; tying them to one net is an ERC output-output conflict even though they're internally bonded. - Multi-voltage programming pins (e.g. TPS7A4701 ANY-OUT bank) → strap per datasheet (grounded in external-FB mode); don't leave floating.
- Multi-unit (dual/quad) parts are placed as one
Partand simulate per-unit automatically — wire both halves on the same part; no need to split a dual into two singles.
Phase 5 — SIMULATE
The skidl.sim layer mirrors circuit-synth's .simulate() API — the SimulationResult
helper methods are vendored verbatim, so measurement code is identical; only the
entry point and the Sim_* attribute spelling differ.
- Entry point:
from skidl.sim import simulatethensim = simulate(circuit). (circuit-synth'scircuit.simulate()→simulate(circuit).) - DC / operating point:
result = sim.operating_point(), read values withresult.get_voltage("NET_NAME")(ngspice node lookup is case-insensitive). An oscillator has no DC operating point —operating_point()raisesNgSpiceCommandError: Command 'run' failedon any self-oscillating design (VCO, relaxation/Royer/multivibrator). To check rails/regulation on such a design, op-point an isolated DC block (e.g. the LDO + load alone), or transient-run the full circuit and tail-average the node. - AC / frequency response: drive the input with a
Part("Simulation_SPICE", "VSIN", ...)source (it carries an AC magnitude of 1 V, so the output node is the transfer function), thenresult = sim.ac_analysis(start_hz, stop_hz, points)and measure withresult.cutoff_frequency("NET")(−3 dB corner),result.passband_gain_db("NET"), andresult.bode("NET")→(freq, magnitude_db, phase_deg). Measure roll-off on the asymptote (10·fc → 100·fc), not fc → 10·fc. - Declaring sources: use KiCad's real
Simulation_SPICEsymbols —Part("Simulation_SPICE", "VDC")for a DC supply,"VSIN"for AC/transient stimulus,"IDC"/"ISIN"for current sources. Pin 1 is+, pin 2 is-. Do NOT useDevice:V/Device:I(not real KiCad symbols). An explicit source overrides the net-name rail heuristic on the nets it drives. That heuristic splits a net name into tokens on every non-alphanumeric and injects a supply if any token is a rail keyword (VIN/VCC/VDD/+5V…): soVINT_RAW(tokens{VINT, RAW}) is not injected, butVIN_SS(tokens{VIN, SS}) is — an underscore-delimitedVINcounts. Two backstops keep an unwanted match from silently breaking a circuit: injection is skipped when the net is already driven by anOUTPUT/PWROUTpin (an op-amp/regulator output — E2E D1), and any injection it does perform now logs at WARNING. Still, avoid naming a signal netV<rail>_*if you don't want a bare rail there. A negativevalueis honored directly (value="-5"gives −5 V) — no pin-swap trick — and an unparseable source value is a loud error, not a silent 1.0. Read a source's current withresult.get_current("V1")by its plain schematic ref. - Transient stimulus: on a
Simulation_SPICEsource, waveform parameters work both ways — as barePartkwargs and as aSim_Params="…"string (kwargs win overSim_Paramswhen both set the same key).VSINreadsamplitude/frequency/offset;VPULSEreadsv1/v2/td/tr/tf/pw/per;VPWLreadspoints. Keep SI suffixes (1k/1m/1u/1n). Both of these are equivalent:Part("Simulation_SPICE","VPULSE", v1="0", v2="12", tr="200n", tf="200n", pw="9.3u", per="10u")andPart("Simulation_SPICE","VPULSE", Sim_Params="v1=0 v2=12 tr=200n tf=200n pw=9.3u per=10u"). (Waveform-looking kwargs on a non-source part are ignored — the sweep is scoped toSimulation_SPICEsources.) Runsim.transient_analysis(step_time, end_time, ...)— the times are the first two positional args (step_s=/end_s=raiseTypeError); an optionaloptions={...}(reltol/abstol/gmin) tunes convergence. UIC/initial conditions: keyword-onlyuse_initial_condition=True(emituic),initial_conditions={"VOUT": 0}(.icnode voltages, by net name),start_time,max_time. Read the sweep back withres.time_array()andres.get_voltage("NET")(there is nores.timeattribute). - Active-device models (diodes/BJTs/MOSFETs): naming a real part in
value(value="1N4148","2N3904","SS14") pulls datasheet-fit parameters when known, else a textbook-generic model; a common package/reel suffix is aliased onto the die model (value="1N4148W"→1N4148,"MMBT3904"→2N3904), sovaluecan match the actual MPN. The tier is recorded insim.model_provenance[ref].tier(datasheet_fit/generic/vendor_lib) and logged, so a generic is never silently passed off as the real part. Diode terminals resolve by the symbol's A/K pin names, not pin order. An unlisted part is a hard error unless you also giveSim_Params, which degrades it to the kind's generic with your overrides. - Op-amps default to an ideal VCVS (infinite gain-bandwidth). For a
bandwidth-/stability-sensitive design (e.g. a TIA with a large source cap) add
Sim_Gbw="1.4G"to opt into a single-pole GBW-limited macromodel — then the Rf·Cf pole, source capacitance, and finite loop bandwidth interact. - Digital / mixed-signal — what simulates and what does NOT (a class rule,
not a parts list). ngspice-in-KiCad runs analog only. Simulatable: any
analog logic you build from transistors/op-amps (a BJT/MOSFET inverter, a
combinational op-amp comparator), and the behavioral primitives below.
NOT simulatable: the corpus's digital models — XSPICE
d_*primitives (e.g. a74HC74whose body isa… d_dff; they are digital event nodes needing adc/dac bridges the sim layer can't inject) and PSpiceU/ugate/IO_*devices (e.g. mostcmos.lib/4000-series; ngspice doesn't implement them).find_spice_modelnow prints asimulatable: no (…class…)line for these (and a--simulatable-onlyfilter), andsimulate()raises a clear class-named error if one is wired in — so you learn before building around it, not after a dead run. Don't reach for a corpus flip-flop/gate; use ↓. - Behavioral logic (DFF / TFF / DLATCH / gates) give a corpus-independent,
ngspice-native digital path — exactly as
Sim_Device="LDO"/"BUCK"give a behavioral power block. PutSim_Device="DFF"(or"TFF","DLATCH","AND"/"OR"/"NAND"/"NOR"/"XOR"/"XNOR"/"NOT") on an ordinary logic symbol. Terminals resolve by pin name (CLK/C,D,Q,~Q,EN); a symbol whose pins are unnamed (many gate symbols are generic~) takes an explicitSim_Pins="3=Y 1=A 2=B"(pin number = role).Sim_Params="vdd=5 tpd=10n"sets the logic swing / threshold (orvih=/vil=) / propagation delay. Levels are analog voltages (HIGH abovevdd/2); flip-flop memory is a switch-held cap (a behavioral master-slave latch — no XSPICE). A ÷2 divider is one DFF with~Q→D; run a seeded stiff transient (transient_analysis(stiff=True, use_initial_condition=True)) so it starts from a defined state. Worked example:canaries/dff_divider/. Limitations: async set/reset and real fan-out/drive-strength are not modeled. - Triggered-breakdown / negative-resistance devices (avalanche transistor,
spark gap, SCR/DIAC). ngspice models none of these natively (Gummel-Poon has
no avalanche), and the usual convergence advice is actively wrong for a
triggered, bistable core. Use
Sim_Device="TRIGSW"(aliases"SPARKGAP"/"DIAC"/"AVSW") — one behavioral smooth-conductance switch. Terminals: powerP(+)/N(−) by pin name (A/anode/C/collector and K/cathode/E/emitter) orSim_Pins="2=P 1=G 3=N"; a third control pinGmakes it externally triggered on the ground-referenced nodeV(G)(avalanche BJT / SCR gate), two terminals make it self-triggered onV(P,N)(spark gap). Rules that cost hours to rediscover — bake them in: (1) reference the trigger to GROUND, not the switch terminals (an emitter-referenced trigger fails when the power− terminal floats on leakage); (2) op-point start, NOTuse_initial_condition(the gated conductance is ≈off at DC, so it has ONE clean DC solution — a latched state node is bistable and can solve stuck-ON); (3) smooth conductance, not an idealsw(answcollapses the timestep in the resonant loop); (4) a reverse catch diode (anode→GND, cathode→node) clamps the L-C negative overshoot; (5) the ns pulse SHAPE is set by the EXTERNAL L-C loop (Ipeak ≈ (Vrail−Vf)/√(Lloop/Cstore)), not the model.Sim_Params="vt=2.5 ron=1.2 width=0.05 rleak=100meg"(optionalvhold=adds a smooth quench). Worked example + multi-pulse repeatability:canaries/trig_pulser/. - Linear regulators / LDOs (
Regulator_Linear:*, or any part withSim_Device="LDO") simulate as a datasheet-parameterized behavioral macromodel. Give itSim_Params="vout=3.3 vdrop=0.3 rser=0.1 iq=2m"(onlyvoutrequired). An LDO with no resolvableVOUTis a hard error. Limitation: no current limit / thermal foldback. - Switching regulators (buck/boost/flyback) need an explicit
Sim_Device="BUCK"/"BOOST"/"FLYBACK"andSim_Params="fsw=500k vout=3.3"; they replace only the IC (your inductor/cap/divider stay real). Run a transient with a fine step (≤ 1/50 of the switching period). These are open-loop computed-duty models (no load-step recovery, non-synchronous, no current limit). For buck loop stability use the averaged model:Sim_Params="... vref=0.8 mode=avg"+.ac_analysis+ voltage injection (res.loop_gain/phase_margin/gain_margin). - Half-bridge / LLC resonant converters are simulatable via a switch-stage
macromodel: put
Sim_Device="HALFBRIDGE"(alias"LLC") on any switcher-shaped symbol (SW/VIN/GND pins) withSim_Params="fsw=100k dt=100n ron=0.1"(onlyfswrequired). It replaces only the two switches — a complementary 50 %-duty S-switch pair with deadtime and built-in antiparallel diodes — so your resonant tank (Lr, Cr), transformer and rectifier stay real parts. It is open-loop: FSW is the control variable, so obtain the DC gain curve by sweeping FSW across.tranruns (there is no duty/VOUT computation). An LLC gain peaks at the parallel resonancefp = 1/(2π√((Lr+Lm)Cr))and crosses unity (M≈1 → Vout ≈ n·Vin/2 − Vf) atfr = 1/(2π√(Lr·Cr)), monotonically decreasing abovefr(buck region). Seecanaries/llc_resonant/. - Bidirectional multi-switch converters (buck-boost / SEPIC). Three
synchronous switch-stage macromodels replace only the switches — your
inductor(s), coupling cap and output cap stay real parts. All are open-loop:
the duty is the control variable you set and sweep (like BUCK/HALFBRIDGE, no
VOUT feedback loop), with built-in antiparallel body diodes so the switches
conduct both ways.
Sim_Paramstakesfsw(required) + duty;dt(deadtime, default 100 n) androndefault as HALFBRIDGE. Each accepts a first-ordervout=…+vin=…convenience that derives the duty (see the caveat under Honest limits — it is an ideal-lossless estimate, so the real rail lands a few % low; sweep duty for the truth). Put theSim_Deviceon a switcher-shaped symbol whose pin names carry the terminals the multi-node resolver keys on:Sim_Device="BUCKBOOST4"— non-inverting 4-switch buck-boost. PinsVIN/SW/SW2/VOUT/GND; your real inductor sits betweenSWandSW2.Sim_Params="fsw=500k dbuck=0.5 dboost=0"— the buck leg (VIN/SW) runs atDBUCK, the boost leg (VOUT/SW2) at1−DBOOST; each unspecified leg defaults to pass-through (DBUCK 1, DBOOST 0), sodbuck=0.5alone is a plain synchronous buck. A leg driven to a saturated endpoint (a pure buck/boost) is a staticRONtie, not a dead switch. Ideal gainVout/Vin = DBUCK/(1−DBOOST). Seed{VOUT:0, SW:0, SW2:0}. Seecanaries/buckboost4/.Sim_Device="INVBUCKBOOST"— inverting 2-switch buck-boost, negative output. PinsVIN/SW/VOUT/GND; your inductor sitsSW→GND.Sim_Params="fsw=500k d=0.5 dt=25n". Ideal gainVout = −Vin·D/(1−D)(negative). Negative-rail convergence:stiff=True+ UIC + seed{VOUT:0, SW:0}(the F4 floating-node tie needs no special handling — the load-tied output isn't DC-floating). Usedt≈25 n: the single switch and rectifier each carry input+output current, so a larger deadtime pulls the high-gain point out of band. Seecanaries/invbuckboost/.Sim_Device="SEPIC"— non-inverting 2-switch SEPIC (SEPIC forward / Zeta reverse), with a coupling cap. PinsVIN/SW/SW2/VOUT/GND, whereSW=node A (main switch) andSW2=node B (rectifier); your two inductors, the coupling cap Cs (A→B) and Cout stay real — the macromodel does not emit Cs (A and B on the same net is a hard error). Ideal gainVout = Vin·D/(1−D), stepping up or down through theD=0.5≈Vincrossover. SEPIC convergence:stiff=True+ UIC + seed{VOUT:0}, and run longer (end≈600·per) so Cs self-biases to ≈Vin — the load-bearing SEPIC invariantV(A)−V(B)≈Vin; usedt≈25 n. Seecanaries/sepic/. Direction of power flow is set purely by where you attach source vs. load (swap them for reverse/boost-up or Zeta), not by any mode-select logic.
- Closed-loop switching controllers (
Sim_Device="CMCONTROLLER"). The large-signal, cycle-accurate closed-loop regime the open-loop macromodels above (duty swept) and the averaged model (small-signal.ac) cannot reach: a behavioral peak-current-mode controller that generates the real gate from feedback and regulates a live rail in.tran— datasheet-style soft-start, cycle-by-cycle current limit, frequency foldback, UVLO, and load-transient recovery (free from the loop). It emits its own switch stage; you supply the real inductor(s), Cout, divider (VOUT→FB→GND) and the VC/ITH compensation network — the same parts the averaged model uses, so the two cross-check. PinsVIN/SW/VOUT/FB/VC/GND(plusSW2=node B for SEPIC/Ćuk). RequiredSim_Params:vref(FB reference, may be negative for an inverting output) andfsw; plustopology=(defaultbuck; alsoboost/sepic/cuk/flyback/forward— forward needs an external transformer- reset winding and defaults
dmax=0.45to holdD<0.5) andri/gm/mcslope(current-sense gain / error-amp gm / slope-comp — datasheet-anchored design inputs).
- Supervisory features, each behind its own param (absent = off):
vsense_max(cycle-by-cycle peak current limit),tss(soft-start ramp),fb_fold/fold_ratio(two-state frequency foldback),uvlo_rise/uvlo_fall,ton_min,dmax, plus error-ampvhigh/vlow+isource/isinkclamps. - Chip profiles:
Sim_Params="chip=LT3757 topology=boost"fills every controller param from a datasheet-anchored preset (explicit params override; an unknown chip warns + falls back). Ships LT3757 and LTC3851. - Ćuk is inverting — a real negative rail:
topology=cukwith a negativevref(e.g.vref=-0.8); the error amp uses the LT3757 FBX negative-output sense to keep the loop stable on the negative rail. - Convergence:
stiff=True+use_initial_condition=True+ seed{VOUT:0}max_time≈per/50(memory caps carryIC=0); a nonzerotsseases the op point. Seecanaries/cmcontroller/. Honest boundary: a behavioral emulation of a controller's headline datasheet specs, not the encrypted vendor silicon — CCM only, no thermal / gate-charge / protection corners beyond the parameterized ones. A boost cannot current-limit a hard short (inductor + rectifier are a direct VIN→VOUT path), so its limit applies under overload; a Ćuk (coupling cap blocks DC) protects a short. Flyback is CCM and needs your real coupled transformer + a primary clamp.
- reset winding and defaults
- Multi-winding transformers.
Transformer_1P_1S(AA/AB, SA/SB),Transformer_1P_2S(adds an independent SC/SD secondary), and the center-tappedTransformer_1P_SS(SA/SC/SB, SC = tap) all simulate as coupled inductors.Sim_Params="lp=100u n=0.5":LP= primary self-inductance (which is the magnetizing inductance Lm for an LLC), each secondary from a turns ration/n2/n3(or explicitls/ls2). Center-tapnis per-half — each half winding isLP·n², both halves sharenunlessn2is given.k(default 0.999) couples every winding pair. All winding ends must be connected. Isolated secondaries need a DC path to the sim GND (a center tap grounds via the tap). - Stiff switching transients (half-bridge/LLC/resonant tanks) need
transient_analysis(..., stiff=True)— it merges a gear/reltol/abstol/gmin/itl4 convergence recipe. Also passuse_initial_condition=True(skip the op point; for a resonant start seedinitial_conditions={"VOUT": 0}) and keepmax_time ≤ per/50(per = 1/fsw) so switch edges aren't aliased. If a switch node still won't converge, add an RC snubber across it or shortenend_time. - UIC is for SELF-OSCILLATING circuits — not for a DRIVEN converter. A driven
converter (external gate/PWM stimulus — a gate-driven half-bridge, not a
self-oscillator) has a valid DC operating point, so start from it (no
use_initial_condition). Vendor behavioral driver/controller subckts (internal.iccaps + ABMVALUE{}nodes) can collapse at t≈0 under a whole-circuituic(Timestep too small; time≈2e-10 … trouble with node "xu1.md1_5") even though the op-point start converges instantly. The surfaced error now carries aHINT:pointing this out. Arrange start-up in the stimulus instead (e.g. start the PWM in the state that pre-charges the bootstrap cap), not with UIC. Reserveuse_initial_conditionfor symmetric self-oscillating cores (Royer/Mazzilli), which genuinely have no DC point (see the self-oscillating bullet below). - Device-level switches (power MOSFETs). For higher fidelity than the
HALFBRIDGE macromodel — real Coss + body-diode reverse conduction, so ZVS is
visible — build the bridge from two MOSFETs. Name a curated power part in
value("IRF540N","IRLZ44N","IRFZ44N") to get a Level-1 fit plus an auto-emitted antiparallel body diode + drain-source Coss;value="powernmos"is a generic power NMOS (conduction only, no companions);Sim_Params="COSS=470p BODY=1"forces companions onto any MOSFET. Size the deadtime so the tank current swings V(sw) rail-to-rail before the opposite gate rises (too short → hard switching). Seecanaries/llc_resonant/llc_devicelevel.py. - ZVS is load-dependent — verify it at the design's real load. The canary's
ZVS-at-0.75·fr is a ~12 W result; heavier load raises the tank Q and pushes
the ZVS boundary toward resonance (a 40 W build of the same tank
hard-switches at ≤0.75·fr and only soft-switches from ~0.9·fr up). Losing ZVS
well below fr at high load is textbook physics, not a tooling failure, and
it is a current-phase (load-Q) effect — deadtime sweeps won't fix it;
move fsw toward fr or redesign the tank. Measure ZVS robustly: sample Vds
just before each gate edge on a settled tail of a fine
.tran(max_time≪ deadtime; skip start-up cycles), and treat rail overshoot as the body-diode-conduction signature of a completed resonant transition — coarse or early-cycle sampling reports phantom hard switching. Reusable snippet:canaries/llc_resonant/zvs_metric.py. - Self-oscillating converters (Royer / Mazzilli ZVS). A cross-coupled
self-oscillating driver (24 V → ~2 kV push-pull resonant step-up) simulates on
real ngspice, but the start-up and tuning are non-obvious — the worked example
is
canaries/royer_zvs/. Four hard-won lessons:- It will not self-start from a clean DC point (the circuit is symmetric).
Seed an asymmetric
.ickick — one gate high, the opposite drain at VBUS — withstiff=True+use_initial_condition=True. Clamp the gate seed tomin(clamp_voltage, VBUS): an unclamped seed above the rail collapses the first timestep at low VBUS (Timestep too small … at t≈0). - Isolated windings need a direct tie to the
GNDnet object — a separate net merged in viagnd += other_netcan leave a degenerate node whose only symptom issingular matrix: check node <net>+ a t≈0 timestep collapse (reads like a model bug; the fix is "tie this winding to node 0"). - Tap-collapse trap. If the drain peak ≪ π·Vin and the center tap sags far below Vin, the drive-winding per-half inductance is too low (magnetizing current collapses the tap). Fix by raising winding L and shrinking Cres at constant L·C — not by the choke.
- Cres stability floor. Shrinking the tank cap below a floor (~10 nF in the
worked example) jumps the oscillator to a parasitic ~MHz mode (small
amplitude). Raise f_osc by lowering winding inductance, not Cres. Always
sanity-check a measured f_osc against the LC estimate; a ×10+ mismatch means
a parasitic mode, not the tank. A real winding DCR (
Sim_Paramsrp=/rs=on the transformer) breaks the ideal-inductor tap degeneracy if you need it.
- It will not self-start from a clean DC point (the circuit is symmetric).
Seed an asymmetric
- Stiff vendor subckts in a high-gain DC loop (HV power MOSFETs, e.g.
IRF740/POWMOS.LIB). A cold
.opcan fail only at mid-range setpoints of a high-gain (Aol≈1e6) DC control loop while the extremes converge — it is a Newton-basin problem, not a Vds boundary, and it surfaces (post-Phase-3) asCommand 'run' failedwith the ngspice tail attached (look forNo convergence). Remedy: UIC transient settle — seed the loop nodes at 0 withuse_initial_condition=True, let the loop charge a small (~10 nF) output cap for ~20 ms, and average the settled tail; fall back to.oponly at the points where it converges (there it is sub-mV exact). Never add a large "conditioning" resistor across the pass device to aid convergence — it injects current and corrupts Vout (measured: 12 V → 13.3 V / 29 V with 10 MΩ / 1 MΩ). - Device-level hard-switched converters with subckt MOSFETs. Expect
Timestep too small … trouble with <ref>:dmos-instanceon hard commutation (now visible in the surfaced error tail). Remedy stack: an RC snubber across the switch node (start ~100 Ω + 680 pF), a gate series resistor with slowed edges (~200 ns), a lower fsw, andstiff=True+ UIC. WhenRload·Cout ≫the runnable transient window, seed the rail near its expected steady state ininitial_conditionsand tail-average (the HV boost repro seeds RAIL≈205 V). And: converged ≠ correct — sanity-check the settled Vout againstVin/(1−D); a big snubber at high fsw burnsC·V²·fand an over-slowed gate can't turn off in a short off-time, both producing plausible-but-wrong rails. - Op-amp / error-amp macromodels ignore the supply rails. An ideal or GBW op-amp is an unbounded VCVS — its output will not clamp at V+/V−. This is useful (an error amp can legitimately drive a 200 V gate node) but surprising if you expect saturation; state which you mean when reasoning about a result.
- Rail-range pre-flight for VENDOR op-amp subckts (they DO saturate). Unlike
the ideal/GBW default above, a real vendor
.subcktop-amp clamps near its rails faithfully — so check the chosen part's output swing at your actual rail before committing to a single-supply topology. Example that cost an E2E iteration:LT1364is not rail-to-rail; on a 3.3 V single supply its output window is only ~1.2–2.1 V, which silently clamps a mid-rail summer and kills a VCO. If the swing is tight, plan a bipolar core (VREF = GND) or pick a rail-to-rail part rated for the supply. The sim is right; the part choice was the bug. - A non-rail-to-rail comparator/op-amp also fails to drive a logic-level FET (not just op-amp saturation). A comparator that swings only ±2 V on ±3.3 rails sits below a logic-level MOSFET's Vgs(th) (~2.1 V for a 2N7000), so the switch barely turns on and the stage silently hangs (an E2E sawtooth VCO charged to one crossing and stopped — no error, just a dead DC-ish state). For a reset/discharge switch or gate drive at low rails, prefer a BJT (needs only ~0.7 V Vbe), and make the comparator a Schmitt (positive feedback) so it latches through the transition.
- Inject a summing-amp offset as a current into the virtual-ground junction,
not as a divider tap feeding the summer input resistor. A resistor-divider
node has real Thévenin impedance; the summer's input resistor pulling toward
virtual ground loads it and the intended offset arrives halved — a
converged-but-wrong result no gate catches (an E2E phase sweep came out as
non-monotonic garbage from a +0.59 V offset that measured +0.32 V). Use a
single resistor from the rail (or a
Simulation_SPICEsource) straight into the−input node; it is immune to loading. - Pin-name lookup returns
Nonesilently on an unnamed pin.part["OUT"]on a symbol whose output pin has an empty name (e.g.Amplifier_Operational:MCP6001Rpin 1) returnsNone, and the subsequentnet += Nonefails with a crypticTypeError … 'NoneType'; theERROR: No pins found using …line just above names the part. Check pin names first ([p.name for p in part.pins]) and wire by pin number when unnamed. transient_analysistimes accept SI strings (step_time="5u",end_time="10ms") as well as float seconds — no need to pre-convert.- Big multi-op-amp transient sweeps take minutes per point — a 14-op-amp oscillator over hundreds of µs is ~20–60 s/point, so a tuning + phase sweep runs 10+ minutes. Run sweeps in the background with a wait-loop rather than blocking the loop; expect it and budget for it.
- Honest remaining limits (say so rather than approximating):
Single-switch forward converters ARE simulatable (Stage 31): open-loop with
either an RCD or a third-winding (tertiary) reset (
canaries/forward/), and closed-loop viaSim_Device="CMCONTROLLER"topology=forward(keepD<0.5for a 1:1 reset —dmaxdefaults to 0.45). The reset is volt-second behavioral: no core remanence/hysteresis (a real core resets to Br, shrinking the usable flux window), the staircase-saturation failure atD>0.5is the 30.2 flux-node knee (not a real B-H loop), and isolation is in-silicon only (secondary shares sim GND). Active-clamp and two-switch forward, push-pull/half-bridge/full-bridge forward-derived topologies, and synchronous secondary rectification remain unmodeled — say so. The half-bridge/LLC and the bidirectional buck-boost/SEPIC switch macromodels are open-loop — duty (and, for multi-leg parts, phase) is the control variable, with no error amp or compensator. Closed-loop buck/boost/SEPIC/Ćuk (soft-start, cycle-by-cycle current limit, frequency foldback, UVLO, load-transient recovery) is available viaSim_Device="CMCONTROLLER"(above) — still a behavioral emulation of datasheet specs, not vendor silicon, CCM-only; single-switch forward (topology=forward,D<0.5, external transformer + reset) closes the loop too; burst-mode / hysteretic / COT control and thermal foldback stay unmodeled. These open-loop macromodels are ideal-switch models, so switching losses are underestimated and a real rail lands a few % below the ideal-gain line (thevout=…convenience is that ideal-lossless estimate — sweep duty to see the true rail). "Bidirectional" means only that the synchronous switches conduct both ways: the direction of power flow is set by source/load placement, there is no automatic forward↔reverse or buck↔boost mode handoff. Real controller-IC vendor subckts (LTC3780 / LT8390 / LM5175-class) are ≥4-node parts → they stayuntestable-generic(the IR2104 lesson); we simulate the power stage, not the controller. - Simulation-only model controls (
Sim_*, asPartkwargs):Sim_Enable="0"excludes a part from simulation (symbol/footprint stay — use it for connectors/test points);Sim_Params="bf=250 vaf=80"overrides model params;Sim_Library="path.lib"+Sim_Name="MODEL"(+ optionalSim_Pins="1=out 2=inp 3=inn") attaches an external vendor.lib/.subckt. (The dotted KiCadSim.Enablespelling becomes underscoreSim_Enableon a skidlPart; the adapter reads either.) - Vendor PSpice/LTspice dialect: most vendor
.libfiles use idioms ngspice rejects by default (PARAMS:,VALUE={IF(...)}). Run them withsimulate(circuit, compat="psa").operating_point()(psa= PSpice + whole-netlist), or putSim_Compat="psa"on the part with theSim_Library. Encrypted vendor models (.enc) can't be used by ngspice. (KiCad's bundled ngspice codemodels — incl.spice2poly.cm, needed for thePOLY(n)sources in most vendor op-amp/IC macromodels — are now loaded automatically.) Benign noise:unrecognized parameter (iave) - ignored/(vpk) - ignoredon corpus diode.modelcards is harmless PSpice-dialect chatter — ngspice drops the PSpice-only params and the model still loads. Not a load failure. - Vendor model library (KiCad-Spice-Library, ~50k models). When you need a
real part whose model isn't a built-in
datasheet_fitcard, search the corpus instead of guessing params:python -m skidl_eda.sourcing.find_spice_model <NAME> [--type diode|bjt|mosfet|jfet|opamp] --verify. It prints a block — the.lib/.subcktname, file, license tier, the recovered subckt node order (so you never misorder subckt pins), and theSim_*kwargs;--verifyconfirms ngspice loads it. A bare.modelblock is paste-ready; a.subcktblock is NOT — itsSim_Pins="<pinN>=<node>"left-hand<pinN>are placeholders for YOUR symbol's pin numbers (replace them; keep the right-hand node values verbatim). A wrongSim_Pinsnow raises a clear Python error naming your symbol's pins and the subckt's nodes (no more cryptic ngspice "Too few parameters"). Worked example — the LT1364 subckt is.subckt LT1364 3 2 7 4 6(+in −in V+ V− out). On theAmplifier_Operational:TL071host symbol (whose pins are 3=+in, 2=−in, 7=V+, 4=V−, 6=out) the map is the identitySim_Pins="3=3 2=2 7=7 4=4 6=6"— left = your symbol's pin number, right = the subckt node in that same slot. The map is the identity ONLY when your symbol's pin numbers happen to coincide with the subckt's node order — never assume it; read both and map role-by-role. A non-identity case:LMV7219nodes are22 6 1 2 18, so on a 5-pin comparator symbol the map isSim_Pins="<+in>=22 <−in>=6 <V+>=1 <V−>=2 <out>=18"(fill the left with your symbol's pin numbers). Two ways to use a hit:- Auto-resolve (simplest): set
SKIDL_SPICE_LIB_PATHto the corpusModelsdir (once), then just name the part invalue(orSim_Name). Bare.modelparts (most diodes/BJTs/MOSFETs) resolve with no pin mapping; a.subckt(op-amps, ICs) also needsSim_Pins(orSim_Prefer="library"). A curateddatasheet_fitcard always wins over the corpus unless you setSim_Prefer="library". The tier is recorded asvendor_lib/ sourcelibrary_indexinsim.model_provenance[ref]. - Explicit (no env var): paste the
Sim_Library="<abs path>"+Sim_Name="<NAME>"(+Sim_Pins) block the CLI emits. Prefer auto-resolve for anything you commit — a hardcoded absoluteSim_Librarypath is a different-checkout landmine (the shippedfunc_gen.pyonce pinned a deadcirc-synth/…path and failed to simulate in its own repo). A stale absolute path now WARNS and falls back to corpus auto-resolve by name rather than hard-failing, but the portable form isvalue="<NAME>"(drop the path). Always keepSim_Compat="psa"for corpus models. Corpus models are real but unvetted — prefer a built-indatasheet_fitwhen one exists, and treat alibrary_indexprovenance as "vendor model, self-verify".--verify's "LOADS + converges" is a single-device op-point check only — it does not promise transient robustness with several instances in a feedback loop (a CMOS-input macromodel likeLMC6482passes verify yet timestep-collapses with 4 instances in an oscillator core). The CLI now prints a curatedreliability:line for models real runs have exercised — heed it over the license tier when choosing a part; for an oscillator/loop core prefer a bipolar-input op-amp (e.g.LT1364). The license tier is advisory metadata only — avendor_restrictedmodel still loads and simulates normally; you own redistribution-terms compliance (only--into-storegates on it). No built-in zener card exists — for a zener clamp, searchfind_spice_model "<part>" --type zener(the corpus has permissive hits, e.g.DI_1N4742A), or fall back to a generic diode withSim_Params="BV=<Vz>"and declare it generic. If the corpus is present butSKIDL_SPICE_LIB_PATHis unset the CLI now prints a one-line reminder thatvalue="<NAME>"auto-resolve won't fire until you set it. If you obtain the corpus first:python -m skidl_eda.sourcing.find_spice_model --help(orskidl_eda.sourcing.spice_library.ensure_library(install=True)) prints the one-linegit clone. Optional gated check:generate(..., verify_models=True)smoke-tests every corpus-resolved part and reports undermodel_verification.
- Auto-resolve (simplest): set
- Keeping a part out of the BOM:
Sim_Enable="0"is not a BOM control. For a model-only passive with no physical part (e.g. a device's internal terminal capacitance you add just for the sim), passPart(..., in_bom=False)so the BOM omits it.Simulation_SPICE:*stimulus symbols are never BOM parts. - On Windows the ngspice DLL bundled with KiCad is auto-configured — no separate ngspice install needed (loads on Python 3.13 and 3.14).
- Save a plot so the log is visual:
result.save_bode_plot(path, node)(path first, then the node name, e.g.result.save_bode_plot("sim_plots/iter1_bode.png", "VOUT")),result.save_transient_plot(...),result.save_dc_transfer_plot(...)under asim_plots/dir. They return the path, orNoneif plotting is unavailable — ifNone, skip the embed, don't fail the loop. - If simulation errors out or the backend is unavailable: fall back to STATIC
verification — recompute expected values by hand (Ohm's law, divider ratios),
confirm net connectivity in the
.kicad_sch, and mark the iteration "not simulation-verified" indesign_log.md. Never fabricate measurements.
Phase 6 — EXAMINE & DECIDE
- Compare each measurement to its criterion → PASS/FAIL table in
design_log.md. - Validate EVERY functional block against its own expected transfer, not just the headline criteria. A telemetry/monitor/auxiliary block that merely "converges" in the sim can still be railing or oscillating (HV LLC D1: an HV-monitor buffer shipped swinging ~9 Vpp instead of a ~1.3 Vpp replica because its bias divider was unbypassed). Add one line of measurement per block (expected vs measured) to the PASS/FAIL table — a block that isn't in the headline criteria still has to do what it was drawn to do.
- To eyeball the drawing in-loop, pass
generate(..., sheet_images=True)andReadthe exported PNGs under<project>/sheet_images/(SVGs if no PNG converter is installed). Readability stays a human call, but gross placement/routing defects are visible to the agent this way. - Embed the plot(s) you saved in Phase 5: after the PASS/FAIL table, add one
markdown image per plot —
— followed by a one-line reading. If a plot save retu
Truncated - read the full file at https://github.com/freudenthal/skidl-eda/blob/e933be492dd0bd4492251285cbcedbd30b863ec1/skills/design-circuit/SKILL.md.