tools/epro2: add ProjectRelations for cross-document resolution
per-doc Relations 在大量 cross-doc 引用前是不够的:PCB 的 PAD_NET 复合
id [PAD_NET, comp, pin, pad] 里的 pad 实际是 FOOTPRINT 文档里的 pad
实例;SCH_PAGE 的 COMPONENT.partId 指向某个 SYMBOL 文档的 PART.id。
ProjectRelations 在 per-doc Relations 之上做项目级聚合,把这些跨文档
引用拼起来。
Probe 阶段(ESP-VoCat)发现的映射规则(已写入 docstring):
1. SCH_PAGE COMPONENT.partId === PART.id in some SYMBOL doc
- 命名两种风格:'pid<hex>' (anonymous/系统 part) + '<name>.<n>' (具
名 SKU),但都直接相等 PART.id,**不**是不同 namespace
- 同一 PART.id 可能出现在多个 SYMBOL 文档里(库快照),
parts_by_id 保留全部,consumer 通常取第一个
2. PCB COMPONENT.id → FOOTPRINT 文档 UUID via 单独 ATTR op:
ATTR(parentId=<comp>, key="Footprint", value=<fp_doc_uuid>)
COMPONENT.attrs 子 dict 只有内务字段(Unique ID / Channel ID / ...),
**不**含 footprint 引用。这跟 schematic 的 partId 在 COMPONENT 上的
做法不一样,是 EPRO2 流的一处不对称
3. PCB PAD_NET[comp,pin,pad] 里的 pad 是 FOOTPRINT 文档内部的 pad id;
解析链: comp → ATTR Footprint → FOOTPRINT relations.pads[pad]
API:
ProjectRelations.build(project) — 单遍构建
resolve_symbol_docs(sch_uuid, comp_id) → [SYMBOL doc uuids]
resolve_footprint_doc(pcb_uuid, comp_id) → FOOTPRINT doc uuid | None
pad_in_footprint(fp_uuid, pad_id) → PAD payload | None
resolve_pcb_pad_net(pcb_uuid, comp, pin, pad) → {footprint, pad} | None
attrs_for_pcb_component(pcb_uuid, comp_id) → {key: value} 折叠
CLI 加 --project-relations,跑 ESP-VoCat:
documents 278
distinct_parts 87
duplicated_parts 9
pcb_components_with_footprint 206
pcb_components_unresolved_footprint 0
sch_components_with_partid 572
sch_components_unresolved_part 0
PCB 样本验证:comp=e0 → fp=1069352d81c6 Designator='U8',
PAD_NET pin=1 pad=e7 net=GND 跨文档解到坐标 (-37.4,-45.24)。
测试:6 个新单测覆盖 partId→symbol、comp→footprint、PAD_NET 跨文档、
attrs 折叠、unresolved 计数。parser + relations + project_relations
共 21/21 通过。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import sys
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
from .project_relations import ProjectRelations
|
||||
from .relations import Relations
|
||||
from .replay import Project, replay_project
|
||||
|
||||
@@ -112,6 +113,59 @@ def _print_relations(proj: Project) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _print_project_relations(proj: Project) -> None:
|
||||
"""Cross-doc resolution stats + a few sample resolutions for sanity."""
|
||||
pr = ProjectRelations.build(proj)
|
||||
s = pr.summary()
|
||||
print()
|
||||
print("=" * 72)
|
||||
print("Project Relations (cross-doc)")
|
||||
print("-" * 72)
|
||||
for k, v in s.items():
|
||||
print(f" {k:<40s} {v}")
|
||||
|
||||
# Show 3 sample SCH_PAGE component → SYMBOL resolutions
|
||||
print()
|
||||
print("Sample sch component → symbol resolutions:")
|
||||
n = 0
|
||||
for sch_uuid in pr.docs_by_type.get("SCH_PAGE", []):
|
||||
for cid in pr.per_doc[sch_uuid].components:
|
||||
symbols = pr.resolve_symbol_docs(sch_uuid, cid)
|
||||
pid = pr.component_to_partid.get((sch_uuid, cid))
|
||||
if symbols:
|
||||
print(f" sch={sch_uuid[:12]} comp={cid} partId={pid!r} → symbol={symbols[0][:12]} (+{len(symbols)-1})")
|
||||
n += 1
|
||||
if n >= 3: break
|
||||
if n >= 3: break
|
||||
|
||||
# Show 3 sample PCB component → FOOTPRINT resolutions, and a PAD_NET cross-doc resolution
|
||||
print()
|
||||
print("Sample pcb component → footprint + first PAD_NET cross-doc:")
|
||||
n = 0
|
||||
for pcb_uuid in pr.docs_by_type.get("PCB", []):
|
||||
rel = pr.per_doc[pcb_uuid]
|
||||
for cid in rel.components:
|
||||
fp = pr.resolve_footprint_doc(pcb_uuid, cid)
|
||||
if not fp: continue
|
||||
attrs = pr.attrs_for_pcb_component(pcb_uuid, cid)
|
||||
print(f" pcb={pcb_uuid[:12]} comp={cid} → fp={fp[:12]} Designator={attrs.get('Designator')!r} Value={attrs.get('Value')!r}")
|
||||
# Find a PAD_NET referencing this comp and try cross-doc resolve
|
||||
for pad_id, records in rel.pad_nets_by_pad.items():
|
||||
for rec in records:
|
||||
if rec["comp"] != cid: continue
|
||||
resolved = pr.resolve_pcb_pad_net(pcb_uuid, cid, rec["pin"], rec["pad"])
|
||||
if resolved:
|
||||
pad = resolved["pad"]
|
||||
print(f" PAD_NET pin={rec['pin']} pad={rec['pad']} net={rec['net_name']} → pad@({pad.get('centerX')},{pad.get('centerY')})")
|
||||
break
|
||||
else:
|
||||
continue
|
||||
break
|
||||
n += 1
|
||||
if n >= 3: break
|
||||
if n >= 3: break
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
ap = argparse.ArgumentParser(description="Replay an EPRO2 project and summarize.")
|
||||
ap.add_argument("project_dir", type=Path, help="data/raw/oshwhub/<project_uuid>/")
|
||||
@@ -126,6 +180,11 @@ def main(argv: list[str] | None = None) -> int:
|
||||
action="store_true",
|
||||
help="build cross-object indices and print per-docType summary",
|
||||
)
|
||||
ap.add_argument(
|
||||
"--project-relations",
|
||||
action="store_true",
|
||||
help="build cross-document indices (partId → SYMBOL, comp → FOOTPRINT, PAD_NET cross-doc)",
|
||||
)
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
proj = replay_project(args.project_dir)
|
||||
@@ -134,6 +193,8 @@ def main(argv: list[str] | None = None) -> int:
|
||||
_dump_doc(proj, doc_id)
|
||||
if args.relations:
|
||||
_print_relations(proj)
|
||||
if args.project_relations:
|
||||
_print_project_relations(proj)
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user