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:
Ryan Malloy 2026-02-13 01:27:45 -07:00
parent e20a956f51
commit eb3ad60bd1
13 changed files with 1250 additions and 38 deletions

View file

@ -1,3 +1,3 @@
"""spice2wireviz — Convert LTspice SPICE netlists to WireViz wiring diagrams."""
__version__ = "2026-02-13"
__version__ = "2026.2.13"

View file

@ -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:

View file

@ -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,21 +105,35 @@ 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):
net_endpoints[node].append((conn_name, pin_idx, node))
for pin_idx, node in enumerate(filtered_nodes, 1):
net_endpoints[node].append((conn_name, pin_idx, node))
# --- Trace connections via shared nets ---
# Group connections by (connector_a, connector_b) pair for parallel wire grouping
@ -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)

View file

@ -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)

View file

@ -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

View file

@ -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"

View file

@ -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():