Add verbose flag, WireViz dev dep, and rendered diagram examples

- Gate model/value heuristic output behind --verbose/-v flag (quiet by default)
- Add wireviz>=0.4 as dev dependency for roundtrip render tests
- Generate SVG/PNG diagram renders for inter_module, single_module, hierarchical
- Embed rendered diagrams in README with layout optimization callout
This commit is contained in:
Ryan Malloy 2026-02-13 09:19:18 -07:00
parent fd822e07ce
commit 95ed08866c
9 changed files with 425 additions and 14 deletions

View File

@ -46,6 +46,28 @@ spice2wireviz design.net --bom
spice2wireviz top_level.net --bom-wiring spice2wireviz top_level.net --bom-wiring
``` ```
## Rendered diagrams
### Inter-module wiring
How multiple boards/modules connect to each other — power supply, amplifier, and I/O board with shared power and signal routing:
![Inter-module wiring diagram](docs/examples/inter_module.png)
### Single-module interface
External interface of one subcircuit — the amplifier board's connectors and test points radiating from a central port header:
![Single-module wiring diagram](docs/examples/single_module.png)
### Hierarchical system
A complex multi-level system with voltage regulator, main board, sensor module, and external connectors:
![Hierarchical wiring diagram](docs/examples/hierarchical.png)
These diagrams are generated automatically by piping `spice2wireviz` output through WireViz (`--render` flag). The layout optimizer minimizes cable crossings — notice how parallel wires between the same module pair are bundled into multi-wire cables.
## Output examples ## Output examples
### Input: SPICE netlist ### Input: SPICE netlist
@ -324,7 +346,7 @@ The parser extracts subcircuit definitions (`.subckt`), instances (`X*`), and bo
```bash ```bash
uv sync --extra dev --extra asc uv sync --extra dev --extra asc
uv run pytest # 188 tests uv run pytest # 194 tests
uv run ruff check src/ tests/ uv run ruff check src/ tests/
``` ```

View File

@ -0,0 +1,160 @@
metadata:
title: 'Wiring diagram: hierarchical'
source: tests/fixtures/hierarchical.net
generator: spice2wireviz 2026.2.14
connectors:
J_PWR:
type: BARREL_JACK
pinlabels:
- VIN_RAW
- GND
notes: 'SPICE ref: J_PWR'
TP_3V3:
type: TP
style: simple
pinlabels:
- V3V3
notes: 'SPICE ref: TP_3V3'
J_USB:
type: USB_B_CONN
pinlabels:
- GND
- USB_DP
- USB_DM
notes: 'SPICE ref: J_USB'
X_REG:
type: regulator
pinlabels:
- VIN
- GND
- VOUT
notes: 'SPICE instance: X_REG (regulator)'
X_MAIN:
type: main_board
pinlabels:
- USB_D+
- USB_D-
- VCC
- GND
- SDA
- SCL
- ALERT
notes: 'SPICE instance: X_MAIN (main_board)'
X_SENSOR:
type: sensor_module
pinlabels:
- VCC
- GND
- SDA
- SCL
- ALERT
notes: 'SPICE instance: X_SENSOR (sensor_module)'
cables:
W1:
category: bundle
colors:
- BK
- ''
wirelabels:
- GND
- VIN_RAW
notes: 'Nets: GND, VIN_RAW'
W2:
category: bundle
wirecount: 2
wirelabels:
- USB_DM
- USB_DP
notes: 'Nets: USB_DM, USB_DP'
W3:
colors:
- BK
wirelabels:
- GND
notes: 'Net: GND'
W5:
wirecount: 1
wirelabels:
- V3V3
notes: 'Net: V3V3'
W7:
category: bundle
colors:
- BK
- ''
wirelabels:
- GND
- V3V3
notes: 'Nets: GND, V3V3'
W8:
category: bundle
wirecount: 3
wirelabels:
- ALERT_SIG
- I2C_SCL
- I2C_SDA
notes: 'Nets: ALERT_SIG, I2C_SCL, I2C_SDA'
W9:
category: bundle
colors:
- BK
- ''
wirelabels:
- GND
- V3V3
notes: 'Nets: GND, V3V3'
connections:
- - J_USB: 1
- W3: 1
- X_REG: 2
- - X_REG:
- 2
- 3
- W7:
- 1
- 2
- X_MAIN:
- 4
- 3
- - X_MAIN:
- 7
- 6
- 5
- W8:
- 1
- 2
- 3
- X_SENSOR:
- 5
- 4
- 3
- - TP_3V3: 1
- W5: 1
- X_REG: 3
- - J_USB:
- 3
- 2
- W2:
- 1
- 2
- X_MAIN:
- 2
- 1
- - X_REG:
- 2
- 3
- W9:
- 1
- 2
- X_SENSOR:
- 2
- 1
- - J_PWR:
- 2
- 1
- W1:
- 1
- 2
- X_REG:
- 2
- 1

