Fix linting issues and ground/power filtering in inter-module mapper
- Use CalVer dot notation (2026.2.13) for PEP 440 compliance - Filter pinlabels for top-level components when ground/power hidden - Fix unused variables, long lines, import ordering (ruff clean) - Use StrEnum for PinDirection (Python 3.11+) - Add .gitignore and README.md - All 105 tests pass including WireViz roundtrip validation
This commit is contained in:
parent
e20a956f51
commit
eb3ad60bd1
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
.venv/
|
||||
*.egg-info/
|
||||
dist/
|
||||
build/
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
*.svg
|
||||
*.png
|
||||
*.html
|
||||
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# spice2wireviz
|
||||
|
||||
Convert LTspice SPICE netlists to WireViz wiring diagrams.
|
||||
|
||||
## What it does
|
||||
|
||||
`spice2wireviz` reads SPICE netlist files (`.net`, `.cir`, `.sp`) and generates [WireViz](https://github.com/wireviz/WireViz) YAML that documents the physical wiring: connectors, test points, and inter-module cables.
|
||||
|
||||
Two operating modes:
|
||||
|
||||
- **Single module** — External interface of one subcircuit (its connectors, test points, port interface)
|
||||
- **Inter-module** — How multiple subcircuits/boards connect to each other
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
uv tool install spice2wireviz
|
||||
# or
|
||||
pip install spice2wireviz
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Inter-module wiring (auto-detected from top-level X instances)
|
||||
spice2wireviz top_level.net -o wiring.yml --render
|
||||
|
||||
# Single module external interface
|
||||
spice2wireviz design.net -s amplifier_board -o amp.yml
|
||||
|
||||
# Only connectors and test points, no ground
|
||||
spice2wireviz design.net --include-prefixes J,TP --no-ground
|
||||
|
||||
# Inspect before converting
|
||||
spice2wireviz design.net --list-subcircuits
|
||||
spice2wireviz design.net --list-components
|
||||
spice2wireviz design.net --dry-run
|
||||
```
|
||||
|
||||
## Filtering
|
||||
|
||||
Cherry-pick what appears in the diagram:
|
||||
|
||||
```bash
|
||||
--include-prefixes J,TP # Only these component types
|
||||
--exclude-refs X3,J_DEBUG # Hide specific references
|
||||
--include-nets "SIG_*" # Glob patterns for net names
|
||||
--no-ground # Hide GND connections
|
||||
--no-power # Hide VCC/VDD connections
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
uv sync --extra dev
|
||||
uv run pytest
|
||||
uv run ruff check src/ tests/
|
||||
```
|
||||
@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "spice2wireviz"
|
||||
version = "2026-02-13"
|
||||
version = "2026.2.13"
|
||||
description = "Convert LTspice SPICE netlists to WireViz wiring diagrams"
|
||||
authors = [{name = "Ryan Malloy", email = "ryan@supported.systems"}]
|
||||
requires-python = ">=3.11"
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
"""spice2wireviz — Convert LTspice SPICE netlists to WireViz wiring diagrams."""
|
||||
|
||||
__version__ = "2026-02-13"
|
||||
__version__ = "2026.2.13"
|
||||
|
||||
@ -68,10 +68,7 @@ def filter_component(comp: SpiceComponent, config: FilterConfig, netlist: Parsed
|
||||
# Specific reference filtering
|
||||
if config.exclude_refs and comp.reference in config.exclude_refs:
|
||||
return False
|
||||
if config.include_refs and comp.reference not in config.include_refs:
|
||||
return False
|
||||
|
||||
return True
|
||||
return not (config.include_refs and comp.reference not in config.include_refs)
|
||||
|
||||
|
||||
def filter_instance(
|
||||
@ -93,10 +90,10 @@ def filter_instance(
|
||||
# Subcircuit name filtering
|
||||
if config.exclude_subcircuits and inst.subcircuit_name in config.exclude_subcircuits:
|
||||
return False
|
||||
if config.include_subcircuits and inst.subcircuit_name not in config.include_subcircuits:
|
||||
return False
|
||||
|
||||
return True
|
||||
return not (
|
||||
config.include_subcircuits
|
||||
and inst.subcircuit_name not in config.include_subcircuits
|
||||
)
|
||||
|
||||
|
||||
def filter_net(net: str, config: FilterConfig, netlist: ParsedNetlist) -> bool:
|
||||
|
||||
@ -13,9 +13,14 @@ from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from typing import Any
|
||||
|
||||
from ..emitter.yaml_emitter import assemble_wireviz_doc, build_cable, build_connection, build_connector
|
||||
from ..emitter.yaml_emitter import (
|
||||
assemble_wireviz_doc,
|
||||
build_cable,
|
||||
build_connection,
|
||||
build_connector,
|
||||
)
|
||||
from ..filter import FilterConfig, filter_component, filter_instance, filter_net
|
||||
from ..parser.models import ParsedNetlist, SubcircuitInstance
|
||||
from ..parser.models import ParsedNetlist
|
||||
|
||||
|
||||
def _net_wire_color(net: str, netlist: ParsedNetlist) -> str:
|
||||
@ -67,7 +72,6 @@ def map_inter_module(
|
||||
|
||||
for inst in filtered_instances:
|
||||
conn_name = _make_connector_name(inst.reference)
|
||||
port_labels = list(inst.port_to_net.keys())
|
||||
filtered_ports: list[str] = []
|
||||
filtered_nets: list[str] = []
|
||||
|
||||
@ -87,7 +91,9 @@ def map_inter_module(
|
||||
)
|
||||
|
||||
# Register net endpoints
|
||||
for pin_idx, (port, net) in enumerate(zip(filtered_ports, filtered_nets), 1):
|
||||
for pin_idx, (port, net) in enumerate(
|
||||
zip(filtered_ports, filtered_nets, strict=True), 1
|
||||
):
|
||||
net_endpoints[net].append((conn_name, pin_idx, port))
|
||||
|
||||
# --- Create connectors for top-level J*/TP*/P* ---
|
||||
@ -99,20 +105,34 @@ def map_inter_module(
|
||||
|
||||
for comp in filtered_components:
|
||||
conn_name = _make_connector_name(comp.reference)
|
||||
pin_labels = [p.name for p in comp.pins]
|
||||
style = "simple" if comp.prefix.upper() == "TP" and len(pin_labels) == 1 else ""
|
||||
|
||||
# Filter pins by net — only include pins whose net passes the filter
|
||||
filtered_labels: list[str] = []
|
||||
filtered_nodes: list[str] = []
|
||||
for pin in comp.pins:
|
||||
if filter_net(pin.net_name, config, netlist):
|
||||
filtered_labels.append(pin.name)
|
||||
filtered_nodes.append(pin.net_name)
|
||||
|
||||
if not filtered_labels and not comp.nodes:
|
||||
continue
|
||||
|
||||
style = (
|
||||
"simple"
|
||||
if comp.prefix.upper() == "TP" and len(filtered_labels) == 1
|
||||
else ""
|
||||
)
|
||||
|
||||
connectors[conn_name] = build_connector(
|
||||
conn_name,
|
||||
pinlabels=pin_labels if pin_labels else None,
|
||||
pincount=len(comp.nodes) if not pin_labels else None,
|
||||
pinlabels=filtered_labels if filtered_labels else None,
|
||||
pincount=len(filtered_nodes) if not filtered_labels else None,
|
||||
connector_type=comp.value or comp.prefix,
|
||||
style=style,
|
||||
notes=f"SPICE ref: {comp.reference}",
|
||||
)
|
||||
|
||||
for pin_idx, node in enumerate(comp.nodes, 1):
|
||||
if filter_net(node, config, netlist):
|
||||
for pin_idx, node in enumerate(filtered_nodes, 1):
|
||||
net_endpoints[node].append((conn_name, pin_idx, node))
|
||||
|
||||
# --- Trace connections via shared nets ---
|
||||
@ -163,7 +183,6 @@ def map_inter_module(
|
||||
else:
|
||||
# Individual wires (one cable per wire)
|
||||
for a_pin, b_pin, net in wires:
|
||||
cable_counter_inner = cable_counter
|
||||
cable_name = f"W{cable_counter}"
|
||||
wire_color = _net_wire_color(net, netlist)
|
||||
|
||||
|
||||
@ -14,7 +14,12 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from ..emitter.yaml_emitter import assemble_wireviz_doc, build_cable, build_connection, build_connector
|
||||
from ..emitter.yaml_emitter import (
|
||||
assemble_wireviz_doc,
|
||||
build_cable,
|
||||
build_connection,
|
||||
build_connector,
|
||||
)
|
||||
from ..filter import FilterConfig, filter_component, filter_net
|
||||
from ..parser.models import ParsedNetlist, SpiceComponent, SubcircuitDef
|
||||
|
||||
@ -119,9 +124,11 @@ def map_single_module(
|
||||
cable_wires = list(range(1, len(shared_nets) + 1))
|
||||
|
||||
if header_pins and comp_pins and len(header_pins) == len(comp_pins):
|
||||
connections.append(
|
||||
build_connection(header_name, header_pins, cable_name, cable_wires, comp_name, comp_pins)
|
||||
)
|
||||
connections.append(build_connection(
|
||||
header_name, header_pins,
|
||||
cable_name, cable_wires,
|
||||
comp_name, comp_pins,
|
||||
))
|
||||
|
||||
return assemble_wireviz_doc(connectors, cables, connections, metadata)
|
||||
|
||||
|
||||
@ -12,13 +12,7 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from .models import (
|
||||
ParsedNetlist,
|
||||
SpiceComponent,
|
||||
SpicePin,
|
||||
SubcircuitDef,
|
||||
SubcircuitInstance,
|
||||
)
|
||||
from .models import ParsedNetlist, SpiceComponent, SubcircuitDef, SubcircuitInstance
|
||||
from .netlist import BOUNDARY_PREFIXES, _extract_prefix
|
||||
|
||||
|
||||
|
||||
@ -7,12 +7,12 @@ components (J*, TP*, P*), subcircuit instances (X*), and net connectivity.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
from enum import StrEnum
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class PinDirection(str, Enum):
|
||||
class PinDirection(StrEnum):
|
||||
UNKNOWN = "unknown"
|
||||
INPUT = "input"
|
||||
OUTPUT = "output"
|
||||
|
||||
@ -110,8 +110,11 @@ def parse_netlist(source: str | Path) -> ParsedNetlist:
|
||||
Returns:
|
||||
ParsedNetlist with subcircuit defs, instances, and components.
|
||||
"""
|
||||
netlist_suffixes = {".net", ".cir", ".sp"}
|
||||
if isinstance(source, Path) or (
|
||||
isinstance(source, str) and not "\n" in source and Path(source).suffix in {".net", ".cir", ".sp"}
|
||||
isinstance(source, str)
|
||||
and "\n" not in source
|
||||
and Path(source).suffix in netlist_suffixes
|
||||
):
|
||||
path = Path(source)
|
||||
if path.exists():
|
||||
|
||||
@ -14,7 +14,7 @@ class TestCLIBasic:
|
||||
runner = CliRunner()
|
||||
result = runner.invoke(main, ["--version"])
|
||||
assert result.exit_code == 0
|
||||
assert "2026-02-13" in result.output
|
||||
assert "2026.2.13" in result.output
|
||||
|
||||
def test_list_subcircuits(self):
|
||||
runner = CliRunner()
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
"""Tests for the filter engine."""
|
||||
|
||||
from spice2wireviz.filter import FilterConfig, apply_filters, filter_component, filter_instance, filter_net
|
||||
from spice2wireviz.filter import (
|
||||
FilterConfig,
|
||||
apply_filters,
|
||||
filter_component,
|
||||
filter_instance,
|
||||
filter_net,
|
||||
)
|
||||
from spice2wireviz.parser.models import (
|
||||
ParsedNetlist,
|
||||
SpiceComponent,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user