Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
9d204aa
fix(setup): 修复 Windows 下 editable 安装的 UTF-8 编码问题
Doris619619 May 13, 2026
683d487
feat(pnr): 增加 human_readable 布局与走线稳定化
Doris619619 May 15, 2026
198fd15
Merge pull request #1 from Doris619619/feat/pnr-optimization
Doris619619 May 15, 2026
6cf4631
feat(schematic): 新增小组弱对齐模块并修正 human_readable 布局过强对齐
Doris619619 May 16, 2026
c9eb282
Merge pull request #2 from Doris619619/feat/pnr-optimization
Doris619619 May 16, 2026
c52dbe6
fix(symbol): 兼容脏网表的项目符号库映射
Doris619619 May 17, 2026
f3f47dc
docs(update):演示
Doris619619 May 18, 2026
e5b9669
Merge pull request #3 from Doris619619/feat/pnr-optimization
Doris619619 May 18, 2026
534adc5
add local detour cleanup optimization
rhaingenix May 19, 2026
4513339
feat(sch): 支持配置原理图布线疏松程度
Doris619619 May 19, 2026
f606d45
feat(sch): 支持配置原理图布线疏松程度
Doris619619 May 19, 2026
d7ea0f3
feat(route): 用带方向状态的 Dijkstra 优化全局布线
Doris619619 May 19, 2026
a950d5c
feat(route): 新增 reuse_junctions 走线复用参数
Doris619619 May 19, 2026
019166c
fix large schematic graph routing issue
rhaingenix May 19, 2026
5aac223
Merge pull request #6 from Doris619619/b1
Doris619619 May 19, 2026
909f3a6
Merge branch 'master' into feat/pnr-optimization
Doris619619 May 19, 2026
32d59c0
Merge pull request #5 from Doris619619/feat/pnr-optimization
Doris619619 May 19, 2026
6d2b171
fix(schematic): 修复 cleanup 去 jog 振荡并增加布线阶段日志
Doris619619 May 19, 2026
bbfa3b5
Merge origin/feat/pnr-optimization: 合并直线路由与 cleanup 防振荡
Doris619619 May 19, 2026
ee26aba
Merge pull request #7 from Doris619619/feat/pnr-optimization
Doris619619 May 19, 2026
1f32521
feat(sch): 为 human_readable 的 driver 拓扑增加 rail 布局与预布线
Doris619619 May 21, 2026
34bf188
fix(schematic): 修正 driver 顶底 rail 距器件过远
Doris619619 May 22, 2026
e67f5f5
Merge pull request #8 from Doris619619/feat/pnr-optimization
Doris619619 May 22, 2026
e8e053f
Add device name and reference preservation
rhaingenix May 23, 2026
c1d623f
merge master into addname
rhaingenix May 25, 2026
0ffb9fa
improve driver rail routing and semantic trunk layout
rhaingenix May 25, 2026
482a8ea
Guard trunk routing for local nets
rhaingenix May 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,253 changes: 1,253 additions & 0 deletions and_gate.kicad_sch

Large diffs are not rendered by default.

8 changes: 5 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
__email__ = "dave@vdb.name"

if "sdist" in sys.argv[1:]:
with open("src/skidl/pckg_info.py", "w") as f:
# Windows 默认编码常为 GBK,显式 UTF-8 避免写入非 ASCII 时出错
with open("src/skidl/pckg_info.py", "w", encoding="utf-8") as f:
for name in ["__version__", "__author__", "__email__"]:
f.write('{} = "{}"\n'.format(name, locals()[name]))

Expand All @@ -20,10 +21,11 @@
from distutils.core import setup


with open("README.md") as readme_file:
# README/HISTORY 为 UTF-8;不设 encoding 时 Windows 会用系统编码读文件,pip 构建会报 UnicodeDecodeError
with open("README.md", encoding="utf-8") as readme_file:
readme = readme_file.read()

with open("HISTORY.md") as history_file:
with open("HISTORY.md", encoding="utf-8") as history_file:
history = history_file.read()

