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:
@@ -38,6 +38,30 @@ RoleHint = Literal[
|
||||
"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
|
||||
_XTAL_RE = re.compile(
|
||||
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
|
||||
|
||||
|
||||
_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(
|
||||
graph: DesignGraph,
|
||||
ref: str,
|
||||
@@ -210,12 +224,17 @@ def _group_for_ic(
|
||||
continue
|
||||
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
|
||||
# Drop power-role parts that sit on a *different* named power rail
|
||||
# (LDO must not inherit VSYS input caps). Strap/bias caps with no
|
||||
# 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
|
||||
if role == "other" and other.component_type in _SKIP_OTHER_TYPES:
|
||||
continue
|
||||
if role not in _ASSEMBLE_ROLES:
|
||||
continue
|
||||
sat_map[oref] = PlacementSatellite(
|
||||
ref=oref,
|
||||
component_type=other.component_type.value,
|
||||
@@ -298,22 +317,40 @@ def _role_hint(
|
||||
if other.component_type == ComponentType.CAPACITOR:
|
||||
if _looks_xtal_net(via_net) or _ic_pin_is_xtal(cons, via_net, ic):
|
||||
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 (
|
||||
_is_power_net(graph, via_net) or _net_is_ic_supply(graph, ic, cons, via_net)
|
||||
):
|
||||
farads = _cap_farads(other)
|
||||
return "bulk" if farads is not None and farads >= _BULK_F else "decoupling"
|
||||
|
||||
pin_nets = {n for n in other.pins.values() if n}
|
||||
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)
|
||||
return "bulk" if farads is not None and farads >= _BULK_F else "decoupling"
|
||||
return "other"
|
||||
|
||||
if other.component_type == ComponentType.INDUCTOR:
|
||||
return "filter"
|
||||
|
||||
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:
|
||||
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):
|
||||
return "divider"
|
||||
return "pullup"
|
||||
@@ -321,12 +358,12 @@ def _role_hint(
|
||||
if a in ic_nets and b in ic_nets:
|
||||
return "bridge"
|
||||
if a in ic_nets or b in ic_nets:
|
||||
# Set resistor / NTC leg to GND stays series (placement-local).
|
||||
return "series"
|
||||
return "other"
|
||||
|
||||
return "other"
|
||||
|
||||
|
||||
def _looks_xtal_net(name: str) -> bool:
|
||||
return bool(_XTAL_RE.search(name or ""))
|
||||
|
||||
|
||||
@@ -44,6 +44,15 @@ Obiettivo unico: **routing migliore** (loop corti, meno crossing, canali liberi)
|
||||
| 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 |
|
||||
|
||||
### 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.
|
||||
|
||||
---
|
||||
|
||||
@@ -2,6 +2,13 @@
|
||||
|
||||
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
|
||||
|
||||
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.
|
||||
|
||||
@@ -85,7 +85,7 @@ def test_ldo_power_satellites_stay_on_primary_rail():
|
||||
reference="U1", value="LDO", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
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(
|
||||
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"},
|
||||
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(
|
||||
reference="J1", value="USB", footprint="",
|
||||
component_type=ComponentType.CONNECTOR,
|
||||
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 = {
|
||||
"VSYS": Net(
|
||||
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"),
|
||||
],
|
||||
),
|
||||
"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(
|
||||
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="C_en", 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))
|
||||
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 sat.get("C_out") == "decoupling"
|
||||
assert sat.get("C_en") == "bulk"
|
||||
assert sat.get("C_boot") == "bridge"
|
||||
assert "C_in" not in sat
|
||||
assert "L1" 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():
|
||||
|
||||
Reference in New Issue
Block a user