Keep power satellites on each IC primary rail only.
Stop LDO 3V3 groups from inheriting VSYS caps/filters, drop connector/switch noise, and align Power rails with domain membership. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -177,6 +177,16 @@ def _ic_rank(comp: Component) -> int:
|
|||||||
return 9
|
return 9
|
||||||
|
|
||||||
|
|
||||||
|
_POWER_SAT_ROLES = frozenset({"decoupling", "bulk", "filter", "pullup"})
|
||||||
|
_SKIP_OTHER_TYPES = frozenset({
|
||||||
|
ComponentType.CONNECTOR,
|
||||||
|
ComponentType.SWITCH,
|
||||||
|
ComponentType.TEST_POINT,
|
||||||
|
ComponentType.FIDUCIAL,
|
||||||
|
ComponentType.MECHANICAL,
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
def _group_for_ic(
|
def _group_for_ic(
|
||||||
graph: DesignGraph,
|
graph: DesignGraph,
|
||||||
ref: str,
|
ref: str,
|
||||||
@@ -185,6 +195,8 @@ def _group_for_ic(
|
|||||||
comp = graph.components[ref]
|
comp = graph.components[ref]
|
||||||
cons = _match_constraints(comp.mpn or comp.value, cmap)
|
cons = _match_constraints(comp.mpn or comp.value, cmap)
|
||||||
nets = [n for n in graph.nets_of_component(ref) if not _is_ground_net(graph, n)]
|
nets = [n for n in graph.nets_of_component(ref) if not _is_ground_net(graph, n)]
|
||||||
|
power_nets = {n for n in nets if _is_power_net(graph, n)}
|
||||||
|
primary = _primary_supply_net(comp, power_nets)
|
||||||
sat_map: dict[str, PlacementSatellite] = {}
|
sat_map: dict[str, PlacementSatellite] = {}
|
||||||
|
|
||||||
for net_name, others in graph.neighbors(ref).items():
|
for net_name, others in graph.neighbors(ref).items():
|
||||||
@@ -197,20 +209,33 @@ def _group_for_ic(
|
|||||||
if not other or other.component_type == ComponentType.IC:
|
if not other or other.component_type == ComponentType.IC:
|
||||||
continue
|
continue
|
||||||
role = _role_hint(graph, comp, cons, other, net_name)
|
role = _role_hint(graph, comp, cons, other, net_name)
|
||||||
|
sat_nets = {n for n in other.pins.values() if n}
|
||||||
|
# Keep decoupling/bulk/filter/pullup on the IC primary rail only —
|
||||||
|
# otherwise an LDO on 3V3 also inherits VSYS input caps/inductors.
|
||||||
|
if role in _POWER_SAT_ROLES and primary and primary not in sat_nets:
|
||||||
|
continue
|
||||||
|
if role == "other" and other.component_type in _SKIP_OTHER_TYPES:
|
||||||
|
continue
|
||||||
sat_map[oref] = PlacementSatellite(
|
sat_map[oref] = PlacementSatellite(
|
||||||
ref=oref,
|
ref=oref,
|
||||||
component_type=other.component_type.value,
|
component_type=other.component_type.value,
|
||||||
component_subtype=other.component_subtype,
|
component_subtype=other.component_subtype,
|
||||||
nets=sorted({n for n in other.pins.values() if n}),
|
nets=sorted(sat_nets),
|
||||||
hop=1,
|
hop=1,
|
||||||
role_hint=role,
|
role_hint=role,
|
||||||
)
|
)
|
||||||
|
|
||||||
for pin_num, net_name in comp.pins.items():
|
# Cap enhancement: only on the primary supply rail (when known).
|
||||||
|
supply_nets = [primary] if primary else []
|
||||||
|
if not supply_nets:
|
||||||
|
supply_nets = [
|
||||||
|
n for pin_num, n in comp.pins.items()
|
||||||
|
if n and not _is_ground_net(graph, n)
|
||||||
|
and _is_ic_supply_pin(graph, cons, pin_num, n)
|
||||||
|
]
|
||||||
|
for net_name in supply_nets:
|
||||||
if not net_name or _is_ground_net(graph, net_name):
|
if not net_name or _is_ground_net(graph, net_name):
|
||||||
continue
|
continue
|
||||||
if not _is_ic_supply_pin(graph, cons, pin_num, net_name):
|
|
||||||
continue
|
|
||||||
for cref in graph.capacitors_on_net(net_name):
|
for cref in graph.capacitors_on_net(net_name):
|
||||||
if cref == ref:
|
if cref == ref:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -2,6 +2,13 @@
|
|||||||
|
|
||||||
What's new in Pinscope.
|
What's new in Pinscope.
|
||||||
|
|
||||||
|
## 2.28.9 — 2026-09-13 — Cleaner Domains / Power rails membership
|
||||||
|
|
||||||
|
Power rails follow primary domain only. IC satellites for decoupling/bulk/filter/pullup stay on the IC’s primary supply rail (LDO 3V3 no longer inherits VSYS input caps). Connectors/switches drop out of noisy “other” satellites.
|
||||||
|
|
||||||
|
- [Fixed] Rails view membership = domain primary rail (not transitive satellite nets).
|
||||||
|
- [Fixed] `_group_for_ic` primary-rail filter for power-role satellites; skip connector/switch “other”.
|
||||||
|
|
||||||
## 2.28.8 — 2026-09-13 — Stronger layout_rules extraction skill
|
## 2.28.8 — 2026-09-13 — Stronger layout_rules extraction skill
|
||||||
|
|
||||||
Pintable skill now treats PCB / typical-application layout guidance as a first-class extract. Page trim keeps layout keywords; IC cache re-extracts once when `layout_rules` are empty under an older `model_version` (1.10.0+).
|
Pintable skill now treats PCB / typical-application layout guidance as a first-class extract. Page trim keeps layout keywords; IC cache re-extracts once when `layout_rules` are empty under an older `model_version` (1.10.0+).
|
||||||
|
|||||||
@@ -165,33 +165,23 @@ type RailRow = {
|
|||||||
function RailsView({ plan }: { plan: PlacementPlan }) {
|
function RailsView({ plan }: { plan: PlacementPlan }) {
|
||||||
const rails = useMemo(() => {
|
const rails = useMemo(() => {
|
||||||
const byRef = Object.fromEntries(plan.groups.map((g) => [g.ref, g]));
|
const byRef = Object.fromEntries(plan.groups.map((g) => [g.ref, g]));
|
||||||
|
// One row per domain primary rail — do not pull ICs onto a rail just
|
||||||
|
// because a connector/pullup satellite also touches it.
|
||||||
const map = new Map<string, { domainIds: Set<string>; groupRefs: Set<string> }>();
|
const map = new Map<string, { domainIds: Set<string>; groupRefs: Set<string> }>();
|
||||||
|
|
||||||
for (const dom of plan.domains) {
|
for (const dom of plan.domains) {
|
||||||
for (const net of dom.power_nets) {
|
const primary = dom.power_nets[0];
|
||||||
let entry = map.get(net);
|
if (!primary) continue;
|
||||||
|
let entry = map.get(primary);
|
||||||
if (!entry) {
|
if (!entry) {
|
||||||
entry = { domainIds: new Set(), groupRefs: new Set() };
|
entry = { domainIds: new Set(), groupRefs: new Set() };
|
||||||
map.set(net, entry);
|
map.set(primary, entry);
|
||||||
}
|
}
|
||||||
entry.domainIds.add(dom.domain_id);
|
entry.domainIds.add(dom.domain_id);
|
||||||
for (const iref of dom.ic_refs) entry.groupRefs.add(iref);
|
for (const iref of dom.ic_refs) entry.groupRefs.add(iref);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Also attach groups that list the rail on their nets / satellites
|
const powerRoles = new Set(["decoupling", "bulk", "filter", "pullup"]);
|
||||||
for (const g of plan.groups) {
|
|
||||||
for (const net of g.nets ?? []) {
|
|
||||||
const entry = map.get(net);
|
|
||||||
if (entry) entry.groupRefs.add(g.ref);
|
|
||||||
}
|
|
||||||
for (const s of g.satellites) {
|
|
||||||
for (const net of s.nets ?? []) {
|
|
||||||
const entry = map.get(net);
|
|
||||||
if (entry) entry.groupRefs.add(g.ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rows: RailRow[] = [...map.entries()]
|
const rows: RailRow[] = [...map.entries()]
|
||||||
.map(([net, v]) => ({
|
.map(([net, v]) => ({
|
||||||
@@ -200,7 +190,21 @@ function RailsView({ plan }: { plan: PlacementPlan }) {
|
|||||||
groups: [...v.groupRefs]
|
groups: [...v.groupRefs]
|
||||||
.map((r) => byRef[r])
|
.map((r) => byRef[r])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a, b) => (a.rank ?? 99) - (b.rank ?? 99) || a.ref.localeCompare(b.ref)),
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a.rank ?? 99) - (b.rank ?? 99) || a.ref.localeCompare(b.ref),
|
||||||
|
)
|
||||||
|
.map((g) => {
|
||||||
|
const onRailSats = g.satellites.filter(
|
||||||
|
(s) =>
|
||||||
|
(s.nets || []).includes(net) &&
|
||||||
|
powerRoles.has(s.role_hint || ""),
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...g,
|
||||||
|
satellites: onRailSats,
|
||||||
|
} as PlacementIcGroup;
|
||||||
|
}),
|
||||||
}))
|
}))
|
||||||
.sort((a, b) => a.net.localeCompare(b.net));
|
.sort((a, b) => a.net.localeCompare(b.net));
|
||||||
|
|
||||||
@@ -234,23 +238,11 @@ function RailsView({ plan }: { plan: PlacementPlan }) {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-2">
|
<CardContent className="space-y-2">
|
||||||
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
<p className="text-[11px] font-medium text-muted-foreground uppercase tracking-wide">
|
||||||
Functional groups on this rail
|
Primary IC groups on this rail
|
||||||
</p>
|
</p>
|
||||||
{rail.groups.map((g) => {
|
{rail.groups.map((g) => (
|
||||||
const onRailSats = g.satellites.filter(
|
<GroupBlock key={g.ref} group={g} emphasize />
|
||||||
(s) => (s.nets || []).includes(rail.net),
|
))}
|
||||||
);
|
|
||||||
const highlight: PlacementIcGroup = {
|
|
||||||
...g,
|
|
||||||
satellites:
|
|
||||||
onRailSats.length > 0
|
|
||||||
? onRailSats
|
|
||||||
: g.satellites.filter((s) =>
|
|
||||||
["decoupling", "bulk"].includes(s.role_hint || ""),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
return <GroupBlock key={g.ref} group={highlight} emphasize />;
|
|
||||||
})}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
))}
|
))}
|
||||||
@@ -315,8 +307,8 @@ export function TopologyPanel({
|
|||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-muted-foreground mt-0.5">
|
<p className="text-xs text-muted-foreground mt-0.5">
|
||||||
{mode === "domains"
|
{mode === "domains"
|
||||||
? "Power-net islands with IC functional groups and satellite roles (routing-first, no mm)."
|
? "Primary supply-rail domains with IC functional groups and satellite roles (routing-first, no mm)."
|
||||||
: "Each supply rail with the functional groups that hang off it."}
|
: "Each primary supply rail with its domain IC groups (not shared connectors across rails)."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{mode === "domains" ? <DomainsView plan={plan} /> : <RailsView plan={plan} />}
|
{mode === "domains" ? <DomainsView plan={plan} /> : <RailsView plan={plan} />}
|
||||||
|
|||||||
@@ -67,6 +67,86 @@ def test_simple_project_splits_5v_and_3v3_domains():
|
|||||||
assert len(report.domains) == 2
|
assert len(report.domains) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_ldo_power_satellites_stay_on_primary_rail():
|
||||||
|
"""LDO input-rail caps must not appear as primary-rail satellites."""
|
||||||
|
from backend.pinscopex.models import (
|
||||||
|
CapacitorSpecs,
|
||||||
|
Component,
|
||||||
|
ComponentType,
|
||||||
|
DesignGraph,
|
||||||
|
InductorSpecs,
|
||||||
|
Net,
|
||||||
|
NetType,
|
||||||
|
PinConnection,
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"U1": Component(
|
||||||
|
reference="U1", value="LDO", footprint="",
|
||||||
|
component_type=ComponentType.IC,
|
||||||
|
component_subtype="ic.power.ldo",
|
||||||
|
pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND"},
|
||||||
|
),
|
||||||
|
"C_in": Component(
|
||||||
|
reference="C_in", value="10u", footprint="",
|
||||||
|
component_type=ComponentType.CAPACITOR,
|
||||||
|
pins={"1": "VSYS", "2": "GND"},
|
||||||
|
specs=CapacitorSpecs(value_farads=10e-6, value_formatted="10uF"),
|
||||||
|
),
|
||||||
|
"L1": Component(
|
||||||
|
reference="L1", value="2.2u", footprint="",
|
||||||
|
component_type=ComponentType.INDUCTOR,
|
||||||
|
pins={"1": "VSYS", "2": "VSYS"},
|
||||||
|
specs=InductorSpecs(value_henries=2.2e-6, value_formatted="2.2uH"),
|
||||||
|
),
|
||||||
|
"C_out": Component(
|
||||||
|
reference="C_out", value="100n", footprint="",
|
||||||
|
component_type=ComponentType.CAPACITOR,
|
||||||
|
pins={"1": "3V3_DIGITAL", "2": "GND"},
|
||||||
|
specs=CapacitorSpecs(value_farads=100e-9, value_formatted="100nF"),
|
||||||
|
),
|
||||||
|
"J1": Component(
|
||||||
|
reference="J1", value="USB", footprint="",
|
||||||
|
component_type=ComponentType.CONNECTOR,
|
||||||
|
pins={"1": "3V3_DIGITAL", "2": "GND"},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
nets = {
|
||||||
|
"VSYS": Net(
|
||||||
|
name="VSYS", net_type=NetType.POWER,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="1"),
|
||||||
|
PinConnection(component_ref="C_in", pin_number="1"),
|
||||||
|
PinConnection(component_ref="L1", pin_number="1"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"3V3_DIGITAL": Net(
|
||||||
|
name="3V3_DIGITAL", net_type=NetType.POWER,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="2"),
|
||||||
|
PinConnection(component_ref="C_out", pin_number="1"),
|
||||||
|
PinConnection(component_ref="J1", pin_number="1"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
"GND": Net(
|
||||||
|
name="GND", net_type=NetType.GROUND,
|
||||||
|
pins=[
|
||||||
|
PinConnection(component_ref="U1", pin_number="3"),
|
||||||
|
PinConnection(component_ref="C_in", pin_number="2"),
|
||||||
|
PinConnection(component_ref="C_out", pin_number="2"),
|
||||||
|
PinConnection(component_ref="J1", pin_number="2"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
}
|
||||||
|
report = build_functional_groups(DesignGraph(components=components, nets=nets))
|
||||||
|
u1 = next(g for g in report.groups if g.ref == "U1")
|
||||||
|
sat = {s.ref: s.role_hint for s in u1.satellites}
|
||||||
|
assert "C_out" in sat
|
||||||
|
assert "C_in" not in sat
|
||||||
|
assert "L1" not in sat
|
||||||
|
assert "J1" not in sat
|
||||||
|
|
||||||
|
|
||||||
def test_multi_rail_board_does_not_collapse_to_one_domain():
|
def test_multi_rail_board_does_not_collapse_to_one_domain():
|
||||||
"""Charger→LDO→MCU must not become a single domain via shared POWER nets."""
|
"""Charger→LDO→MCU must not become a single domain via shared POWER nets."""
|
||||||
from backend.pinscopex.models import (
|
from backend.pinscopex.models import (
|
||||||
|
|||||||
Reference in New Issue
Block a user