A Python framework for modelling real electronic circuits with physical fidelity.
Project description
wirebench
Describe real electronic circuits in Python. The framework won't let a design that wouldn't physically work compile — so when your tests are green, the breadboard build matches.
KiCad's ERC catches defective wiring after you've drawn the schematic. wirebench prevents you from constructing the wrong design in the first place — no burnt CMOS outputs from two drivers fighting on one net, no half-evenings tracing a regulator that never had a ground wired, no second order at Mouser because the cable housings don't fit the header you placed. Wire two chip outputs together and wire() raises. Leave a regulator's input floating and the Circuit subclass refuses to instantiate. Try to mate a male pin header to a JST plug and mate() says no. The defects ERC would flag after you've drawn the schematic are flagged before the Python design even imports cleanly.
If you've programmed an Arduino but felt like wiring the surrounding circuit is a black art — every project a hunt for the magic combination of pull-ups, current-limiting resistors, decoupling caps, and "did I just wire the LED backwards?" — wirebench is where every line of code maps to a physical operation. You write Python that says "a 330 Ω resistor in series with a red LED between VCC and GND." The wires in the code are the wires you'll need on the breadboard, and the design either constructs cleanly or it doesn't — there's no middle state where pytest passes but the bench build smokes.
Hello, world
from wirebench import Resistor, LED, Rail, Circuit, wire, export
class HelloLED(Circuit):
def __init__(self) -> None:
self.vcc = Rail(True) # 5 V supply rail
self.gnd = Rail(False) # 0 V ground
self.r1 = Resistor(330, refdes_number=1) # R1: 330 Ω current limiter
self.d1 = LED('red', refdes_number=1) # D1: red LED
wire(self.vcc.out, self.r1.t1) # VCC → R1 pin 1
wire(self.r1.t2, self.d1.anode) # R1 pin 2 → LED anode
wire(self.d1.cathode, self.gnd.out) # LED cathode → GND
super().__init__()
design = HelloLED()
export(design, 'bom', 'hello.bom.csv')
export(design, 'kicad', 'hello.net')
export(design, 'spice', 'hello.cir')
export(design, 'mermaid', 'hello.mmd')
This is a buildable circuit. The wire-list in the Python file maps one-for-one to the jumpers on your breadboard. Drop the BOM into Mouser or Digikey, order two parts, and you have everything you need.
Components are stored as self.<name> attributes so the framework can auto-collect them — super().__init__() walks self.__dict__ and picks up every Resistor, LED, Chip, and Rail you've placed. The two-stage pattern (declare parts → wire them → finalise with super().__init__()) reads like a bench-style buildout: take parts off the shelf, wire them together, close the assembly.
What comes out:
| Format | Extension | What it's for |
|---|---|---|
| BOM CSV | .bom.csv |
Paste into a parts cart at Mouser / Digikey / Tayda |
| KiCad netlist | .net |
Import into KiCad's Pcbnew to lay out a PCB |
| SPICE deck | .cir |
Simulate in ngspice or LTspice before building |
| Mermaid | .mmd |
Embed in a README to document what you built |
| Graphviz DOT | .dot |
Render to SVG / PNG with dot -Tsvg |
| Yosys JSON | .yosys.json |
Render to a browser-friendly schematic via netlistsvg |
| Assembly Guide | .md |
Read at the bench — recipe-style breadboard build instructions |
Every demo in demos/ ships with all seven exports pre-generated in its docs/ subfolder — open any *.svg to see the rendered schematic, or any *.md to read the bench-assembly guide.
What it prevents
Three categories of real-world harm wirebench rules out before the design ever reaches a breadboard — each caught in milliseconds, each with the kind of error message that names the offending part and tells you what to fix.
Burnt outputs
u1 = SN74HC04(refdes_number=1)
wire(u1.y_1, u1.y_2)
# ShortCircuitError: wire() has multiple drivers ('y_1', 'y_2') — short circuit
Two CMOS outputs fighting on the same net will burn out one or both. You don't need to know which — wirebench knows, and wire() raises before the call returns. The chip never gets the chance to suffer the experiment.
A wasted parts order
mate(Header2xNMale(...), JSTPHCableHousing(...))
# IncompatibleMateError: Header2xNMale mates with Header2xNFemale, not JSTPHCableHousing
Pitch, pin count, and gender all checked at mate() time. You don't put through a JST cable order, wait five days, and discover the housings don't fit the header on your board — the wrong combination never gets past the Python.
Hours of fault-tracing
class CrossedRails(Circuit):
def __init__(self):
self.vcc1 = Rail(True)
self.vcc2 = Rail(True)
self.r1 = Resistor(330, refdes_number=1)
wire(self.vcc1.out, self.r1.t1)
wire(self.vcc2.out, self.r1.t1) # legal in isolation
super().__init__()
# ShortCircuitError: Short circuit on logical net — multiple drivers: 'Rail.out', 'Rail.out'
Each wire() call is fine in isolation — the contention only emerges when the framework walks the combined logical net at super().__init__(). This is the same algorithm KiCad's ERC runs, except KiCad waits for you to click Run ERC and wirebench waits no longer than __init__ returning. Without that walk, you'd find the conflict the slow way: build the design, watch the supplies fight, scope every node looking for the one that doesn't sit where it should.
The same logic prevents other classes of harm hobbyists don't always know to look for: forbidden runtime states (S=1, R=1 on an SR latch) that would lock a latch into undefined behaviour, ground-domain crossings without an isolator that would defeat the point of having isolation, chips with declared output pins that nothing internal drives — every quiet failure mode where the circuit looks right and doesn't work. Every error message names the offending part by refdes and pin number, so you know what to fix before you reach for a soldering iron.
When pytest is green, the topology is sound.
A real design
demos/water_alarm/ is the simplest end-to-end example — four chips, two LEDs, a pair of probes mounted in a tank. The source is water_alarm.py; open docs/WaterAlarm.svg for the rendered schematic, or docs/WaterAlarm.bom.csv for the parts list. The same folder shape applies to every demo. See docs/learning-path.md for the suggested order to work through them.
What it doesn't do
- Solve Ohm's law. Logic-level only. For continuous-voltage simulation, export to SPICE and let ngspice handle it.
- Catch parameter mistakes. The framework prevents defective topology — shorts, floating nets, mismatched connectors, forbidden states, cross-domain wiring — but it won't save you from a 330 Ω current limiter sized for a high-current LED, a 25 V capacitor on a 30 V rail, or a regulator dissipating 4 W with no heatsink. Use SPICE, arithmetic, or the per-component
GOTCHASstrings for those. - Simulate firmware. Model firmware as a private cell inside a chip subclass (see
demos/digital_thermometer/); the cell is your code. - Lay out a PCB. KiCad netlist export gets you to the start of layout; KiCad does the rest.
See docs/design-principles.md for why the framework is shaped the way it is.
Install
Requires Python 3.10+:
uv venv && uv pip install -e ".[dev]"
# or
python -m venv .venv && source .venv/bin/activate && pip install -e ".[dev]"
Run tests
pytest
Full coverage of the framework, components, every export format with byte-deterministic golden files, end-to-end round-trips, and property-based stress on the load-bearing logic.
Going further
demos/— every demo is a complete study artifact (source + all six exports + rendered schematic).docs/learning-path.md— suggested order for working through the demos.docs/design-principles.md— why the framework prevents what it prevents.docs/component-library-data.md— catalogue of all 122 modelled components with datasheet links, pin maps, and footprints.docs/— implementation specs for every major work package.CLAUDE.md— design philosophy in full, for contributors.
Licensing
Released under the PolyForm Noncommercial License 1.0.0 — free for non-commercial use, including personal study, hobby projects, academic research, and use by educational, charitable, or public institutions.
Commercial use requires a separate paid license. Contact the project maintainer for terms.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wirebench-0.1.0.tar.gz.
File metadata
- Download URL: wirebench-0.1.0.tar.gz
- Upload date:
- Size: 197.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
9faf30439f1748cd3ca52b895dcb146b3569a3d13ae2bb78531c2460c9358b82
|
|
| MD5 |
99d7523ce08d72cdb7a3344dda98971f
|
|
| BLAKE2b-256 |
0c1243c3cfb39b1cd4d1f5c80062f74a731e863b8c46ea840a0c584b48d60e0a
|
Provenance
The following attestation bundles were made for wirebench-0.1.0.tar.gz:
Publisher:
release.yml on raeq/wirebench
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wirebench-0.1.0.tar.gz -
Subject digest:
9faf30439f1748cd3ca52b895dcb146b3569a3d13ae2bb78531c2460c9358b82 - Sigstore transparency entry: 1526013413
- Sigstore integration time:
-
Permalink:
raeq/wirebench@defc94a38829ac59829e9878844d90d6fccaf93c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@defc94a38829ac59829e9878844d90d6fccaf93c -
Trigger Event:
push
-
Statement type:
File details
Details for the file wirebench-0.1.0-py3-none-any.whl.
File metadata
- Download URL: wirebench-0.1.0-py3-none-any.whl
- Upload date:
- Size: 359.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8bc4c9d2d43a1651722e172762237c00fe2884d0458ca45287527d9fc5418542
|
|
| MD5 |
136eca63ecb103f9e460a3564227551e
|
|
| BLAKE2b-256 |
cdeb1d4e4fffc2d7d7a7e78b662ac1442524c93ff9477bdfd89637fe9a96cd1a
|
Provenance
The following attestation bundles were made for wirebench-0.1.0-py3-none-any.whl:
Publisher:
release.yml on raeq/wirebench
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wirebench-0.1.0-py3-none-any.whl -
Subject digest:
8bc4c9d2d43a1651722e172762237c00fe2884d0458ca45287527d9fc5418542 - Sigstore transparency entry: 1526013643
- Sigstore integration time:
-
Permalink:
raeq/wirebench@defc94a38829ac59829e9878844d90d6fccaf93c -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/raeq
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@defc94a38829ac59829e9878844d90d6fccaf93c -
Trigger Event:
push
-
Statement type: