diff --git a/README.md b/README.md index f45f2fd..14002e7 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,28 @@ spice2wireviz design.net --bom 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 ### Input: SPICE netlist @@ -324,7 +346,7 @@ The parser extracts subcircuit definitions (`.subckt`), instances (`X*`), and bo ```bash uv sync --extra dev --extra asc -uv run pytest # 188 tests +uv run pytest # 194 tests uv run ruff check src/ tests/ ``` diff --git a/docs/examples/hierarchical.yml b/docs/examples/hierarchical.yml new file mode 100644 index 0000000..9a6636f --- /dev/null +++ b/docs/examples/hierarchical.yml @@ -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 diff --git a/docs/examples/inter_module.yml b/docs/examples/inter_module.yml new file mode 100644 index 0000000..99f1250 --- /dev/null +++ b/docs/examples/inter_module.yml @@ -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 diff --git a/docs/examples/single_module.yml b/docs/examples/single_module.yml new file mode 100644 index 0000000..d9a58f7 --- /dev/null +++ b/docs/examples/single_module.yml @@ -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 diff --git a/pyproject.toml b/pyproject.toml index bb9c031..bc0582b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,7 +35,7 @@ Issues = "https://git.supported.systems/warehack.ing/spice2wireviz/issues" [project.optional-dependencies] asc = ["spicelib>=1.4.9"] -dev = ["ruff", "pytest", "pytest-cov"] +dev = ["ruff", "pytest", "pytest-cov", "wireviz>=0.4"] [project.scripts] spice2wireviz = "spice2wireviz.cli:main" diff --git a/src/spice2wireviz/cli.py b/src/spice2wireviz/cli.py index bee611e..46ccc4c 100644 --- a/src/spice2wireviz/cli.py +++ b/src/spice2wireviz/cli.py @@ -146,6 +146,12 @@ def _parse_comma_list(ctx: click.Context, param: click.Parameter, value: str | N is_flag=True, 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__) def main( input_file: Path, @@ -172,8 +178,14 @@ def main( ltspice_exe: Path | None, bom: bool, bom_wiring: bool, + verbose: bool, ) -> None: """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 --- if bom and bom_wiring: click.echo("Error: --bom and --bom-wiring are mutually exclusive.", err=True) diff --git a/src/spice2wireviz/parser/netlist.py b/src/spice2wireviz/parser/netlist.py index 3fa48bd..348872a 100644 --- a/src/spice2wireviz/parser/netlist.py +++ b/src/spice2wireviz/parser/netlist.py @@ -48,6 +48,11 @@ _KNOWN_NET_PATTERN = re.compile( 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: """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 nodes = positional[:-1] - print( - f"Note: treating '{last}' as model/value for {ref} " - f"(not as net name). If this is wrong, the net " - f"connection to '{last}' will be missing.", - file=sys.stderr, - ) + if verbose: + print( + f"Note: treating '{last}' as model/value for {ref} " + f"(not as net name). If this is wrong, the net " + f"connection to '{last}' will be missing.", + file=sys.stderr, + ) pins = [ SpicePin( diff --git a/tests/test_netlist_parser.py b/tests/test_netlist_parser.py index e15616d..e51e569 100644 --- a/tests/test_netlist_parser.py +++ b/tests/test_netlist_parser.py @@ -209,16 +209,41 @@ X1 NET1 NET2 NET3 NET4 small assert "extra nodes" in captured.err def test_model_value_heuristic_warning(self, capsys): - """I6: The model/value heuristic should warn when it triggers.""" - text = """\ + """I6: The model/value heuristic should warn when verbose is on.""" + from spice2wireviz.parser import netlist as netlist_module + + original = netlist_module.verbose + try: + netlist_module.verbose = True + text = """\ .subckt test A B J1 A B SOME_MODEL .ends test """ - parse_netlist(text) - captured = capsys.readouterr() - assert "SOME_MODEL" in captured.err - assert "model/value" in captured.err + parse_netlist(text) + captured = capsys.readouterr() + assert "SOME_MODEL" 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): """S3: Duplicate reference designators should both be parsed.""" diff --git a/uv.lock b/uv.lock index ca35d55..c25d60e 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "iniconfig" version = "2.3.0" @@ -1010,6 +1019,7 @@ dev = [ { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, + { name = "wireviz" }, ] [package.metadata] @@ -1021,6 +1031,7 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0" }, { name = "ruff", marker = "extra == 'dev'" }, { name = "spicelib", marker = "extra == 'asc'", specifier = ">=1.4.9" }, + { name = "wireviz", marker = "extra == 'dev'", specifier = ">=0.4" }, ] provides-extras = ["asc", "dev"] @@ -1114,3 +1125,18 @@ sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac 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" }, ] + +[[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" }, +]