View File

@ -0,0 +1,93 @@
metadata:
title: 'Wiring diagram: multi_board'
source: tests/fixtures/multi_board.net
generator: spice2wireviz 2026.2.14
connectors:
J_CHASSIS:
type: chassis_gnd
pinlabels:
- EARTH
- GND
notes: 'SPICE ref: J_CHASSIS'
TP_VCC:
type: TP
style: simple
pinlabels:
- VCC
notes: 'SPICE ref: TP_VCC'
X1:
type: power_supply
pinlabels:
- VCC
- GND
notes: 'SPICE instance: X1 (power_supply)'
X2:
type: amplifier
pinlabels:
- VIN
- GND
- VOUT
notes: 'SPICE instance: X2 (amplifier)'
X3:
type: io_board
pinlabels:
- GND
- SIG_IN
- SIG_OUT
- CTRL
notes: 'SPICE instance: X3 (io_board)'
cables:
W1:
colors:
- BK
wirelabels:
- GND
notes: 'Net: GND'
W3:
colors:
- RD
wirelabels:
- VCC
notes: 'Net: VCC'
W5:
category: bundle
colors:
- BK
- RD
wirelabels:
- GND
- VCC
notes: 'Nets: GND, VCC'
W6:
colors:
- BK
wirelabels:
- GND
notes: 'Net: GND'
W8:
wirecount: 1
wirelabels:
- AUDIO_OUT
notes: 'Net: AUDIO_OUT'
connections:
- - TP_VCC: 1
- W3: 1
- X1: 1
- - X1:
- 2
- 1
- W5:
- 1
- 2
- X2:
- 2
- 1
- - X2: 3
- W8: 1
- X3: 2
- - J_CHASSIS: 2
- W1: 1
- X1: 2
- - X1: 2
- W6: 1
- X3: 1

View File

@ -0,0 +1,67 @@
metadata:
title: 'Wiring diagram: simple_board'
source: tests/fixtures/simple_board.net
generator: spice2wireviz 2026.2.14
connectors:
amplifier_board:
type: Module Interface
pinlabels:
- VIN
- GND
- VOUT
- SIGNAL_IN
notes: 'SPICE subcircuit: .subckt amplifier_board'
J1:
type: PWR_CONN
pinlabels:
- VIN
- GND
notes: 'SPICE ref: J1, nets: VIN, GND'
J2:
type: SIG_CONN
pinlabels:
- VOUT
- SIGNAL_IN
notes: 'SPICE ref: J2, nets: SIGNAL_IN, VOUT'
TP1:
type: TP
style: simple
pinlabels:
- N001
notes: 'SPICE ref: TP1, nets: N001'
cables:
W_J1:
category: bundle
colors:
- ''
- BK
wirelabels:
- VIN
- GND
notes: 'Nets: VIN, GND'
W_J2:
category: bundle
wirecount: 2
wirelabels:
- VOUT
- SIGNAL_IN
notes: 'Nets: VOUT, SIGNAL_IN'
connections:
- - amplifier_board:
- 1
- 2
- W_J1:
- 1
- 2
- J1:
- 1
- 2
- - amplifier_board:
- 3
- 4
- W_J2:
- 1
- 2
- J2:
- 1
- 2

