Classify strap/bootstrap caps and drop unclassified satellites.

EN/BOOT/REGN/PMID bypass and flying bootstrap caps get real roles; other is omitted from assemble_order.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 18:37:01 +02:00
co-authored by Cursor
parent d47ad91cee
commit 68af0fd432
4 changed files with 132 additions and 25 deletions
+56 -19
View File
@@ -38,6 +38,30 @@ RoleHint = Literal[
"other", "other",
] ]
# Roles kept in satellites / assemble_order. Unclassified "other" is dropped.
_ASSEMBLE_ROLES = frozenset({
"decoupling", "bulk", "load_cap", "filter", "pullup",
"series", "divider", "bridge", "crystal",
})
_POWER_SAT_ROLES = frozenset({"decoupling", "bulk", "filter", "pullup"})
_SKIP_OTHER_TYPES = frozenset({
ComponentType.CONNECTOR,
ComponentType.SWITCH,
ComponentType.TEST_POINT,
ComponentType.FIDUCIAL,
ComponentType.MECHANICAL,
})
# Bias / charge-pump / bootstrap nets often stay SIGNAL in the graph.
_BIAS_NET_RE = re.compile(
r"(?:^|[_/\-])(REGN|PMID|BTST|BOOT|SW|LX|BST|VREG|VLDO|VREF)"
r"(?:$|[_/\-\d])",
re.IGNORECASE,
)
_STRAP_NET_RE = re.compile(
r"(?:EN|ENABLE|RESET|NRST|BOOT|CHIP_PU|GPIO0)",
re.IGNORECASE,
)
_BULK_F = 1e-6 # >= 1 µF → bulk candidate _BULK_F = 1e-6 # >= 1 µF → bulk candidate
_XTAL_RE = re.compile( _XTAL_RE = re.compile(
r"(?:^|[_/])(X(?:IN|OUT)|XTAL|OSC|HFX(?:IN|OUT)|LFX(?:IN|OUT)|CLK(?:IN|OUT)?)(?:$|[_/\d])", r"(?:^|[_/])(X(?:IN|OUT)|XTAL|OSC|HFX(?:IN|OUT)|LFX(?:IN|OUT)|CLK(?:IN|OUT)?)(?:$|[_/\d])",
@@ -177,16 +201,6 @@ 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,
@@ -210,12 +224,17 @@ def _group_for_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} sat_nets = {n for n in other.pins.values() if n}
# Keep decoupling/bulk/filter/pullup on the IC primary rail only — # Drop power-role parts that sit on a *different* named power rail
# otherwise an LDO on 3V3 also inherits VSYS input caps/inductors. # (LDO must not inherit VSYS input caps). Strap/bias caps with no
if role in _POWER_SAT_ROLES and primary and primary not in sat_nets: # typed POWER net still attach.
if role in _POWER_SAT_ROLES and primary:
sat_power = {n for n in sat_nets if _is_power_net(graph, n)}
if sat_power and primary not in sat_power:
continue continue
if role == "other" and other.component_type in _SKIP_OTHER_TYPES: if role == "other" and other.component_type in _SKIP_OTHER_TYPES:
continue continue
if role not in _ASSEMBLE_ROLES:
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,
@@ -298,9 +317,25 @@ def _role_hint(
if other.component_type == ComponentType.CAPACITOR: if other.component_type == ComponentType.CAPACITOR:
if _looks_xtal_net(via_net) or _ic_pin_is_xtal(cons, via_net, ic): if _looks_xtal_net(via_net) or _ic_pin_is_xtal(cons, via_net, ic):
return "load_cap" return "load_cap"
others = {n for n in other.pins.values() if n != via_net}
if any(_is_ground_net(graph, n) for n in others) and ( pin_nets = {n for n in other.pins.values() if n}
_is_power_net(graph, via_net) or _net_is_ic_supply(graph, ic, cons, via_net) gnd_nets = {n for n in pin_nets if _is_ground_net(graph, n)}
live = [n for n in pin_nets if n not in gnd_nets]
ic_nets = {n for n in ic.pins.values() if n}
# Bootstrap / flying cap between two pins of this IC.
if len(live) == 2 and all(n in ic_nets for n in live):
return "bridge"
# Cap to GND on an IC pin / bias / strap / power net → local bypass.
if gnd_nets and len(live) == 1:
net = live[0]
if (
net in ic_nets
or _is_power_net(graph, net)
or _net_is_ic_supply(graph, ic, cons, net)
or _BIAS_NET_RE.search(net or "")
or _STRAP_NET_RE.search(net or "")
): ):
farads = _cap_farads(other) farads = _cap_farads(other)
return "bulk" if farads is not None and farads >= _BULK_F else "decoupling" return "bulk" if farads is not None and farads >= _BULK_F else "decoupling"
@@ -310,10 +345,12 @@ def _role_hint(
return "filter" return "filter"
if other.component_type == ComponentType.RESISTOR: if other.component_type == ComponentType.RESISTOR:
nets = list(dict.fromkeys(other.pins.values())) nets = list(dict.fromkeys(n for n in other.pins.values() if n))
if len(nets) == 2: if len(nets) == 2:
a, b = nets a, b = nets
if _is_power_net(graph, a) or _is_power_net(graph, b): if _is_power_net(graph, a) or _is_power_net(graph, b) or (
_BIAS_NET_RE.search(a or "") or _BIAS_NET_RE.search(b or "")
):
if _is_ground_net(graph, a) or _is_ground_net(graph, b): if _is_ground_net(graph, a) or _is_ground_net(graph, b):
return "divider" return "divider"
return "pullup" return "pullup"
@@ -321,12 +358,12 @@ def _role_hint(
if a in ic_nets and b in ic_nets: if a in ic_nets and b in ic_nets:
return "bridge" return "bridge"
if a in ic_nets or b in ic_nets: if a in ic_nets or b in ic_nets:
# Set resistor / NTC leg to GND stays series (placement-local).
return "series" return "series"
return "other" return "other"
return "other" return "other"
def _looks_xtal_net(name: str) -> bool: def _looks_xtal_net(name: str) -> bool:
return bool(_XTAL_RE.search(name or "")) return bool(_XTAL_RE.search(name or ""))
+9
View File
@@ -44,6 +44,15 @@ Obiettivo unico: **routing migliore** (loop corti, meno crossing, canali liberi)
| 5 | `assemble_order` dominio → chip → gruppi (contratto packer) | F1 metadato | | 5 | `assemble_order` dominio → chip → gruppi (contratto packer) | F1 metadato |
| 6 | Packing mm / zone PCB / export | **F2 partial**`placement_pack.json` (skip senza PCB + `max_distance_mm`); zone/export dopo | | 6 | Packing mm / zone PCB / export | **F2 partial**`placement_pack.json` (skip senza PCB + `max_distance_mm`); zone/export dopo |
### Roadmap Placement (da qui a coordinate utili)
1. **F1 polish (ora)** — satelliti classificatì; `other` nascosti; Domains/Rails = primary rail.
2. **C4 fill** — riestrazione IC → `layout_rules` con `max_distance_mm` dove il PDF lo dice (skill 1.10.0).
3. **PCB gate** — upload `.kicad_pcb``layout_graph.json` (footprint xy già usati da PS-PLC*).
4. **F2 pack v1** — per ogni `decoupling_proximity` numerica: proporre xy satellite entro `max_distance_mm` dal pad (già skeleton); UI Pack lista proposte.
5. **F2 pack v2** — collisioni courtyard, stesso layer, ordine `assemble_order` per dominio, ancore IC fissi se già piazzati.
6. **F2 export** — scrivere posizioni proposte in file/plugin (pcbnew) senza muovere rame; `placement_check` resta verifica.
Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PS-PLC*) — non confondere con packing. Output F1: `functional_groups.json` scritto in `graph_build`. Pipeline parallela Placement (`POST …/placement/start`) riscrive anche `placement_plan.json` senza toccare lo `status` di analisi, poi tenta F2 pack. Verifica PCB esistente resta `placement_check` (PS-PLC*) — non confondere con packing.
--- ---
+7
View File
@@ -2,6 +2,13 @@
What's new in Pinscope. What's new in Pinscope.
## 2.28.10 — 2026-09-13 — Classify strap/bootstrap satellites; hide other
Caps on EN/BOOT/REGN/PMID and bootstrap caps between IC pins get real roles. Unclassified `other` parts are dropped from satellites and assemble_order.
- [Changed] `_role_hint` classifies GND bypass on bias/strap nets and flying bootstrap caps.
- [Changed] Assemble / IC groups omit `role_hint=other`.
## 2.28.9 — 2026-09-13 — Cleaner Domains / Power rails membership ## 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 ICs primary supply rail (LDO 3V3 no longer inherits VSYS input caps). Connectors/switches drop out of noisy “other” satellites. Power rails follow primary domain only. IC satellites for decoupling/bulk/filter/pullup stay on the ICs primary supply rail (LDO 3V3 no longer inherits VSYS input caps). Connectors/switches drop out of noisy “other” satellites.
+56 -2
View File
@@ -85,7 +85,7 @@ def test_ldo_power_satellites_stay_on_primary_rail():
reference="U1", value="LDO", footprint="", reference="U1", value="LDO", footprint="",
component_type=ComponentType.IC, component_type=ComponentType.IC,
component_subtype="ic.power.ldo", component_subtype="ic.power.ldo",
pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND"}, pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND", "4": "EN"},
), ),
"C_in": Component( "C_in": Component(
reference="C_in", value="10u", footprint="", reference="C_in", value="10u", footprint="",
@@ -105,12 +105,32 @@ def test_ldo_power_satellites_stay_on_primary_rail():
pins={"1": "3V3_DIGITAL", "2": "GND"}, pins={"1": "3V3_DIGITAL", "2": "GND"},
specs=CapacitorSpecs(value_farads=100e-9, value_formatted="100nF"), specs=CapacitorSpecs(value_farads=100e-9, value_formatted="100nF"),
), ),
"C_en": Component(
reference="C_en", value="1u", footprint="",
component_type=ComponentType.CAPACITOR,
pins={"1": "EN", "2": "GND"},
specs=CapacitorSpecs(value_farads=1e-6, value_formatted="1uF"),
),
"C_boot": Component(
reference="C_boot", value="47n", footprint="",
component_type=ComponentType.CAPACITOR,
pins={"1": "BTST", "2": "SW"},
specs=CapacitorSpecs(value_farads=47e-9, value_formatted="47nF"),
),
"J1": Component( "J1": Component(
reference="J1", value="USB", footprint="", reference="J1", value="USB", footprint="",
component_type=ComponentType.CONNECTOR, component_type=ComponentType.CONNECTOR,
pins={"1": "3V3_DIGITAL", "2": "GND"}, pins={"1": "3V3_DIGITAL", "2": "GND"},
), ),
"C_noise": Component(
reference="C_noise", value="100n", footprint="",
component_type=ComponentType.CAPACITOR,
pins={"1": "orphan", "2": "somewhere"},
),
} }
# Extend U1 pins for bootstrap
components["U1"].pins["5"] = "BTST"
components["U1"].pins["6"] = "SW"
nets = { nets = {
"VSYS": Net( "VSYS": Net(
name="VSYS", net_type=NetType.POWER, name="VSYS", net_type=NetType.POWER,
@@ -128,23 +148,57 @@ def test_ldo_power_satellites_stay_on_primary_rail():
PinConnection(component_ref="J1", pin_number="1"), PinConnection(component_ref="J1", pin_number="1"),
], ],
), ),
"EN": Net(
name="EN", net_type=NetType.SIGNAL,
pins=[
PinConnection(component_ref="U1", pin_number="4"),
PinConnection(component_ref="C_en", pin_number="1"),
],
),
"BTST": Net(
name="BTST", net_type=NetType.SIGNAL,
pins=[
PinConnection(component_ref="U1", pin_number="5"),
PinConnection(component_ref="C_boot", pin_number="1"),
],
),
"SW": Net(
name="SW", net_type=NetType.SIGNAL,
pins=[
PinConnection(component_ref="U1", pin_number="6"),
PinConnection(component_ref="C_boot", pin_number="2"),
],
),
"GND": Net( "GND": Net(
name="GND", net_type=NetType.GROUND, name="GND", net_type=NetType.GROUND,
pins=[ pins=[
PinConnection(component_ref="U1", pin_number="3"), PinConnection(component_ref="U1", pin_number="3"),
PinConnection(component_ref="C_in", pin_number="2"), PinConnection(component_ref="C_in", pin_number="2"),
PinConnection(component_ref="C_out", pin_number="2"), PinConnection(component_ref="C_out", pin_number="2"),
PinConnection(component_ref="C_en", pin_number="2"),
PinConnection(component_ref="J1", pin_number="2"), PinConnection(component_ref="J1", pin_number="2"),
], ],
), ),
"orphan": Net(
name="orphan", net_type=NetType.SIGNAL,
pins=[PinConnection(component_ref="C_noise", pin_number="1")],
),
"somewhere": Net(
name="somewhere", net_type=NetType.SIGNAL,
pins=[PinConnection(component_ref="C_noise", pin_number="2")],
),
} }
report = build_functional_groups(DesignGraph(components=components, nets=nets)) report = build_functional_groups(DesignGraph(components=components, nets=nets))
u1 = next(g for g in report.groups if g.ref == "U1") u1 = next(g for g in report.groups if g.ref == "U1")
sat = {s.ref: s.role_hint for s in u1.satellites} sat = {s.ref: s.role_hint for s in u1.satellites}
assert "C_out" in sat assert sat.get("C_out") == "decoupling"
assert sat.get("C_en") == "bulk"
assert sat.get("C_boot") == "bridge"
assert "C_in" not in sat assert "C_in" not in sat
assert "L1" not in sat assert "L1" not in sat
assert "J1" not in sat assert "J1" not in sat
assert "C_noise" not in sat
assert "other" not in sat.values()
def test_multi_rail_board_does_not_collapse_to_one_domain(): def test_multi_rail_board_does_not_collapse_to_one_domain():