# -*- coding: utf-8 -*-
# The MIT License (MIT) - Copyright (c) Dave Vandenbout.
"""
Generic, hierarchical netlist emitter for schematic generation.
SKiDL's job is to describe electrical interconnection. The *graphical* work of
turning that interconnection into a schematic (placement, routing, KiCad file
generation) is done by a separate tool. The bridge between the two is the
JSON document produced here by :func:`build_generic_netlist`.
The document is deliberately tool-neutral and easy to extend:
- Component symbol geometry is **embedded** (one definition per unique
``lib_id``), so the downstream tool needs no access to KiCad symbol
libraries and the result is fully reproducible.
- Orientation/layout *hints* the downstream placer consumes are carried
alongside the electrical data: per-component ``symtx`` (mirror/rotate) and
per-net ``stub``/``netio``. New hints can be added as new keys without
breaking readers.
Coordinates are emitted in native KiCad units (mm) and integer degrees; the
downstream tool performs the mm->mils and degrees->direction normalization
(the equivalent of ``preprocess_circuit`` in the current KiCad backends).
"""
import os.path
from skidl.utilities import export_to_all
__all__ = ["build_generic_netlist"]
# Schema identifier and version. Bump SCHEMA_VERSION on incompatible changes;
# additive hints do not require a bump.
SCHEMA_NAME = "skidl-generic-netlist"
SCHEMA_VERSION = 1
def _lib_name_of(part):
"""Return the library name (no extension/path) a part came from."""
filename = getattr(getattr(part, "lib", None), "filename", "") or ""
if filename:
return os.path.splitext(os.path.basename(filename))[0]
return "Device"
def _lib_id_of(part):
"""Return the ``lib:part`` identifier used to key embedded symbols."""
return "{}:{}".format(_lib_name_of(part), part.name or "Unknown")
def _units_of(part):
"""Yield ``(unit_num, unit_obj)`` for a part.
Mirrors ``preprocess_circuit.units``: a part with no explicit units is its
own single unit. Multi-unit parts yield each sub-unit.
"""
if not getattr(part, "unit", None):
yield 1, part
else:
for unit in part.unit.values():
yield getattr(unit, "num", 1), unit
def _jsonify(obj):
"""Convert Sexp/nested-list draw commands into plain JSON-safe lists.
``draw_cmds`` are ``simp_sexp.Sexp`` objects (a ``list`` subclass) holding
strings and numbers. Recurse to strip the subclass so ``json`` accepts them
and so the document does not depend on Sexp for reading.
"""
if isinstance(obj, (list, tuple)):
return [_jsonify(x) for x in obj]
return obj
def _json_value(val):
"""Return val if it is a JSON scalar, else its string form."""
if val is None or isinstance(val, (bool, int, float, str)):
return val
return str(val)
def _pin_geometry(pin):
"""Serialize the symbol-level geometry of a single pin."""
return {
"num": str(pin.num),
"name": getattr(pin, "name", "") or "",
"x": pin.x,
"y": pin.y,
# Native symbol orientation. May already be a direction string
# ("R"/"U"/"L"/"D") after library parsing, or an integer angle.
"orient": pin.orientation,
"rotation": getattr(pin, "rotation", 0),
"length": getattr(pin, "length", 0),
"func": str(getattr(pin, "func", "")),
}
def _symbol_def(part):
"""Build the embedded, reusable symbol definition for a part's ``lib_id``.
Captures both the raw draw commands (needed to emit KiCad ``lib_symbols``)
and per-unit pin geometry (needed by the placement/routing engine).
"""
units = {}
for unit_num, unit in _units_of(part):
units[str(unit_num)] = {
"pins": [_pin_geometry(p) for p in unit.pins],
}
draw_cmds = {}
for unit_num, cmds in getattr(part, "draw_cmds", {}).items():
draw_cmds[str(unit_num)] = _jsonify(cmds)
return {
"ref_prefix": getattr(part, "ref_prefix", "") or "U",
"name": part.name or "Unknown",
"lib": _lib_name_of(part),
"description": getattr(part, "description", "") or "",
"datasheet": getattr(part, "datasheet", "") or "",
"draw_cmds": draw_cmds,
"units": units,
}
def _component(part):
"""Serialize one placeable component instance (electrical + hints)."""
pins = []
for unit_num, unit in _units_of(part):
for pin in unit.pins:
net = getattr(pin, "net", None)
pins.append(
{
"num": str(pin.num),
"unit": unit_num,
"net": net.name if net is not None else None,
}
)
return {
"ref": part.ref,
"lib_id": _lib_id_of(part),
"value": part.value_to_str(),
"footprint": getattr(part, "footprint", "") or "",
"hiertuple": list(part.hiertuple),
# --- layout hints ---
"symtx": getattr(part, "symtx", "") or "",
"orientation_locked": bool(getattr(part, "orientation_locked", False)),
"pins": pins,
# --- additive (schema v1): user part fields (e.g. MPN, manufacturer, DNP) ---
"fields": {
str(k): _json_value(v) for k, v in (getattr(part, "fields", {}) or {}).items()
},
}
def _net(net, code):
"""Serialize one net: connectivity plus stub/io hints."""
nodes = []
for pin in sorted(net.pins, key=str):
try:
_, _, pin_type = pin.get_pin_info()
except Exception:
pin_type = ""
nodes.append(
{
"ref": pin.part.ref,
"pin": str(pin.num),
"pintype": pin_type,
}
)
return {
"name": net.name,
"code": code,
# --- layout hints ---
# ``stub`` is the effective value; ``stub_explicit`` records whether the
# user set it (vs. an automatic heuristic the downstream tool may apply).
"stub": bool(getattr(net, "stub", False)),
"stub_explicit": bool(getattr(net, "_stub_explicit", False)),
"netio": (getattr(net, "netio", "") or "").lower(),
"implicit": bool(net.is_implicit()),
"nodes": nodes,
# --- additive (schema v1): net attributes for layout ---
"attrs": _net_attrs(net),
}
def _net_attrs(net):
"""Net attributes used by layout tools: net classes, lowest priority first."""
try:
classes = net.netclasses.by_priority()
except AttributeError:
classes = []
return {"netclasses": classes} if classes else {}
def _netclass_defs(circuit):
"""Attributes of every net class defined in the circuit, keyed by class name."""
defs = {}
for name, nc in sorted(getattr(circuit, "netclasses", {}).items()):
defs[str(name)] = {
str(k): _json_value(v) for k, v in vars(nc).items() if k != "name"
}
return defs
[docs]
@export_to_all
def build_generic_netlist(circuit, title="", top_name=""):
"""Build a generic, hierarchical netlist document from a circuit.
Args:
circuit (Circuit): The circuit to serialize. It should already be
merged (``merge_net_names``/``merge_nets``) as ``generate_schematic``
does before calling this.
title (str): Optional schematic title carried through to the tool.
top_name (str): Optional top-of-hierarchy name for output filenames.
Returns:
dict: A JSON-serializable document with ``symbols``, ``components``,
``nets`` and ``sheets`` sections. The structure is stable and additive;
readers should ignore unknown keys.
"""
# Real parts only. NetTerminals are a placement-time construct created by
# the downstream tool, so they never appear here.
parts = [p for p in circuit.parts if getattr(p, "name", None) != "NT"]
# Embedded symbol definitions, one per unique lib_id (deduplicated).
symbols = {}
for part in parts:
lib_id = _lib_id_of(part)
if lib_id not in symbols:
symbols[lib_id] = _symbol_def(part)
# Components, sorted by reference for deterministic output.
components = [_component(p) for p in sorted(parts, key=lambda p: str(p.ref))]
# Nets, sorted by name; assign sequential codes (matches gen_netlist).
nets = []
sorted_nets = sorted(circuit.get_nets(), key=lambda n: str(n.name))
for code, net in enumerate(sorted_nets, 1):
nets.append(_net(net, code))
# The no-connect net is excluded by get_nets(), but its pins need no-connect
# flags, so emit it last with the "nc" flag the reader uses to recognize it.
nc_net = getattr(circuit, "NC", None)
part_ids = {id(p) for p in parts}
nc_pins = [p for p in getattr(nc_net, "pins", []) if id(p.part) in part_ids]
if nc_pins:
nc_entry = _net(nc_net, len(nets) + 1)
nc_entry["nc"] = True
nets.append(nc_entry)
# Hierarchy sheet names (each is a hiertuple, e.g. ("", "stage1")).
sheets = [list(name) for name in circuit.get_node_names()]
return {
"format": SCHEMA_NAME,
"version": SCHEMA_VERSION,
"title": title,
"top_name": top_name,
"sheets": sheets,
"symbols": symbols,
"components": components,
"nets": nets,
# --- additive (schema v1): net class definitions referenced by nets[].attrs ---
"netclasses": _netclass_defs(circuit),
}