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",
|
"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 ""))
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -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 IC’s 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 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="",
|
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():
|
||||||
|
|||||||
Reference in New Issue
Block a user