Split Domains by primary supply rail instead of POWER union-find.
Converters bridging VBUS/VSYS/3V3 no longer collapse the whole board into one domain. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
"""Topology-only functional groups for Layout F1 (routing-first floorplan).
|
"""Topology-only functional groups for Layout F1 (routing-first floorplan).
|
||||||
|
|
||||||
No millimetres. Domains = power-net islands; satellites = 1-hop neighbors
|
No millimetres. Domains = primary supply-rail clusters (not transitive
|
||||||
|
POWER connectivity through converters); satellites = 1-hop neighbors
|
||||||
classified with role_hint; layout_rules attached from IC extraction when present.
|
classified with role_hint; layout_rules attached from IC extraction when present.
|
||||||
|
|
||||||
Self-contained helpers (no import of ``validate`` / Anthropic).
|
Self-contained helpers (no import of ``validate`` / Anthropic).
|
||||||
@@ -331,52 +332,90 @@ def _net_is_ic_supply(
|
|||||||
return _is_power_net(graph, net_name)
|
return _is_power_net(graph, net_name)
|
||||||
|
|
||||||
|
|
||||||
|
_UPSTREAM_BUS_RE = re.compile(
|
||||||
|
r"(?:^|[_/\-])(VBUS|VBAT|VIN|VCHG|VAC|VPH)(?:$|[_/\-\d])",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_OUTPUT_BUS_RE = re.compile(
|
||||||
|
r"(?:^|[_/\-])(VSYS|VOUT|VREG)(?:$|[_/\-\d])",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_REGULATED_RAIL_RE = re.compile(r"^\+?\d+V\d*", re.IGNORECASE)
|
||||||
|
|
||||||
|
|
||||||
|
def _primary_supply_net(comp: Component, power_nets: set[str]) -> str | None:
|
||||||
|
"""Pick one supply rail per IC so converters do not merge the whole board.
|
||||||
|
|
||||||
|
Consumers prefer regulated digital rails (3V3 / VDD). Power ICs prefer
|
||||||
|
output-ish nets (VSYS / VOUT / regulated) over upstream buses (VBUS / VIN).
|
||||||
|
"""
|
||||||
|
if not power_nets:
|
||||||
|
return None
|
||||||
|
|
||||||
|
sub = (comp.component_subtype or "").lower()
|
||||||
|
is_power_ic = sub.startswith("ic.power")
|
||||||
|
|
||||||
|
def score(name: str) -> tuple[int, str]:
|
||||||
|
u = name.upper()
|
||||||
|
s = 0
|
||||||
|
if _UPSTREAM_BUS_RE.search(u):
|
||||||
|
s -= 100
|
||||||
|
if _OUTPUT_BUS_RE.search(u):
|
||||||
|
s += 50
|
||||||
|
if _REGULATED_RAIL_RE.match(u):
|
||||||
|
s += 40
|
||||||
|
if "3V3" in u or "3.3V" in u:
|
||||||
|
s += 25
|
||||||
|
elif re.search(r"1V\d+|1\.?\d+V", u):
|
||||||
|
s += 10 # core rails still regulated, but secondary to I/O
|
||||||
|
if "VDD" in u or "VCC" in u:
|
||||||
|
s += 15
|
||||||
|
if is_power_ic:
|
||||||
|
if _UPSTREAM_BUS_RE.search(u):
|
||||||
|
s -= 40
|
||||||
|
if _OUTPUT_BUS_RE.search(u) or _REGULATED_RAIL_RE.match(u):
|
||||||
|
s += 30
|
||||||
|
return (s, name)
|
||||||
|
|
||||||
|
return max(power_nets, key=score)
|
||||||
|
|
||||||
|
|
||||||
|
def _domain_id_for_rail(rail: str) -> str:
|
||||||
|
safe = re.sub(r"[^A-Za-z0-9]+", "_", rail or "").strip("_")
|
||||||
|
return f"domain_{safe}" if safe else "domain_unknown"
|
||||||
|
|
||||||
|
|
||||||
def _build_domains(graph: DesignGraph, ic_refs: list[str]) -> list[PlacementDomain]:
|
def _build_domains(graph: DesignGraph, ic_refs: list[str]) -> list[PlacementDomain]:
|
||||||
parent = {r: r for r in ic_refs}
|
"""Cluster ICs by primary supply rail (not union-find across converters)."""
|
||||||
|
|
||||||
def find(x: str) -> str:
|
|
||||||
while parent[x] != x:
|
|
||||||
parent[x] = parent[parent[x]]
|
|
||||||
x = parent[x]
|
|
||||||
return x
|
|
||||||
|
|
||||||
def union(a: str, b: str) -> None:
|
|
||||||
ra, rb = find(a), find(b)
|
|
||||||
if ra != rb:
|
|
||||||
parent[rb] = ra
|
|
||||||
|
|
||||||
power_by_ic: dict[str, set[str]] = {}
|
power_by_ic: dict[str, set[str]] = {}
|
||||||
for ref in ic_refs:
|
for ref in ic_refs:
|
||||||
nets = set()
|
nets: set[str] = set()
|
||||||
for n in graph.nets_of_component(ref):
|
for n in graph.nets_of_component(ref):
|
||||||
if _is_power_net(graph, n) and not _is_ground_net(graph, n):
|
if _is_power_net(graph, n) and not _is_ground_net(graph, n):
|
||||||
nets.add(n)
|
nets.add(n)
|
||||||
power_by_ic[ref] = nets
|
power_by_ic[ref] = nets
|
||||||
|
|
||||||
rail_owners: dict[str, list[str]] = {}
|
by_rail: dict[str, list[str]] = {}
|
||||||
for ref, nets in power_by_ic.items():
|
no_rail: list[str] = []
|
||||||
for n in nets:
|
|
||||||
rail_owners.setdefault(n, []).append(ref)
|
|
||||||
for refs in rail_owners.values():
|
|
||||||
for i in range(1, len(refs)):
|
|
||||||
union(refs[0], refs[i])
|
|
||||||
|
|
||||||
clusters: dict[str, list[str]] = {}
|
|
||||||
for ref in ic_refs:
|
for ref in ic_refs:
|
||||||
clusters.setdefault(find(ref), []).append(ref)
|
primary = _primary_supply_net(graph.components[ref], power_by_ic[ref])
|
||||||
|
if primary is None:
|
||||||
|
no_rail.append(ref)
|
||||||
|
else:
|
||||||
|
by_rail.setdefault(primary, []).append(ref)
|
||||||
|
|
||||||
domains: list[PlacementDomain] = []
|
domains: list[PlacementDomain] = []
|
||||||
for i, (_root, members) in enumerate(
|
for rail, members in sorted(by_rail.items(), key=lambda x: x[0].upper()):
|
||||||
sorted(clusters.items(), key=lambda x: sorted(x[1])[0]),
|
|
||||||
):
|
|
||||||
members_sorted = sorted(members)
|
|
||||||
rails: set[str] = set()
|
|
||||||
for m in members_sorted:
|
|
||||||
rails |= power_by_ic.get(m, set())
|
|
||||||
domains.append(PlacementDomain(
|
domains.append(PlacementDomain(
|
||||||
domain_id=f"domain_{i + 1}",
|
domain_id=_domain_id_for_rail(rail),
|
||||||
power_nets=sorted(rails),
|
power_nets=[rail],
|
||||||
ic_refs=members_sorted,
|
ic_refs=sorted(members),
|
||||||
|
))
|
||||||
|
if no_rail:
|
||||||
|
domains.append(PlacementDomain(
|
||||||
|
domain_id="domain_unpowered",
|
||||||
|
power_nets=[],
|
||||||
|
ic_refs=sorted(no_rail),
|
||||||
))
|
))
|
||||||
return domains
|
return domains
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Pinscope.
|
What's new in Pinscope.
|
||||||
|
|
||||||
|
## 2.28.6 — 2026-09-13 — Split Domains by primary supply rail
|
||||||
|
|
||||||
|
Domains no longer merge the whole board when a charger/LDO bridges VBUS→VSYS→3V3. Each IC is clustered by its primary supply rail (output for power ICs, regulated digital rail for consumers).
|
||||||
|
|
||||||
|
- [Fixed] `_build_domains` groups by primary rail instead of transitive POWER union-find.
|
||||||
|
- [Changed] Domain ids use the rail name (e.g. `domain_3V3_DIGITAL`).
|
||||||
|
|
||||||
## 2.28.5 — 2026-09-13 — Domains & power-rail topology views
|
## 2.28.5 — 2026-09-13 — Domains & power-rail topology views
|
||||||
|
|
||||||
Browse routing-first functional groups from the project sidebar: Domains (power-net islands) and Power rails, each highlighting IC groups and satellite roles.
|
Browse routing-first functional groups from the project sidebar: Domains (power-net islands) and Power rails, each highlighting IC groups and satellite roles.
|
||||||
|
|||||||
@@ -58,6 +58,92 @@ def test_domains_cover_all_ics():
|
|||||||
assert all(d.assemble_order for d in report.domains)
|
assert all(d.assemble_order for d in report.domains)
|
||||||
|
|
||||||
|
|
||||||
|
def test_simple_project_splits_5v_and_3v3_domains():
|
||||||
|
"""LDO bridges +5V/+3V3 electrically but domains follow primary rails."""
|
||||||
|
report = build_functional_groups(_graph())
|
||||||
|
by_rail = {d.power_nets[0]: set(d.ic_refs) for d in report.domains if d.power_nets}
|
||||||
|
assert by_rail.get("+5V") == {"U2"}
|
||||||
|
assert by_rail.get("+3V3") == {"U1", "U3"}
|
||||||
|
assert len(report.domains) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_multi_rail_board_does_not_collapse_to_one_domain():
|
||||||
|
"""Charger→LDO→MCU must not become a single domain via shared POWER nets."""
|
||||||
|
from backend.pinscopex.models import (
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
DesignGraph,
|
||||||
|
Net,
|
||||||
|
NetType,
|
||||||
|
PinConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"U1": Component(
|
||||||
|
reference="U1", value="BQ25896", footprint="",
|
||||||
|
component_type=ComponentType.IC,
|
||||||
|
component_subtype="ic.power.battery_charger",
|
||||||
|
pins={"1": "VBUS", "2": "VSYS", "3": "GND"},
|
||||||
|
),
|
||||||
|
"U2": Component(
|
||||||
|
reference="U2", value="AP2112", footprint="",
|
||||||
|
component_type=ComponentType.IC,
|
||||||
|
component_subtype="ic.power.ldo",
|
||||||
|
pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND"},
|
||||||
|
),
|
||||||
|
"U3": Component(
|
||||||
|
reference="U3", value="ESP32", footprint="",
|
||||||
|
component_type=ComponentType.IC,
|
||||||
|
component_subtype="ic.rf.wifi_module",
|
||||||
|
pins={"1": "3V3_DIGITAL", "2": "GND"},
|
||||||
|
),
|
||||||
|
"U4": Component(
|
||||||
|
reference="U4", value="SRV05", footprint="",
|
||||||
|
component_type=ComponentType.IC,
|
||||||
|
component_subtype="ic.protection.esd",
|
||||||
|
pins={"1": "VBUS", "2": "GND"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
nets = {
|
||||||
|
"VBUS": Net(
|
||||||
|
name="VBUS", net_type=NetType.POWER,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="1"),
|
||||||
|
PinConnection(component_ref="U4", pin_number="1"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"VSYS": Net(
|
||||||
|
name="VSYS", net_type=NetType.POWER,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="2"),
|
||||||
|
PinConnection(component_ref="U2", pin_number="1"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"3V3_DIGITAL": Net(
|
||||||
|
name="3V3_DIGITAL", net_type=NetType.POWER,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U2", pin_number="2"),
|
||||||
|
PinConnection(component_ref="U3", pin_number="1"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"GND": Net(
|
||||||
|
name="GND", net_type=NetType.GROUND,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="3"),
|
||||||
|
PinConnection(component_ref="U2", pin_number="3"),
|
||||||
|
PinConnection(component_ref="U3", pin_number="2"),
|
||||||
|
PinConnection(component_ref="U4", pin_number="2"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
report = build_functional_groups(DesignGraph(components=components, nets=nets))
|
||||||
|
by_rail = {d.power_nets[0]: set(d.ic_refs) for d in report.domains if d.power_nets}
|
||||||
|
assert len(report.domains) >= 3
|
||||||
|
assert by_rail.get("VBUS") == {"U4"}
|
||||||
|
assert by_rail.get("VSYS") == {"U1"}
|
||||||
|
assert by_rail.get("3V3_DIGITAL") == {"U2", "U3"}
|
||||||
|
|
||||||
|
|
||||||
def test_build_placement_plan_alias():
|
def test_build_placement_plan_alias():
|
||||||
from backend.pinscopex.functional_groups import build_placement_plan
|
from backend.pinscopex.functional_groups import build_placement_plan
|
||||||
report = build_placement_plan(_graph())
|
report = build_placement_plan(_graph())
|
||||||
|
|||||||
Reference in New Issue
Block a user