Skip to content

demonodojo/wd-netlist

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wd-netlist

Reconstruct electrical connectivity (a netlist) from vector wiring-diagram PDFs (WD schematics).

A wiring diagram drawn as vector graphics stores its real connectivity as line segments — information that is lost when the page is rasterized to PNG and that plain PDF text extraction ignores entirely. wd-netlist reads the vector geometry and the positioned text, then groups connected segments into nets and attaches the nearest labels (signal names, wire colors, connector/part numbers).

Status: working prototype. Junction semantics + a decoration filter (both on by default) keep crossings and component illustrations from fusing unrelated signals (largest net on the reference WD: 921 → 176 nodes). A few hard over-merges remain where one drawn rail genuinely links several connectors. See Roadmap.

How it works

  1. Extract line segments and words (with coordinates) from a page — PyMuPDF.
  2. Snap segment endpoints to a tolerance grid and union-find them into connected groups (nets).
  3. Attach each text label to the nearest net via a spatial grid index.
  4. Report stats and per-net labels (text or JSON).

Install

uv sync

Usage

CLI:

uv run wd-netlist path/to/diagram.pdf
uv run wd-netlist path/to/diagram.pdf --json
uv run wd-netlist path/to/diagram.pdf --max-width 1.0 --min-labels 3 --top 30

# export to standard formats (wire list, SPICE, KiCad, EDIF, KBL/VEC, graphs)
uv run wd-netlist path/to/diagram.pdf --std all --std-out auto

# component neighborhood subgraph (connectors linked within N hops)
uv run wd-netlist path/to/diagram.pdf --subgraph X16 --hops 2 --subgraph-out auto