requirements = [
Expand Down
162 changes: 161 additions & 1 deletion src/skidl/circuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import builtins
import json
import re
import subprocess
from collections import Counter, deque

Expand Down Expand Up @@ -72,6 +73,41 @@ class Circuit(SkidlBaseObject):

# Set the default ERC functions for all Circuit instances.
erc_list = [dflt_circuit_erc]
_ANNOTATION_PREFIX_RULES = (
("connector", "J"),
("transistor", "Q"),
("mosfet", "Q"),
("fet", "Q"),
("bjt", "Q"),
("igbt", "Q"),
("diode", "D"),
("led", "D"),
("zener", "D"),
("resistor", "R"),
("r_array", "R"),
("potentiometer", "R"),
("trimmer", "R"),
("capacitor", "C"),
("cap", "C"),
("inductor", "L"),
("coil", "L"),
("ic", "U"),
("opamp", "U"),
("amplifier", "U"),
("comparator", "U"),
("buffer", "U"),
("logic", "U"),
("driver", "U"),
("interface", "U"),
("memory", "U"),
("sensor", "U"),
("regulator", "U"),
("power", "U"),
("mcu", "U"),
("cpu", "U"),
("fpga", "U"),
)
_REF_NUM_RE = re.compile(r"^(.*?)(\d+)$")

def __init__(self, **attrs):
"""
Expand Down Expand Up @@ -369,7 +405,16 @@ def add_parts(self, *parts):

# Add the part to this circuit.
part.circuit = self # Record the Circuit object for this part.
part.ref = part.ref # Adjusts the part reference if necessary.
clean_prefix = self._infer_ref_prefix(part)
part.ref_prefix = clean_prefix
if self._ref_is_placeholder(part.ref):
# Replace library placeholders such as D? immediately so
# later export stages always see a concrete reference.
part.ref = None
else:
part.ref = part.ref # Adjusts the part reference if necessary.
if getattr(part, "ref", None):
self._sync_reference_metadata(part, part.ref)

# Add the part to the currently active node.
self.active_node.parts.append(part)
Expand All @@ -385,6 +430,105 @@ def add_parts(self, *parts):
f"Can't add unmovable part {part.ref} to this circuit.",
)

@classmethod
def _ref_is_placeholder(cls, ref):
"""Return True if a reference is empty or still using a library placeholder."""
ref = str(ref or "").strip()
return not ref or "?" in ref

@classmethod
def _split_annotated_ref(cls, ref):
"""Return ``(prefix, number)`` for refs like ``R12`` or ``None`` if not numbered."""
ref = str(ref or "").strip()
match = cls._REF_NUM_RE.match(ref)
if not match:
return None
return match.group(1), int(match.group(2))

@classmethod
def _infer_ref_prefix(cls, part):
"""Infer a human-readable reference prefix for a part."""
raw_prefix = str(getattr(part, "ref_prefix", "") or "").strip().upper().rstrip("?")
if raw_prefix and raw_prefix not in {"?", "#"}:
return raw_prefix

search_fields = (
getattr(part, "name", ""),
getattr(part, "description", ""),
getattr(getattr(part, "lib", None), "filename", ""),
)
search_text = " ".join(str(field or "").lower() for field in search_fields)

for needle, prefix in cls._ANNOTATION_PREFIX_RULES:
if needle in search_text:
return prefix

ref = str(getattr(part, "ref", "") or "").strip().upper()
if ref:
inferred = "".join(ch for ch in ref if ch.isalpha() or ch == "#").rstrip("?")
if inferred:
return inferred

return "U"

@staticmethod
def _sync_reference_metadata(part, full_ref):
"""Propagate an assigned reference into common metadata containers."""
if hasattr(part, "fields") and isinstance(part.fields, dict):
part.fields["Reference"] = full_ref
part.fields["reference"] = full_ref
if hasattr(part, "properties") and isinstance(part.properties, dict):
part.properties["Reference"] = full_ref
part.properties["reference"] = full_ref

def annotate_parts(self, force=False):
"""Assign human-readable sequential references to parts lacking annotation.

Args:
force (bool): When True, renumber all parts even if they already have
numbered references. By default, only empty / placeholder refs such
as ``R?`` and ``U?`` are replaced.
"""
counters = Counter()
used_refs = set()

# First reserve any existing numbered references so explicit annotations stay stable.
for part in self.parts:
ref = str(getattr(part, "ref", "") or "").strip()
split_ref = self._split_annotated_ref(ref)
if force or self._ref_is_placeholder(ref) or not split_ref:
continue

prefix, number = split_ref
used_refs.add(ref)
counters[prefix] = max(counters[prefix], number + 1)

# Then fill in missing / placeholder references using inferred prefixes.
for part in self.parts:
ref = str(getattr(part, "ref", "") or "").strip()
split_ref = self._split_annotated_ref(ref)
if not force and split_ref and not self._ref_is_placeholder(ref):
continue

prefix = self._infer_ref_prefix(part)
part.ref_prefix = prefix
next_num = max(1, counters[prefix])
new_ref = f"{prefix}{next_num}"
while new_ref in used_refs:
next_num += 1
new_ref = f"{prefix}{next_num}"

part.ref = new_ref
self._sync_reference_metadata(part, new_ref)
used_refs.add(new_ref)
counters[prefix] = next_num + 1

# Keep metadata synchronized even for already-annotated parts.
for part in self.parts:
ref = str(getattr(part, "ref", "") or "").strip()
if ref:
self._sync_reference_metadata(part, ref)

def rmv_parts(self, *parts):
"""
Remove parts from the circuit.
Expand Down Expand Up @@ -1281,6 +1425,9 @@ def generate_schematic(self, **kwargs):
Args:
**kwargs: Arguments for the schematic generator including:
empty_footprint_handler (function, optional): Custom handler for parts without footprints.
annotate_refs (bool, optional): Automatically replace placeholder
references such as ``R?`` and ``U?`` with numbered references
before schematic export. Defaults to True.
tool (str, optional): The EDA tool to generate the schematic for.
"""

Expand Down Expand Up @@ -1312,6 +1459,8 @@ def _empty_footprint_handler(part):

self.merge_net_names()
self.merge_nets() # Merge nets or schematic routing will fail.
if kwargs.pop("annotate_refs", True):
self.annotate_parts()

tool = kwargs.pop("tool", skidl.config.tool)

Expand All @@ -1322,6 +1471,17 @@ def _empty_footprint_handler(part):

active_logger.report_summary("generating schematic")

# topology 摘要放在 schematic 阶段 warnings/errors 汇总之后,便于 grep 识别结果。
sch_root = getattr(self, "_schematic_sch_root", None)
if sch_root is not None:
from skidl.schematics.topology import log_topology_summaries_deep

# 收尾 topology 行紧跟 warnings/errors 汇总,不受 schematic_progress 关闭影响。
topo_log_opts = dict(kwargs)
topo_log_opts["schematic_progress"] = True
log_topology_summaries_deep(sch_root, topo_log_opts)
delattr(self, "_schematic_sch_root")

def generate_dot(
self,
file_=None,
Expand Down
71 changes: 70 additions & 1 deletion src/skidl/netlist_to_skidl.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,49 @@
from .logger import active_logger # Import the active_logger


_REFERENCE_ID_RE = re.compile(r"^([A-Za-z#]+)(\d+)(.*)$")
_REFERENCE_PREFIX_RE = re.compile(r"^([A-Za-z#]+)")


def get_ref_prefix(ref: str) -> str:
"""Return the alphabetic reference prefix from a component reference."""
match = _REFERENCE_PREFIX_RE.match(str(ref or "").strip())
return match.group(1) if match else "X"


def get_ref_number(ref: str):
"""Return the numeric suffix from a component reference if present."""
match = _REFERENCE_ID_RE.match(str(ref or "").strip())
return int(match.group(2)) if match else None


def normalize_reference(ref: str) -> str:
"""Normalize a parsed component reference into its canonical string form."""
ref = str(ref or "").strip()
return ref if ref else "X"


def annotate_components(components):
"""Assign stable full references to components that lack numeric refs."""
counters = defaultdict(int)

for comp in components:
prefix = comp.ref_prefix
if comp.ref_number is not None:
counters[prefix] = max(counters[prefix], comp.ref_number + 1)

for comp in components:
if comp.ref_number is not None:
continue
prefix = comp.ref_prefix or get_ref_prefix(comp.name) or "X"
next_num = max(counters[prefix], 1)
comp.ref = f"{prefix}{next_num}"
comp.ref_prefix = prefix
comp.ref_number = next_num
counters[prefix] = next_num + 1
comp.set_property("Reference", comp.ref)


class Sheet:
"""
Represents a hierarchical sheet from a KiCad schematic.
Expand Down Expand Up @@ -131,12 +174,24 @@ class PartSexp:

def __init__(self, sexp):
self.sheetpath = sexp.search("/comp/sheetpath/names").value
self.ref = sexp.search("/comp/ref").value
self.original_ref = sexp.search("/comp/ref").value
self.ref = normalize_reference(self.original_ref)
self.ref_prefix = get_ref_prefix(self.ref)
self.ref_number = get_ref_number(self.ref)
self.value = sexp.search("/comp/value").value
self.footprint = sexp.search("/comp/footprint").value
self.name = sexp.search("/comp/libsource/part").value
self.lib = sexp.search("/comp/libsource/lib").value
self.properties = [PropertySexp(prop) for prop in sexp.search("/comp/property")]
self.set_property("Reference", self.ref)

def set_property(self, name, value):
"""Set or create a parsed component property."""
for prop in self.properties:
if prop.name == name:
prop.value = value
return
self.properties.append(PropertySexp.from_value(name, value))


class PropertySexp:
Expand All @@ -148,6 +203,13 @@ def __init__(self, sexp):
self.name = sexp.search("/property/name").value
self.value = sexp.search("/property/value").value

@classmethod
def from_value(cls, name, value):
prop = cls.__new__(cls)
prop.name = name
prop.value = value
return prop


class PinSexp:
"""
Expand Down Expand Up @@ -185,7 +247,12 @@ class NetlistSexp:
def __init__(self, sexp):
self.sheets = [SheetSexp(sht) for sht in sexp.search("design/sheet")]
self.parts = [PartSexp(comp) for comp in sexp.search("components/comp")]
annotate_components(self.parts)
self.nets = [NetSexp(net) for net in sexp.search("nets/net")]
ref_map = {part.original_ref: part.ref for part in self.parts}
for net in self.nets:
for pin in net.pins:
pin.ref = ref_map.get(pin.ref, pin.ref)


class HierarchicalConverter:
Expand Down Expand Up @@ -458,6 +525,8 @@ def component_to_skidl(self, comp: object) -> str:
desc = next((p.value for p in comp.properties if p.name == "Description"), None)
if desc:
props.append(f"description='{desc}'")
if comp.ref_prefix:
props.append(f"ref_prefix='{comp.ref_prefix}'")
props.append(f"ref='{ref}'")
extra_fields = {}
if hasattr(comp, "properties"):
Expand Down
29 changes: 29 additions & 0 deletions src/skidl/part.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"""

import functools
import re
from collections.abc import Iterable
from copy import copy
from random import randint
Expand Down Expand Up @@ -44,6 +45,17 @@
NETLIST, LIBRARY, TEMPLATE = ["NETLIST", "LIBRARY", "TEMPLATE"]


_REFERENCE_ID_RE = re.compile(r"^([A-Za-z#]+)(\d+)(.*)$")


def split_reference(ref):
"""Return the prefix and numeric suffix from a reference designator."""
match = _REFERENCE_ID_RE.match(str(ref or "").strip())
if not match:
return str(ref or "").strip().rstrip("?"), None
return match.group(1), int(match.group(2))


class PinNumberSearch(object):
"""
A class for restricting part pin indexing to only pin numbers while ignoring pin names.
Expand Down Expand Up @@ -1066,8 +1078,25 @@ def ref(self, r):
# Now name the object with the given reference or some variation
# of it that doesn't collide with anything else in the list.
self._ref = get_unique_name(self.circuit.parts, "ref", self.ref_prefix, r)
self._sync_reference_identity(original_ref=r)
return

def _sync_reference_identity(self, original_ref=None):
"""Store the assigned full reference as this part's stable identity."""
full_ref = str(self._ref or "").strip()
if original_ref is not None:
self.original_ref = str(original_ref or "").strip()
if not full_ref:
self.ref_number = None
return

prefix, number = split_reference(full_ref)
if prefix:
self.ref_prefix = prefix
self.ref_number = number
self.fields["Reference"] = full_ref
self.fields["reference"] = full_ref

@ref.deleter
def ref(self):
"""
Expand Down
Loading