View File

@ -35,7 +35,7 @@ Issues = "https://git.supported.systems/warehack.ing/spice2wireviz/issues"
[project.optional-dependencies] [project.optional-dependencies]
asc = ["spicelib>=1.4.9"] asc = ["spicelib>=1.4.9"]
dev = ["ruff", "pytest", "pytest-cov"] dev = ["ruff", "pytest", "pytest-cov", "wireviz>=0.4"]
[project.scripts] [project.scripts]
spice2wireviz = "spice2wireviz.cli:main" spice2wireviz = "spice2wireviz.cli:main"

View File

@ -146,6 +146,12 @@ def _parse_comma_list(ctx: click.Context, param: click.Parameter, value: str | N
is_flag=True, is_flag=True,
help="Emit wiring BOM as CSV instead of WireViz YAML (requires full mapping).", help="Emit wiring BOM as CSV instead of WireViz YAML (requires full mapping).",
) )
@click.option(
"-v",
"--verbose",
is_flag=True,
help="Show detailed parser notes (model/value heuristic decisions, etc.).",
)
@click.version_option(version=__version__) @click.version_option(version=__version__)
def main( def main(
input_file: Path, input_file: Path,
@ -172,8 +178,14 @@ def main(
ltspice_exe: Path | None, ltspice_exe: Path | None,
bom: bool, bom: bool,
bom_wiring: bool, bom_wiring: bool,
verbose: bool,
) -> None: ) -> None:
"""Convert SPICE netlist to WireViz YAML wiring diagram.""" """Convert SPICE netlist to WireViz YAML wiring diagram."""
# --- Set parser verbosity ---
from .parser import netlist as netlist_module
netlist_module.verbose = verbose
# --- Validate mutually exclusive BOM flags --- # --- Validate mutually exclusive BOM flags ---
if bom and bom_wiring: if bom and bom_wiring:
click.echo("Error: --bom and --bom-wiring are mutually exclusive.", err=True) click.echo("Error: --bom and --bom-wiring are mutually exclusive.", err=True)

View File

@ -48,6 +48,11 @@ _KNOWN_NET_PATTERN = re.compile(
re.IGNORECASE, re.IGNORECASE,
) )
# Module-level verbosity flag — controls whether informational notes
# (like model/value heuristic decisions) are printed to stderr.
# Set by the CLI's --verbose flag; defaults to False (quiet).
verbose = False
def _strip_comment(line: str) -> str: def _strip_comment(line: str) -> str:
"""Remove inline ; comments, respecting that ; inside quotes is literal.""" """Remove inline ; comments, respecting that ; inside quotes is literal."""
@ -366,12 +371,13 @@ def _parse_boundary_component(tokens: list[str], prefix: str) -> SpiceComponent
): ):
value = last value = last
nodes = positional[:-1] nodes = positional[:-1]
print( if verbose:
f"Note: treating '{last}' as model/value for {ref} " print(
f"(not as net name). If this is wrong, the net " f"Note: treating '{last}' as model/value for {ref} "
f"connection to '{last}' will be missing.", f"(not as net name). If this is wrong, the net "
file=sys.stderr, f"connection to '{last}' will be missing.",
) file=sys.stderr,
)
pins = [ pins = [
SpicePin( SpicePin(

View File

@ -209,16 +209,41 @@ X1 NET1 NET2 NET3 NET4 small
assert "extra nodes" in captured.err assert "extra nodes" in captured.err
def test_model_value_heuristic_warning(self, capsys): def test_model_value_heuristic_warning(self, capsys):
"""I6: The model/value heuristic should warn when it triggers.""" """I6: The model/value heuristic should warn when verbose is on."""
text = """\ from spice2wireviz.parser import netlist as netlist_module
original = netlist_module.verbose
try:
netlist_module.verbose = True
text = """\
.subckt test A B .subckt test A B
J1 A B SOME_MODEL J1 A B SOME_MODEL
.ends test .ends test
""" """
parse_netlist(text) parse_netlist(text)
captured = capsys.readouterr() captured = capsys.readouterr()
assert "SOME_MODEL" in captured.err assert "SOME_MODEL" in captured.err
assert "model/value" in captured.err assert "model/value" in captured.err
finally:
netlist_module.verbose = original
def test_model_value_heuristic_quiet_by_default(self, capsys):
"""The model/value heuristic should be silent when verbose is off."""
from spice2wireviz.parser import netlist as netlist_module
original = netlist_module.verbose
try:
netlist_module.verbose = False
text = """\
.subckt test A B
J1 A B SOME_MODEL
.ends test
"""
parse_netlist(text)
captured = capsys.readouterr()
assert "model/value" not in captured.err
finally:
netlist_module.verbose = original
def test_duplicate_refs_both_parsed(self): def test_duplicate_refs_both_parsed(self):
"""S3: Duplicate reference designators should both be parsed.""" """S3: Duplicate reference designators should both be parsed."""

26
uv.lock generated
View File

@ -285,6 +285,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" }, { url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
] ]
[[package]]
name = "graphviz"
version = "0.21"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/b3/3ac91e9be6b761a4b30d66ff165e54439dcd48b83f4e20d644867215f6ca/graphviz-0.21.tar.gz", hash = "sha256:20743e7183be82aaaa8ad6c93f8893c923bd6658a04c32ee115edb3c8a835f78", size = 200434, upload-time = "2025-06-15T09:35:05.824Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/4c/e0ce1ef95d4000ebc1c11801f9b944fa5910ecc15b5e351865763d8657f8/graphviz-0.21-py3-none-any.whl", hash = "sha256:54f33de9f4f911d7e84e4191749cac8cc5653f815b06738c54db9a15ab8b1e42", size = 47300, upload-time = "2025-06-15T09:35:04.433Z" },
]
[[package]] [[package]]
name = "iniconfig" name = "iniconfig"
version = "2.3.0" version = "2.3.0"
@ -1010,6 +1019,7 @@ dev = [
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "ruff" }, { name = "ruff" },
{ name = "wireviz" },
] ]
[package.metadata] [package.metadata]
@ -1021,6 +1031,7 @@ requires-dist = [
{ name = "pyyaml", specifier = ">=6.0" }, { name = "pyyaml", specifier = ">=6.0" },
{ name = "ruff", marker = "extra == 'dev'" }, { name = "ruff", marker = "extra == 'dev'" },
{ name = "spicelib", marker = "extra == 'asc'", specifier = ">=1.4.9" }, { name = "spicelib", marker = "extra == 'asc'", specifier = ">=1.4.9" },
{ name = "wireviz", marker = "extra == 'dev'", specifier = ">=0.4" },
] ]
provides-extras = ["asc", "dev"] provides-extras = ["asc", "dev"]
@ -1114,3 +1125,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
] ]
[[package]]
name = "wireviz"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "graphviz" },
{ name = "pillow" },
{ name = "pyyaml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/68/31/26e518535ae54f540dae0e83e583240d4bae9a883a6213d751b7b0c00304/wireviz-0.4.1.tar.gz", hash = "sha256:0e25ad8c2e3a269a7dd4a7f45e4e8d304db7e0432b1843a620acfa6be70570e1", size = 50281, upload-time = "2024-07-13T11:29:30.319Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/5fc147e36159bdf7ff70cdd239e48512384ce32708ef8410df8fd62b4cf8/wireviz-0.4.1-py3-none-any.whl", hash = "sha256:5dfdfec91e5d26e4f27f0f9d031672da7fb3c133d127167d233f4742f01873a2", size = 52083, upload-time = "2024-07-13T11:29:28.972Z" },
]