Add gated Placement F2 pack skeleton for satellite xy.
Propose positions only from PCB footprints plus numeric decoupling max_distance_mm; otherwise skip with an explicit reason. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
"""Layout F2 placement_pack — gated; no invented millimetres."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.functional_groups import (
|
||||
PlacementIcGroup,
|
||||
PlacementSatellite,
|
||||
FunctionalGroupsReport,
|
||||
build_functional_groups,
|
||||
)
|
||||
from backend.pinscopex.models import (
|
||||
DesignGraph,
|
||||
LayoutFootprint,
|
||||
LayoutGraph,
|
||||
LayoutPad,
|
||||
)
|
||||
from backend.pinscopex.placement_pack import build_placement_pack
|
||||
|
||||
SIMPLE = Path(__file__).resolve().parents[1] / "simple_project"
|
||||
|
||||
|
||||
def _graph() -> DesignGraph:
|
||||
return DesignGraph.model_validate_json(
|
||||
(SIMPLE / "design_graph.json").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def test_simple_project_without_pcb_skips_pack():
|
||||
plan = build_functional_groups(_graph())
|
||||
pack = build_placement_pack(plan, None)
|
||||
assert pack.status == "skipped"
|
||||
assert pack.skip_reason == "no_pcb_footprints"
|
||||
assert pack.placements == []
|
||||
|
||||
|
||||
def test_layout_without_numeric_rules_skips():
|
||||
plan = FunctionalGroupsReport(
|
||||
groups=[
|
||||
PlacementIcGroup(
|
||||
ref="U1",
|
||||
layout_rules=[{"kind": "decoupling_proximity", "pin": "5"}],
|
||||
satellites=[
|
||||
PlacementSatellite(
|
||||
ref="C4",
|
||||
component_type="capacitor",
|
||||
role_hint="decoupling",
|
||||
nets=["+3V3"],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": LayoutFootprint(
|
||||
reference="U1", x=0, y=0, layer="F.Cu",
|
||||
pads=[LayoutPad(number="5", x=0.0, y=0.0, net="+3V3")],
|
||||
),
|
||||
},
|
||||
)
|
||||
pack = build_placement_pack(plan, layout)
|
||||
assert pack.status == "skipped"
|
||||
assert pack.skip_reason == "no_numeric_layout_rules"
|
||||
|
||||
|
||||
def test_numeric_rule_proposes_satellite_within_limit():
|
||||
limit = 2.0
|
||||
plan = FunctionalGroupsReport(
|
||||
groups=[
|
||||
PlacementIcGroup(
|
||||
ref="U1",
|
||||
layout_rules=[{
|
||||
"kind": "decoupling_proximity",
|
||||
"pin": "5",
|
||||
"max_distance_mm": limit,
|
||||
}],
|
||||
satellites=[
|
||||
PlacementSatellite(
|
||||
ref="C4",
|
||||
component_type="capacitor",
|
||||
role_hint="decoupling",
|
||||
nets=["+3V3"],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
layout = LayoutGraph(
|
||||
footprints={
|
||||
"U1": LayoutFootprint(
|
||||
reference="U1", x=0, y=0, layer="F.Cu",
|
||||
pads=[LayoutPad(number="5", x=10.0, y=20.0, net="+3V3")],
|
||||
),
|
||||
},
|
||||
)
|
||||
pack = build_placement_pack(plan, layout)
|
||||
assert pack.status == "packed"
|
||||
assert pack.skip_reason is None
|
||||
assert len(pack.placements) == 1
|
||||
p = pack.placements[0]
|
||||
assert p.ref == "C4"
|
||||
assert p.anchor_ref == "U1"
|
||||
assert p.max_distance_mm == limit
|
||||
assert p.layer == "F.Cu"
|
||||
dist = ((p.proposed_x - 10.0) ** 2 + (p.proposed_y - 20.0) ** 2) ** 0.5
|
||||
assert dist <= limit + 1e-6
|
||||
assert dist > 0
|
||||
Reference in New Issue
Block a user