By default the CLI runs the high-fidelity reconstruction: junction semantics (crossings without a dot don't merge) + the decoration filter + a sub-pixel-noise min-seg-len. Pass --legacy to restore the old naive union-everything pass (it over-merges dense areas; only needed to reproduce pre-0.2 output).

Key options:

  • --legacy restore the old naive reconstruction (no junction semantics / filter)
  • --snap-tol endpoint merge tolerance (pt, default 1.5)
  • --label-tol max label↔net distance (pt, default 10); labels attach to the nearest wire body, not just the nearest endpoint
  • --min-seg-len discard micro-segments (pt, default 1.0; 0.3 under --legacy)
  • --decoration-density flag dense illustration nets and drop them (default 0.012)
  • --max-width keep only thin strokes (filter out thick outlines)
  • --include-rects treat rectangle edges as wires (off by default)
  • --std export to standard formats (wirelist, graphml, jsongraph, dot, spice, kicad, edif, kbl, vec, or all); use with --std-out auto for <stem>.out/standard/netlist.*
  • --subgraph REF export the component neighborhood around connector REF
  • --hops N max component-to-component hops for --subgraph (default 1)
  • --subgraph-out PATH output directory; auto<stem>.out/subgraph/<REF>/

Library:

from wd_netlist import extract_page, build_netlist

segments, words = extract_page("diagram.pdf", page_number=0)
netlist = build_netlist(segments, words, snap_tol=1.5, label_tol=10.0)

print(netlist.stats())
for net in sorted(netlist.labeled_nets(), key=lambda n: len(n.labels), reverse=True)[:10]:
    print(net.id, net.node_count, net.labels)

Tests

uv run pytest

Tests use synthetic geometry only — no PDF, no network access.

Page regeneration (round-trip)

The full-fidelity scene (extract_scene) captures every vector primitive and text span with style, enough to redraw the page. scene_to_doc / render_scene rebuild an image from it, and --roundtrip reports the pixel diff between the original and the regenerated page (plus extraction idempotence).

uv run wd-netlist diagram.pdf --regen auto                          # regenerated image
uv run wd-netlist diagram.pdf --regen out.png --render-dpi 200       # at a chosen path/dpi
uv run wd-netlist diagram.pdf --regen crop.png --clip x0,y0,x1,y1    # just a region
uv run wd-netlist diagram.pdf --dump-scene auto --regen-scene auto   # scene.json + image
uv run wd-netlist diagram.pdf --roundtrip                            # fidelity report

--regen is the one-shot command: it extracts the styled scene from the PDF (vectors, text, raster images) and redraws it to an image — no intermediate scene file needed. auto writes <stem>.work/regen.png.

Two things are essential for the regenerated image to match the original:

  • Path grouping — a filled shape is stored as several line items plus a fill; the elements of one original drawing share a path id and are stroked/filled once, so the enclosed area fills (junction dots, ground triangles, solid components). Finishing each line on its own loses every fill.
  • Text direction — these schematics are on 90°-rotated pages, so the text runs vertically in unrotated space (span.dir). Each span is drawn with the matching rotate angle; writing it horizontally made all text come out sideways.
  • Raster images — component photos/illustrations (the NEO3 HMI display, the RTU unit) are embedded image XObjects, not vector paths. The scene captures each as a SceneImage (bytes + placement bbox) and re-inserts it, so they survive the round-trip (BBCC has 131 such placements; ZW has none).

Together these cut the reference round-trip image diff roughly in half (ZW 5.5%→2.7%, BBCC 11%→4.5%). The residual is mostly font substitution (PyMuPDF base-14 Helvetica for the embedded fonts).

Component neighborhood subgraph

Given a connector/component designator (e.g. X16), extract every other component reachable within N hops along labeled nets (1 hop = direct neighbors; 2 hops = neighbors of neighbors, and so on). Writes data files plus a PNG graph image under <stem>.out/subgraph/<REF>/:

uv run wd-netlist diagram.pdf --subgraph X16 --hops 2 --subgraph-out auto

Outputs: netlist.graphml, netlist.jsongraph.json, netlist.csv, netlist.dot (Graphviz), and <REF>.png (component nodes at diagram positions, edges = cables). Library:

from wd_netlist import build_netlist, extract_page, neighborhood, layout_positions
from wd_netlist.standard import build_standard_netlist
from wd_netlist.extract import render_subgraph

segments, words = extract_page("diagram.pdf")
netlist = build_netlist(segments, words)
std = build_standard_netlist(netlist, words, segments=segments)
sub, hop_dist = neighborhood(std, "X16", hops=2)
render_subgraph(layout_positions(sub), sub, "x16-neighborhood.png",
                hop_by_ref=hop_dist, start_ref="X16")

Roadmap

Turning "connected groups" into a correct terminal→wire→terminal netlist. Items 1–5 are implemented and on by default (1, 2) or available via --export / --vision-bundle (3–5); see the netlist-algorithm skill for details.

  1. Wire vs decoration (done) — dense illustration nets are flagged by node density and dropped (--decoration-density).
  2. Junction semantics (done) — two lines crossing without a junction dot are not connected; dots/T-joints/continuations are. On by default.
  3. Terminal/pin model (done) — pins are attributed to connectors and labels attach to the nearest wire body (--export).
  4. Netlist export (done) — normalized, LLM-friendly JSON with classified labels, terminals and connections (--export).
  5. Hybrid vision (first cut) — per-connector image crops + index for an LLM to resolve symbols/annotations (--vision-bundle).

Split connector designators (done) — the text layer often emits a connector label as two tokens (CN + 8, RL + 1); export.merge_split_designators fuses an axis-aligned prefix+number pair back into one CN8 designator before attribution, so its pins attribute correctly and emit named connections. On the reference ZW WD this took fully-attributed connections from 0 to 15 (every prior connection had an unattributed ?.N terminal).

Component type (done) — each connector in the --vision-bundle index gets a type (relay/fuse/connector) from export.infer_component_type: the DIN-72552 relay coil pins (85 & 86) co-occurring is a robust relay signature, and an F-prefixed designator is a fuse. The wider per-connector crop (it now frames the surrounding signal/colour labels, not just the pin row) lets a vision model refine the rest.

Semantic netlist (done) — the --vision-bundle also writes prompts.json (one pin→signal question per connector). A vision model answers them into a resolution map {connector: {pin: signal}}; feeding that back with --resolve map.json [--export out.json] (export.apply_resolution) produces the semantic netlist: edges carrying each terminal's resolved signal and a derived net name. When both ends of a geometric edge resolve to the same signal (e.g. CN36.12[VCC+30] — CN9.5[VCC+30]) the connection is cross-checked / corroborated; when they differ it flags a likely pin→connector mis-attribution to review.

Still open: robust over-merge handling where a single drawn rail/bus connects several connectors (e.g. shared horizontal rails), and wire-colour attribution on the recovered connections (currently often null).

About

wire diagrams from pdf to netlist

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages