Make the component library a standalone product door (2.63.0).
Add /api/library datasheet import and component GET/PUT with no exam. Reorganize pytest into datasheet, library, schematic, PCB, and AF+AI. Document Rust criteria (none chosen; no rustup) and coding conformity.
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
"""Layout F1 functional_groups on simple_project — topology only, no mm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from tests.paths import SIMPLE_PROJECT, TAXONOMY
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from backend.periscopex.functional_groups import build_functional_groups
|
||||
from backend.periscopex.models import DesignGraph
|
||||
|
||||
SIMPLE = SIMPLE_PROJECT
|
||||
|
||||
|
||||
def _graph() -> DesignGraph:
|
||||
return DesignGraph.model_validate_json(
|
||||
(SIMPLE / "design_graph.json").read_text(encoding="utf-8"),
|
||||
)
|
||||
|
||||
|
||||
def test_simple_project_has_mcu_ldo_bridge_groups():
|
||||
report = build_functional_groups(_graph())
|
||||
assert report.objective == "routing"
|
||||
refs = {g.ref for g in report.groups}
|
||||
assert {"U1", "U2", "U3"} <= refs
|
||||
# No millimetre fields on the report model dump
|
||||
raw = json.loads(report.model_dump_json())
|
||||
blob = json.dumps(raw)
|
||||
assert "max_distance_mm" not in blob or all(
|
||||
r.get("max_distance_mm") is None
|
||||
for g in raw["groups"]
|
||||
for r in g.get("layout_rules") or []
|
||||
)
|
||||
|
||||
|
||||
def test_u3_owns_crystal_load_caps():
|
||||
report = build_functional_groups(_graph())
|
||||
u3 = next(g for g in report.groups if g.ref == "U3")
|
||||
sat = {s.ref: s.role_hint for s in u3.satellites}
|
||||
assert "X1" in sat
|
||||
assert sat["X1"] == "crystal"
|
||||
assert sat.get("C9") == "load_cap" or sat.get("C10") == "load_cap"
|
||||
assert "C9" in sat and "C10" in sat
|
||||
assert sat["C9"] == "load_cap"
|
||||
assert sat["C10"] == "load_cap"
|
||||
|
||||
|
||||
def test_u1_has_decoupling_or_bulk_on_rails():
|
||||
report = build_functional_groups(_graph())
|
||||
u1 = next(g for g in report.groups if g.ref == "U1")
|
||||
roles = {s.role_hint for s in u1.satellites}
|
||||
assert roles & {"decoupling", "bulk"}
|
||||
|
||||
|
||||
def test_domains_cover_all_ics():
|
||||
report = build_functional_groups(_graph())
|
||||
covered = {r for d in report.domains for r in d.ic_refs}
|
||||
assert covered == {"U1", "U2", "U3"}
|
||||
assert all(d.assemble_order for d in report.domains)
|
||||
|
||||
|
||||
def test_simple_project_splits_5v_and_3v3_domains():
|
||||
"""LDO bridges +5V/+3V3 electrically but domains follow primary rails."""
|
||||
report = build_functional_groups(_graph())
|
||||
by_rail = {d.power_nets[0]: set(d.ic_refs) for d in report.domains if d.power_nets}
|
||||
assert by_rail.get("+5V") == {"U2"}
|
||||
assert by_rail.get("+3V3") == {"U1", "U3"}
|
||||
assert len(report.domains) == 2
|
||||
|
||||
|
||||
def test_ldo_power_satellites_stay_on_primary_rail():
|
||||
"""LDO input-rail caps must not appear as primary-rail satellites."""
|
||||
from backend.periscopex.models import (
|
||||
CapacitorSpecs,
|
||||
Component,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
InductorSpecs,
|
||||
Net,
|
||||
NetType,
|
||||
PinConnection,
|
||||
)
|
||||
|
||||
components = {
|
||||
"U1": Component(
|
||||
reference="U1", value="LDO", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
component_subtype="ic.power.ldo",
|
||||
pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND", "4": "EN"},
|
||||
),
|
||||
"C_in": Component(
|
||||
reference="C_in", value="10u", footprint="",
|
||||
component_type=ComponentType.CAPACITOR,
|
||||
pins={"1": "VSYS", "2": "GND"},
|
||||
specs=CapacitorSpecs(value_farads=10e-6, value_formatted="10uF"),
|
||||
),
|
||||
"L1": Component(
|
||||
reference="L1", value="2.2u", footprint="",
|
||||
component_type=ComponentType.INDUCTOR,
|
||||
pins={"1": "VSYS", "2": "VSYS"},
|
||||
specs=InductorSpecs(value_henries=2.2e-6, value_formatted="2.2uH"),
|
||||
),
|
||||
"C_out": Component(
|
||||
reference="C_out", value="100n", footprint="",
|
||||
component_type=ComponentType.CAPACITOR,
|
||||
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,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="1"),
|
||||
PinConnection(component_ref="C_in", pin_number="1"),
|
||||
PinConnection(component_ref="L1", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"3V3_DIGITAL": Net(
|
||||
name="3V3_DIGITAL", net_type=NetType.POWER,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="2"),
|
||||
PinConnection(component_ref="C_out", 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(
|
||||
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 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():
|
||||
"""Charger→LDO→MCU must not become a single domain via shared POWER nets."""
|
||||
from backend.periscopex.models import (
|
||||
Component,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
PinConnection,
|
||||
)
|
||||
|
||||
components = {
|
||||
"U1": Component(
|
||||
reference="U1", value="BQ25896", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
component_subtype="ic.power.battery_charger",
|
||||
pins={"1": "VBUS", "2": "VSYS", "3": "GND"},
|
||||
),
|
||||
"U2": Component(
|
||||
reference="U2", value="AP2112", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
component_subtype="ic.power.ldo",
|
||||
pins={"1": "VSYS", "2": "3V3_DIGITAL", "3": "GND"},
|
||||
),
|
||||
"U3": Component(
|
||||
reference="U3", value="ESP32", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
component_subtype="ic.rf.wifi_module",
|
||||
pins={"1": "3V3_DIGITAL", "2": "GND"},
|
||||
),
|
||||
"U4": Component(
|
||||
reference="U4", value="SRV05", footprint="",
|
||||
component_type=ComponentType.IC,
|
||||
component_subtype="ic.protection.esd",
|
||||
pins={"1": "VBUS", "2": "GND"},
|
||||
),
|
||||
}
|
||||
nets = {
|
||||
"VBUS": Net(
|
||||
name="VBUS", net_type=NetType.POWER,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="1"),
|
||||
PinConnection(component_ref="U4", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"VSYS": Net(
|
||||
name="VSYS", net_type=NetType.POWER,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="2"),
|
||||
PinConnection(component_ref="U2", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"3V3_DIGITAL": Net(
|
||||
name="3V3_DIGITAL", net_type=NetType.POWER,
|
||||
pins=[
|
||||
PinConnection(component_ref="U2", pin_number="2"),
|
||||
PinConnection(component_ref="U3", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"GND": Net(
|
||||
name="GND", net_type=NetType.GROUND,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="3"),
|
||||
PinConnection(component_ref="U2", pin_number="3"),
|
||||
PinConnection(component_ref="U3", pin_number="2"),
|
||||
PinConnection(component_ref="U4", pin_number="2"),
|
||||
],
|
||||
),
|
||||
}
|
||||
report = build_functional_groups(DesignGraph(components=components, nets=nets))
|
||||
by_rail = {d.power_nets[0]: set(d.ic_refs) for d in report.domains if d.power_nets}
|
||||
assert len(report.domains) >= 3
|
||||
assert by_rail.get("VBUS") == {"U4"}
|
||||
assert by_rail.get("VSYS") == {"U1"}
|
||||
assert by_rail.get("3V3_DIGITAL") == {"U2", "U3"}
|
||||
|
||||
|
||||
def test_build_placement_plan_alias():
|
||||
from backend.periscopex.functional_groups import build_placement_plan
|
||||
report = build_placement_plan(_graph())
|
||||
assert report.objective == "routing"
|
||||
assert {g.ref for g in report.groups} >= {"U1", "U2", "U3"}
|
||||
|
||||
Reference in New Issue
Block a user