framework: passive + diode terminals mandatory - #30
Closed
raeq wants to merge 4 commits into
Closed
Conversation
Render any wirebench design as it would sit on a standard solderless breadboard. SVG output sibling to the assembly-guide markdown: the two artefacts share `assembly_guide.placement.place()` so the positions named in the assembly guide's step-by-step instructions match the components' positions in the rendered SVG (acceptance criterion #8 of `.plans/phase-2.6-spec.md`). Subpackage `src/framework/export/breadboard/`: - colors.py — locked palette (red reserved for + rail, black for −, 4-tone analog & digital signal cycles). Cycles are ordinal, not hash-based: adjacent nets on the board get adjacent colours so physically-close jumpers can't collide. - svg.py — coordinate system, inline-style SVG primitives, dynamic canvas width (grows for designs over 63 columns), dynamic viewBox y-offset for designs needing many detour bands. - placement.py — refusal of SMD / Board / multi-Board designs; wraps `assembly_guide.placement.place()`. - routing.py — net walker producing coloured Jumper records. - renderer.py — interval-coloured detour-band allocation, double- detour for cross-bank jumpers (pivots at clear columns so the wire never traverses a chip body's opposite-bank tie strip), pin-name labels on chip bodies, end-cap discs at every plug-in point, per-endpoint row assignment so chained nets land in distinct holes on a shared tie strip. Z-order: surface → jumpers → components, so any jumper section that would pass behind a chip is hidden by the opaque body — visually representing how a real jumper physically routes around the chip. Spec deferrals (`.plans/phase-2.6-spec.md` §12): full-size 63-column board (assembly_guide places parts past col 30, half-size impossible in practice); multi-board composite SVG (multi-Board designs raise `BreadboardIncompatibleError`, matching assembly_guide). Other changes: - `assembly_guide.placement._place_2lead_part` now lands passive leads on row D (near the trough) instead of row A. Leaves rows A–C clear for jumpers terminating on the same tie strip; the assembly-guide goldens stay byte-identical because they use the `any of <pos>A–<pos>E` tie-strip phrasing. - `scripts/render_demo_docs.py` adds `breadboard` to its format loop and emits an SVG refusal stub for designs the visualiser refuses. - 26 new tests under `tests/framework/export/breadboard/` (placement, routing, renderer, byte-identical goldens for 5 demos). - Main README and 12 per-demo READMEs reference the new format. - Every demo's `docs/` folder gains a `<Design>.breadboard.svg`.
There was a problem hiding this comment.
Pull request overview
This PR tightens the framework’s “physical fidelity” enforcement by making passive-component terminals and all diode terminals mandatory, so circuits that list parts in the BOM but leave their pins floating are rejected at Circuit construction time. It also updates affected demos/fixtures and refreshes exported/golden artifacts so every declared part is connected (or explicitly modeled as a “BOM-only” 0‑Ω passthrough where the voltage-only evaluator can’t represent the real analog behavior).
Changes:
- Make
Resistor.t1/t2,Capacitor.t1/t2,Inductor.t1/t2, and all diodeanode/cathodeportsmandatory=True. - Update demos and test fixtures to wire previously-floating passives/diodes (either to a real net or as isolated/rail passthroughs using
dynamically_driven=Truewhere required). - Add regression tests ensuring floating passives/diodes raise
UnconnectedPinError, and refresh many generated export/golden files impacted by the rewiring.
Reviewed changes
Copilot reviewed 102 out of 115 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
src/components/passives/resistor.py |
Make resistor terminals mandatory (mandatory=True) and update rationale comment. |
src/components/passives/capacitor.py |
Make capacitor terminals mandatory and update rationale comment. |
src/components/passives/inductor.py |
Make inductor terminals mandatory and update rationale comment. |
src/components/diodes/d1n4001.py |
Make diode terminals mandatory. |
src/components/diodes/d1n4007.py |
Make diode terminals mandatory. |
src/components/diodes/d1n4148.py |
Make diode terminals mandatory. |
src/components/diodes/d1n4728a.py |
Make diode terminals mandatory. |
src/components/diodes/d1n4733a.py |
Make diode terminals mandatory. |
src/components/diodes/d1n4742a.py |
Make diode terminals mandatory. |
src/components/diodes/d1n5817.py |
Make diode terminals mandatory. |
tests/components/test_resistor.py |
Add regression tests asserting terminals are mandatory + floating resistor rejected. |
tests/components/test_capacitor.py |
Update expectations for mandatory ports + add floating capacitor rejection test. |
tests/components/test_inductor.py |
Update expectations for mandatory ports + add floating inductor rejection test. |
tests/components/test_diode_mandatory_terminals.py |
New parametrized regression tests for all diode subclasses + floating diode rejection. |
tests/framework/test_refdes.py |
Adjust tests to wire mandatory resistor terminals so refdes validation is reached. |
tests/framework/test_format_roundtrip.py |
Update hand-edited .wirebench fixture to include a rail and wires satisfying mandatory pins. |
tests/framework/import_kicad/fixtures/regulator_with_named_rail.net |
Fixture update reflecting added passive connectivity. |
tests/framework/export/bom/test_bom_components.py |
Update BOM export test to include wiring that satisfies mandatory pins. |
tests/framework/export/dot/test_dot_components.py |
Update DOT export test to include wiring that satisfies mandatory pins. |
tests/framework/export/mermaid/test_mermaid_components.py |
Update Mermaid export test to include wiring that satisfies mandatory pins. |
tests/cli/fixtures/floating_net.py |
Update CLI fixture to satisfy mandatory pins while still triggering FloatingNetError. |
demos/backup_power/backup_power.py |
Wire BOM-only passives as 0‑Ω passthroughs on VCC rail to satisfy mandatory-pin enforcement. |
demos/doorbell_protector/doorbell_protector.py |
Wire previously BOM-only passives/diode terminals (incl. synthetic dynamically-driven net for coil clamp node). |
demos/dice/dice.py |
Wire BOM-only timing/decoupling passives as 0‑Ω passthroughs on VCC to satisfy mandatory pins. |
demos/bldc_motor/bldc_motor.py |
Wire BOM-only decoupling/bootstraps + reset pull-up as isolated dynamically-driven passthrough nets. |
demos/fan_cooling/fan_cooling.py |
Wire BOM-only passives/diode as isolated dynamically-driven passthrough nets (avoid graph feedback). |
demos/isolated_rs232/isolated_rs232.py |
Wire BOM-only caps as isolated dynamically-driven passthrough nets. |
demos/backup_power/docs/BackupPower.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
demos/doorbell_protector/docs/DoorbellProtector.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/assembly guide). |
demos/dice/docs/Dice.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/breadboard). |
demos/fan_cooling/docs/FanCoolingBoard.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs/schematic). |
demos/fan_cooling/docs/CooledSystem.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
demos/isolated_rs232/docs/IsolatedRS232Link.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
demos/isolated_rs232/docs/IsolatedRS232Board.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
demos/bldc_motor/docs/BLDCSystem.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
demos/bldc_motor/docs/BLDCControllerBoard.* |
Regenerated exports/goldens reflecting new wiring (netlists/reports/graphs). |
tests/golden/assembly_guide/dice.md |
Golden refresh for dice assembly guide due to added wiring steps. |
tests/golden/assembly_guide/doorbell_protector.md |
Golden refresh for doorbell protector assembly guide due to added wiring steps. |
tests/golden/breadboard/dice.breadboard.svg |
Golden refresh for dice breadboard SVG due to added pass-through wiring. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
added 2 commits
May 19, 2026 18:04
- Rail jumpers always route to the TOP rails. The shared assembly guide instructs builders to wire only the top `+`/`-` rails to the supply; standard breadboards keep top and bottom rail strips electrically isolated, so a chip pin on the bot bank routing to `+BOT`/`-BOT` would show an unpowered connection. Bot-bank pins now cross the trough via the standard cross-bank double-detour, terminating at the powered top rail. Same-column rail verticals (DIP VCC pin at col X to +TOP at the same col X) get hidden in the middle by the chip body's z-order, visually representing the wire arching over the package. - Refusal docstring updated to describe the actual capacity limit: designs that exceed `svg.MAX_POSITIONS` (four daisy-chained full-size breadboards) are refused. Designs that just exceed one board's 63 columns (Dice → 106, DoorbellProtector → 92) are rendered on a canvas wide enough to fit them, with a faint column-break marker every 63 columns. Breadboard goldens for WaterAlarm, Dice, DoorbellProtector refreshed; demo SVGs regenerated.
Found while reviewing WaterAlarm's breadboard SVG: the LEDs in the
design had no ground connections drawn. Investigation revealed
`LED.cathode` was declared `mandatory=False` with the docstring claim
*"floating = GND"* — a "structurally inert under graph evaluation"
shortcut that lets demos write electrically-broken circuits and have
them pass ERC.
A real LED with a dangling cathode does not light. This is exactly
the kind of physical-fidelity violation `CLAUDE.md` lists as
disallowed.
- `src/components/passives/led.py`: `cathode` becomes
`mandatory=True`. Docstring updated to drop the "floating = GND"
claim and explain the contract.
- `tests/components/test_led.py`: regression tests pin both ports
as mandatory and confirm `Circuit.__init__` raises
`UnconnectedPinError` end-to-end when a cathode is left
floating.
- `demos/water_alarm/water_alarm.py` and
`demos/water_alarm_split/water_alarm_split.py`: cathodes wired
to the GND rail. (Bench builds still need a series current-limit
resistor; the logic-level demo doesn't model V_F or current.)
- `tests/framework/test_circuit_autocollect.py`,
`tests/framework/test_framework.py`, and
`tests/framework/import_kicad/test_import.py`: fixtures that
leaned on the broken default updated so they still pin down the
specific error each test exists to catch.
- Demo docs regenerated for the two WaterAlarm variants.
raeq
force-pushed
the
framework-led-cathode-mandatory
branch
from
May 19, 2026 16:05
89570e4 to
a265d46
Compare
Continuation of the LED.cathode regression fix (#29). Resistor, Capacitor, Inductor, and every Diode subclass had their terminals declared `mandatory=False` with the same "structurally inert under graph evaluation" justification — letting demos declare BOM-only timing / decoupling / pull-up parts without wiring them, then ship breadboard SVGs and netlists where those parts sat orphaned. A real resistor / capacitor / inductor / diode with a dangling terminal does nothing. Marking the ports mandatory closes the silent regression at the framework layer. - `src/components/passives/{resistor,capacitor,inductor}.py` and every diode subclass under `src/components/diodes/` now declare their terminals `mandatory=True`. - `tests/components/test_{resistor,capacitor,inductor}.py`: regression tests pin the new contract per component. - `tests/components/test_diode_mandatory_terminals.py` (new): parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A / D1N4733A / D1N4742A / D1N5817. - Six demos (`backup_power`, `bldc_motor`, `dice`, `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had declared-but-unwired passives — each now wires them either to their datasheet-correct net (e.g. DoorbellProtector's flyback diode anode joins the relay coil_minus net) or as an isolated 0-Ω passthrough with `dynamically_driven=True` where the part is genuinely BOM-only and the voltage-only simulator can't honour its behaviour. Comments at each call site explain which kind. - Test fixtures across CLI, refdes, format-roundtrip, kicad import, and per-format renderer tests updated so they still pin down the specific error each one exists to catch without tripping the new mandatory-pin check first. - All demo docs regenerated; goldens refreshed. After this commit the breadboard SVGs show every BOM part connected to something — no more orphaned caps and resistors floating without jumpers.
raeq
force-pushed
the
framework-passive-terminals-mandatory
branch
from
May 19, 2026 16:06
07ab539 to
04ef6e3
Compare
raeq
force-pushed
the
framework-led-cathode-mandatory
branch
from
May 19, 2026 16:32
a265d46 to
2f70dab
Compare
raeq
pushed a commit
that referenced
this pull request
May 19, 2026
Continuation of the LED.cathode regression fix (#29). Resistor, Capacitor, Inductor, and every Diode subclass had their terminals declared `mandatory=False` with the same "structurally inert under graph evaluation" justification — letting demos declare BOM-only timing / decoupling / pull-up parts without wiring them, then ship breadboard SVGs and netlists where those parts sat orphaned. A real resistor / capacitor / inductor / diode with a dangling terminal does nothing. Marking the ports mandatory closes the silent regression at the framework layer. - `src/components/passives/{resistor,capacitor,inductor}.py` and every diode subclass under `src/components/diodes/` now declare their terminals `mandatory=True`. - `tests/components/test_{resistor,capacitor,inductor}.py`: regression tests pin the new contract per component. - `tests/components/test_diode_mandatory_terminals.py` (new): parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A / D1N4733A / D1N4742A / D1N5817. - Six demos (`backup_power`, `bldc_motor`, `dice`, `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had declared-but-unwired passives — each now wires them either to their datasheet-correct net (e.g. DoorbellProtector's flyback diode anode joins the relay coil_minus net) or as an isolated 0-Ω passthrough with `dynamically_driven=True` where the part is genuinely BOM-only and the voltage-only simulator can't honour its behaviour. Comments at each call site explain which kind. - Test fixtures across CLI, refdes, format-roundtrip, kicad import, and per-format renderer tests updated so they still pin down the specific error each one exists to catch without tripping the new mandatory-pin check first. - All demo docs regenerated; goldens refreshed. After this commit the breadboard SVGs show every BOM part connected to something — no more orphaned caps and resistors floating without jumpers.
3 tasks
raeq
added a commit
that referenced
this pull request
May 19, 2026
Continuation of the LED.cathode regression fix (#29). Resistor, Capacitor, Inductor, and every Diode subclass had their terminals declared `mandatory=False` with the same "structurally inert under graph evaluation" justification — letting demos declare BOM-only timing / decoupling / pull-up parts without wiring them, then ship breadboard SVGs and netlists where those parts sat orphaned. A real resistor / capacitor / inductor / diode with a dangling terminal does nothing. Marking the ports mandatory closes the silent regression at the framework layer. - `src/components/passives/{resistor,capacitor,inductor}.py` and every diode subclass under `src/components/diodes/` now declare their terminals `mandatory=True`. - `tests/components/test_{resistor,capacitor,inductor}.py`: regression tests pin the new contract per component. - `tests/components/test_diode_mandatory_terminals.py` (new): parametrized over D1N4001 / D1N4007 / D1N4148 / D1N4728A / D1N4733A / D1N4742A / D1N5817. - Six demos (`backup_power`, `bldc_motor`, `dice`, `doorbell_protector`, `fan_cooling`, `isolated_rs232`) had declared-but-unwired passives — each now wires them either to their datasheet-correct net (e.g. DoorbellProtector's flyback diode anode joins the relay coil_minus net) or as an isolated 0-Ω passthrough with `dynamically_driven=True` where the part is genuinely BOM-only and the voltage-only simulator can't honour its behaviour. Comments at each call site explain which kind. - Test fixtures across CLI, refdes, format-roundtrip, kicad import, and per-format renderer tests updated so they still pin down the specific error each one exists to catch without tripping the new mandatory-pin check first. - All demo docs regenerated; goldens refreshed. After this commit the breadboard SVGs show every BOM part connected to something — no more orphaned caps and resistors floating without jumpers. Co-authored-by: subzero <subzero@noreply.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Continuation of #29. The same physical-fidelity violation that left LED.cathode floating was also present on every other passive component:
Resistor,Capacitor,Inductor, and every diode subclass had their terminals declaredmandatory=Falsewith the "structurally inert under graph evaluation" shortcut. Real components with dangling terminals do nothing — they sit on the BOM but contribute nothing to the circuit.Resistor.t1/t2,Capacitor.t1/t2,Inductor.t1/t2, and every Diode subclass'sanode/cathodenowmandatory=True.backup_power,bldc_motor,dice,doorbell_protector,fan_cooling,isolated_rs232) declared BOM-only passives that were never wired. Each now wires them either to their datasheet-correct net (e.g. DoorbellProtector's flyback diode anode joining the relay's coil_minus) or as an isolated 0-Ω passthrough withdynamically_driven=Truewhere the part is genuinely BOM-only and the voltage-only simulator can't model its behaviour. Comments at each call site explain which.tests/components/test_resistor.py,test_capacitor.py,test_inductor.py—test_terminals_are_mandatory+test_floating_*_refused_at_circuit_constructiontests/components/test_diode_mandatory_terminals.py(new file) — parametrized across all 7 diode subclassesAfter this PR, the breadboard SVGs from #28 show every BOM-listed part connected to something — no more orphaned caps and resistors floating without jumpers.
Test plan
uv run pytest— 4382 passed (20 new regression tests).uv run mypy src/ demos/clean.Based on #29.