Replaces the bpj2ini.py prototype with a packaged tool. The A2F codec is now reimplemented in Python, so neither Boxsim.exe nor a CPU emulator is needed to decode a container; the emulator survives as a development-only oracle that the codec is differentially tested against. Verified: - 134 containers decode byte-identically to Boxsim's own decoder, 270 pre-2.0 files rejected by both implementations (tools/difftest.py --self-test) - Default.BPJ: 22 components, 14 nets, no dangling terminal - 22 of 22 parts resolve to library symbols; one series inferred from a sibling, two DC resistance deviations reported - ERC clean and the netlist KiCad extracts from the generated sheet is an identical partition to the one derived from the Boxsim grid - container build byte-identical to the host build - 32 tests passing, ruff clean Decode time went 69 s -> 14.5 s (binding emulator hooks to trap addresses) -> 1.6 s (pure Python). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
168 lines
5.3 KiB
Python
168 lines
5.3 KiB
Python
"""Verify a generated project against the Boxsim data it came from.
|
|
|
|
The gate that matters is not ERC: a schematic can be perfectly well formed and
|
|
still wired to the wrong cells. Instead the netlist KiCad extracts from the
|
|
generated sheet is compared against the netlist derived independently from the
|
|
Boxsim grid. Two derivations that agree is real evidence; ERC alone is not.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
|
|
from .model import Crossover
|
|
from .schematic import Placement
|
|
|
|
MIN_KICAD_MAJOR = 10
|
|
|
|
_NET_RE = re.compile(r"\(net\b(.*?)(?=\n\t\t\(net\b|\n\t\)\s*$)", re.S)
|
|
_NAME_RE = re.compile(r'\(name\s+"([^"]*)"\)')
|
|
_NODE_RE = re.compile(r'\(node\s*\(ref\s+"([^"]+)"\)\s*\(pin\s+"([^"]+)"\)', re.S)
|
|
|
|
|
|
class VerifyError(RuntimeError):
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class GateResult:
|
|
erc_ok: bool
|
|
erc_violations: int
|
|
netlist_matches: bool
|
|
kicad_version: str
|
|
reference_nets: list[frozenset[str]] = field(default_factory=list)
|
|
kicad_nets: list[frozenset[str]] = field(default_factory=list)
|
|
messages: list[str] = field(default_factory=list)
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return self.erc_ok and self.netlist_matches
|
|
|
|
|
|
def kicad_cli() -> str:
|
|
exe = shutil.which("kicad-cli")
|
|
if exe is None:
|
|
raise VerifyError(
|
|
"kicad-cli not found. KiCad 10 is a hard dependency of the "
|
|
"verification gate; install it or run `boxsim build` without --verify."
|
|
)
|
|
return exe
|
|
|
|
|
|
def kicad_version() -> str:
|
|
out = subprocess.run(
|
|
[kicad_cli(), "version"], capture_output=True, text=True, check=True
|
|
).stdout.strip()
|
|
major = out.split(".")[0]
|
|
if not major.isdigit() or int(major) != MIN_KICAD_MAJOR:
|
|
raise VerifyError(
|
|
f"kicad-cli reports version {out!r}, but this tool targets "
|
|
f"KiCad {MIN_KICAD_MAJOR}.x schematics"
|
|
)
|
|
return out
|
|
|
|
|
|
def reference_partition(
|
|
crossover: Crossover, placements: list[Placement]
|
|
) -> list[frozenset[str]]:
|
|
"""Group `REFDES.PIN` terminals by the net they sit on in the Boxsim grid."""
|
|
netlist = crossover.build_netlist()
|
|
groups: dict[int, set[str]] = {}
|
|
for p in placements:
|
|
for pin, node in p.pin_nodes.items():
|
|
net = netlist.net_of(node)
|
|
groups.setdefault(net, set()).add(f"{p.refdes}.{pin}")
|
|
return sorted((frozenset(v) for v in groups.values()), key=_net_key)
|
|
|
|
|
|
def parse_netlist(text: str) -> list[frozenset[str]]:
|
|
"""Read the `(nets ...)` block of a KiCad sexpr netlist into a partition."""
|
|
start = text.find("\t(nets")
|
|
if start < 0:
|
|
raise VerifyError("exported netlist has no (nets ...) section")
|
|
body = text[start:]
|
|
out = []
|
|
for chunk in body.split("\t\t(net")[1:]:
|
|
terminals = {f"{ref}.{pin}" for ref, pin in _NODE_RE.findall(chunk)}
|
|
if terminals:
|
|
out.append(frozenset(terminals))
|
|
if not out:
|
|
raise VerifyError("exported netlist contained no nets")
|
|
return sorted(out, key=_net_key)
|
|
|
|
|
|
def _net_key(net: frozenset[str]) -> tuple:
|
|
return (len(net), tuple(sorted(net)))
|
|
|
|
|
|
def run_gate(
|
|
schematic: Path,
|
|
crossover: Crossover,
|
|
placements: list[Placement],
|
|
*,
|
|
workdir: Path | None = None,
|
|
) -> GateResult:
|
|
version = kicad_version()
|
|
workdir = workdir or schematic.parent
|
|
erc_report = workdir / f"{schematic.stem}-erc.rpt"
|
|
netlist_path = workdir / f"{schematic.stem}.net"
|
|
|
|
erc = subprocess.run(
|
|
[
|
|
kicad_cli(), "sch", "erc",
|
|
"--severity-error", "--exit-code-violations",
|
|
"-o", str(erc_report), str(schematic),
|
|
],
|
|
capture_output=True, text=True,
|
|
)
|
|
violations = 0
|
|
if erc_report.exists():
|
|
m = re.search(r"Errors (\d+)", erc_report.read_text(encoding="utf-8"))
|
|
if m:
|
|
violations = int(m.group(1))
|
|
erc_ok = erc.returncode == 0 and violations == 0
|
|
|
|
export = subprocess.run(
|
|
[
|
|
kicad_cli(), "sch", "export", "netlist",
|
|
"--format", "kicadsexpr", "-o", str(netlist_path), str(schematic),
|
|
],
|
|
capture_output=True, text=True,
|
|
)
|
|
if export.returncode != 0:
|
|
raise VerifyError(f"netlist export failed: {export.stderr.strip()}")
|
|
|
|
reference = reference_partition(crossover, placements)
|
|
extracted = parse_netlist(netlist_path.read_text(encoding="utf-8"))
|
|
|
|
messages = []
|
|
if not erc_ok:
|
|
messages.append(f"ERC reported {violations} error(s); see {erc_report.name}")
|
|
|
|
matches = reference == extracted
|
|
if not matches:
|
|
only_ref = [sorted(n) for n in reference if n not in extracted]
|
|
only_kicad = [sorted(n) for n in extracted if n not in reference]
|
|
messages.append(
|
|
f"netlist mismatch: {len(reference)} reference nets vs "
|
|
f"{len(extracted)} extracted"
|
|
)
|
|
for net in only_ref[:8]:
|
|
messages.append(f" only in Boxsim reference: {net}")
|
|
for net in only_kicad[:8]:
|
|
messages.append(f" only in KiCad netlist: {net}")
|
|
|
|
return GateResult(
|
|
erc_ok=erc_ok,
|
|
erc_violations=violations,
|
|
netlist_matches=matches,
|
|
kicad_version=version,
|
|
reference_nets=reference,
|
|
kicad_nets=extracted,
|
|
messages=messages,
|
|
)
|