Keep validate.py and the finding engine as-is. AGPL LICENSE stays at the repo root. Docker overlays dependency then src. Do not delete the inherited tree.
112 lines
3.2 KiB
Python
112 lines
3.2 KiB
Python
"""Layout F2 placement_pack — gated; no invented millimetres."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from tests.paths import SIMPLE_PROJECT, TAXONOMY
|
|
|
|
from pathlib import Path
|
|
|
|
from backend.periscopex.functional_groups import (
|
|
PlacementIcGroup,
|
|
PlacementSatellite,
|
|
FunctionalGroupsReport,
|
|
build_functional_groups,
|
|
)
|
|
from backend.periscopex.models import (
|
|
DesignGraph,
|
|
LayoutFootprint,
|
|
LayoutGraph,
|
|
LayoutPad,
|
|
)
|
|
from backend.periscopex.placement_pack import build_placement_pack
|
|
|
|
SIMPLE = 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
|