Add Layout F1 functional groups and crystal/NC checks.
Write routing-first domain/satellite topology after graph_build, and flag crystal CL mismatches and NC pins on active nets without inventing millimetres. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
"""Crystal CL check — only fires when CL and cap values are known."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.crystal_cl_check import check_crystal_cl
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
PinConnection,
|
||||
SimpleComponentSpecs,
|
||||
)
|
||||
|
||||
|
||||
def _xtal_graph(*, cl_f: float | None, c1: str, c2: str, stray: float | None = None) -> DesignGraph:
|
||||
values: dict = {}
|
||||
if cl_f is not None:
|
||||
values["load_capacitance_f"] = cl_f
|
||||
if stray is not None:
|
||||
values["stray_capacitance_f"] = stray
|
||||
return DesignGraph(
|
||||
components={
|
||||
"X1": Component(
|
||||
reference="X1",
|
||||
value="8MHz",
|
||||
footprint="",
|
||||
component_type=ComponentType.CRYSTAL,
|
||||
mpn="XTAL",
|
||||
pins={"1": "XIN", "2": "XOUT", "3": "GND"},
|
||||
specs=SimpleComponentSpecs(specs_type="crystal", values=values) if values else None,
|
||||
),
|
||||
"C1": Component(
|
||||
reference="C1", value=c1, footprint="",
|
||||
component_type=ComponentType.CAPACITOR,
|
||||
pins={"1": "XIN", "2": "GND"},
|
||||
),
|
||||
"C2": Component(
|
||||
reference="C2", value=c2, footprint="",
|
||||
component_type=ComponentType.CAPACITOR,
|
||||
pins={"1": "XOUT", "2": "GND"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"XIN": Net(
|
||||
name="XIN", net_type=NetType.SIGNAL,
|
||||
pins=[
|
||||
PinConnection(component_ref="X1", pin_number="1"),
|
||||
PinConnection(component_ref="C1", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"XOUT": Net(
|
||||
name="XOUT", net_type=NetType.SIGNAL,
|
||||
pins=[
|
||||
PinConnection(component_ref="X1", pin_number="2"),
|
||||
PinConnection(component_ref="C2", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"GND": Net(
|
||||
name="GND", net_type=NetType.GROUND,
|
||||
pins=[
|
||||
PinConnection(component_ref="X1", pin_number="3"),
|
||||
PinConnection(component_ref="C1", pin_number="2"),
|
||||
PinConnection(component_ref="C2", pin_number="2"),
|
||||
],
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_no_cl_in_specs_is_silent():
|
||||
g = _xtal_graph(cl_f=None, c1="18p", c2="18p")
|
||||
assert check_crystal_cl(g) == []
|
||||
|
||||
|
||||
def test_series_above_cl_without_stray_warns():
|
||||
# series of 22p || 22p = 11p — wait we need series > CL.
|
||||
# 100p || 100p = 50p > CL 18p * 1.25
|
||||
g = _xtal_graph(cl_f=18e-12, c1="100p", c2="100p")
|
||||
findings = check_crystal_cl(g)
|
||||
assert any(f.rule_id == "PS-XTAL-002" for f in findings)
|
||||
|
||||
|
||||
def test_with_stray_mismatch_warns():
|
||||
# 18p||18p = 9p + 2p stray = 11p vs CL 18p → below 0.75*18
|
||||
g = _xtal_graph(cl_f=18e-12, c1="18p", c2="18p", stray=2e-12)
|
||||
findings = check_crystal_cl(g)
|
||||
assert any(f.rule_id == "PS-XTAL-002" for f in findings)
|
||||
|
||||
|
||||
def test_simple_project_without_cl_silent():
|
||||
from pathlib import Path
|
||||
from backend.pinscopex.models import DesignGraph
|
||||
path = Path(__file__).resolve().parents[1] / "simple_project" / "design_graph.json"
|
||||
g = DesignGraph.model_validate_json(path.read_text())
|
||||
assert check_crystal_cl(g) == []
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Layout F1 functional_groups on simple_project — topology only, no mm."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from backend.pinscopex.functional_groups import build_functional_groups
|
||||
from backend.pinscopex.models import DesignGraph
|
||||
|
||||
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_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)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""NC pin connectivity check."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from backend.pinscopex.models import (
|
||||
Component,
|
||||
ComponentConstraints,
|
||||
ComponentType,
|
||||
DesignGraph,
|
||||
Net,
|
||||
NetType,
|
||||
Pin,
|
||||
PinConnection,
|
||||
)
|
||||
from backend.pinscopex.nc_pin_check import check_nc_pins
|
||||
|
||||
|
||||
def test_nc_pin_on_active_net_warns():
|
||||
g = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="IC", footprint="",
|
||||
component_type=ComponentType.IC, mpn="PART",
|
||||
pins={"1": "SIG", "2": "GND"},
|
||||
),
|
||||
"R1": Component(
|
||||
reference="R1", value="10k", footprint="",
|
||||
component_type=ComponentType.RESISTOR,
|
||||
pins={"1": "SIG", "2": "GND"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"SIG": Net(
|
||||
name="SIG", net_type=NetType.SIGNAL,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="1"),
|
||||
PinConnection(component_ref="R1", pin_number="1"),
|
||||
],
|
||||
),
|
||||
"GND": Net(
|
||||
name="GND", net_type=NetType.GROUND,
|
||||
pins=[
|
||||
PinConnection(component_ref="U1", pin_number="2"),
|
||||
PinConnection(component_ref="R1", pin_number="2"),
|
||||
],
|
||||
),
|
||||
},
|
||||
)
|
||||
cmap = {
|
||||
"PART": ComponentConstraints(
|
||||
mpn="PART",
|
||||
pintable=[Pin(number="1", name="NC"), Pin(number="2", name="GND")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
),
|
||||
}
|
||||
findings = check_nc_pins(g, cmap)
|
||||
assert len(findings) == 1
|
||||
assert findings[0].rule_id == "PS-NC-001"
|
||||
assert findings[0].net == "SIG"
|
||||
|
||||
|
||||
def test_nc_pin_on_nc_net_silent():
|
||||
g = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="IC", footprint="",
|
||||
component_type=ComponentType.IC, mpn="PART",
|
||||
pins={"1": "NC"},
|
||||
),
|
||||
},
|
||||
nets={
|
||||
"NC": Net(
|
||||
name="NC", net_type=NetType.UNKNOWN,
|
||||
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
||||
),
|
||||
},
|
||||
)
|
||||
cmap = {
|
||||
"PART": ComponentConstraints(
|
||||
mpn="PART",
|
||||
pintable=[Pin(number="1", name="NC")],
|
||||
absolute_maximum_ratings=[],
|
||||
rules=[],
|
||||
),
|
||||
}
|
||||
assert check_nc_pins(g, cmap) == []
|
||||
|
||||
|
||||
def test_no_pintable_silent():
|
||||
g = DesignGraph(
|
||||
components={
|
||||
"U1": Component(
|
||||
reference="U1", value="IC", footprint="",
|
||||
component_type=ComponentType.IC, mpn="PART",
|
||||
pins={"1": "SIG"},
|
||||
),
|
||||
},
|
||||
nets={},
|
||||
)
|
||||
assert check_nc_pins(g, {}) == []
|
||||
Reference in New Issue
Block a user