Detect matching toward ANT*/ANT_FEED, compute 50 Ω feed width from stackup, and optional λ/4 length from f0 — before auto-draw or F2 packing. Co-authored-by: Cursor <cursoragent@cursor.com>
180 lines
6.0 KiB
Python
180 lines
6.0 KiB
Python
"""Antenna RF verify + design recipe — no invented EM."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from backend.pinscopex.antenna_rf import build_antenna_report, build_design_recipe
|
|
from backend.pinscopex.models import (
|
|
CapacitorSpecs,
|
|
Component,
|
|
ComponentType,
|
|
DesignGraph,
|
|
InductorSpecs,
|
|
LayoutDielectric,
|
|
LayoutFootprint,
|
|
LayoutGraph,
|
|
LayoutPad,
|
|
LayoutStackup,
|
|
LayoutZone,
|
|
Net,
|
|
NetType,
|
|
PinConnection,
|
|
)
|
|
|
|
|
|
def _graph_with_pi_match():
|
|
components = {
|
|
"U1": Component(
|
|
reference="U1", value="RFIC", footprint="",
|
|
component_type=ComponentType.IC,
|
|
component_subtype="ic.rf.wifi_module",
|
|
pins={"1": "RF_ANT", "2": "GND"},
|
|
),
|
|
"L1": Component(
|
|
reference="L1", value="2.2n", footprint="",
|
|
component_type=ComponentType.INDUCTOR,
|
|
pins={"1": "RF_ANT", "2": "ANT_MID"},
|
|
specs=InductorSpecs(value_henries=2.2e-9, value_formatted="2.2nH"),
|
|
),
|
|
"C1": Component(
|
|
reference="C1", value="1p", footprint="",
|
|
component_type=ComponentType.CAPACITOR,
|
|
pins={"1": "RF_ANT", "2": "GND"},
|
|
specs=CapacitorSpecs(value_farads=1e-12, value_formatted="1pF"),
|
|
),
|
|
"C2": Component(
|
|
reference="C2", value="1p", footprint="",
|
|
component_type=ComponentType.CAPACITOR,
|
|
pins={"1": "ANT_MID", "2": "GND"},
|
|
specs=CapacitorSpecs(value_farads=1e-12, value_formatted="1pF"),
|
|
),
|
|
"ANT1": Component(
|
|
reference="ANT1", value="PCB_ANT", footprint="",
|
|
component_type=ComponentType.CONNECTOR,
|
|
pins={"1": "ANT_MID", "2": "GND"},
|
|
),
|
|
}
|
|
nets = {
|
|
"RF_ANT": Net(
|
|
name="RF_ANT", net_type=NetType.SIGNAL,
|
|
pins=[
|
|
PinConnection(component_ref="U1", pin_number="1"),
|
|
PinConnection(component_ref="L1", pin_number="1"),
|
|
PinConnection(component_ref="C1", pin_number="1"),
|
|
],
|
|
),
|
|
"ANT_MID": Net(
|
|
name="ANT_MID", net_type=NetType.SIGNAL,
|
|
pins=[
|
|
PinConnection(component_ref="L1", pin_number="2"),
|
|
PinConnection(component_ref="C2", pin_number="1"),
|
|
PinConnection(component_ref="ANT1", pin_number="1"),
|
|
],
|
|
),
|
|
"GND": Net(
|
|
name="GND", net_type=NetType.GROUND,
|
|
pins=[
|
|
PinConnection(component_ref="U1", pin_number="2"),
|
|
PinConnection(component_ref="C1", pin_number="2"),
|
|
PinConnection(component_ref="C2", pin_number="2"),
|
|
PinConnection(component_ref="ANT1", pin_number="2"),
|
|
],
|
|
),
|
|
}
|
|
return DesignGraph(components=components, nets=nets)
|
|
|
|
|
|
def test_verify_finds_matching_path_to_ant_footprint():
|
|
report = build_antenna_report(_graph_with_pi_match())
|
|
assert report.verify
|
|
row = report.verify[0]
|
|
assert row.ic_ref == "U1"
|
|
assert row.topology in ("pi", "LC", "series_L", "unknown")
|
|
assert "L1" in row.parts
|
|
assert row.status == "ok"
|
|
assert row.marker_ref == "ANT1"
|
|
|
|
|
|
def test_verify_missing_matching_is_warning():
|
|
components = {
|
|
"U1": Component(
|
|
reference="U1", value="RFIC", footprint="",
|
|
component_type=ComponentType.IC,
|
|
component_subtype="ic.rf.transceiver",
|
|
pins={"1": "RF_OUT", "2": "GND"},
|
|
),
|
|
}
|
|
nets = {
|
|
"RF_OUT": Net(
|
|
name="RF_OUT", net_type=NetType.SIGNAL,
|
|
pins=[PinConnection(component_ref="U1", pin_number="1")],
|
|
),
|
|
"GND": Net(
|
|
name="GND", net_type=NetType.GROUND,
|
|
pins=[PinConnection(component_ref="U1", pin_number="2")],
|
|
),
|
|
}
|
|
report = build_antenna_report(DesignGraph(components=components, nets=nets))
|
|
assert report.verify
|
|
assert report.verify[0].topology == "missing"
|
|
assert report.verify[0].status == "warning"
|
|
|
|
|
|
def test_design_recipe_needs_marker_without_ant():
|
|
g = DesignGraph(components={}, nets={})
|
|
layout = LayoutGraph(
|
|
stackup=LayoutStackup(
|
|
copper_layers=["F.Cu", "B.Cu"],
|
|
dielectrics=[LayoutDielectric(name="FR4", er=4.5, height_mm=0.2)],
|
|
copper_thickness_mm=0.035,
|
|
),
|
|
)
|
|
recipe = build_design_recipe(g, layout, f0_mhz=2440.0)
|
|
assert recipe.status == "need_marker"
|
|
|
|
|
|
def test_design_recipe_ready_with_ant_and_stackup():
|
|
g = DesignGraph(
|
|
components={
|
|
"ANT1": Component(
|
|
reference="ANT1", value="feed", footprint="",
|
|
component_type=ComponentType.CONNECTOR,
|
|
pins={"1": "ANT_FEED"},
|
|
),
|
|
},
|
|
nets={
|
|
"ANT_FEED": Net(
|
|
name="ANT_FEED", net_type=NetType.SIGNAL,
|
|
pins=[PinConnection(component_ref="ANT1", pin_number="1")],
|
|
),
|
|
},
|
|
)
|
|
layout = LayoutGraph(
|
|
footprints={
|
|
"ANT1": LayoutFootprint(
|
|
reference="ANT1", x=10.0, y=20.0, layer="F.Cu",
|
|
pads=[LayoutPad(number="1", x=10.0, y=20.0, net="ANT_FEED")],
|
|
),
|
|
},
|
|
nets={"ANT_FEED": 1, "antenna": 2},
|
|
stackup=LayoutStackup(
|
|
copper_layers=["F.Cu", "B.Cu"],
|
|
dielectrics=[LayoutDielectric(name="FR4", er=4.5, height_mm=0.2)],
|
|
copper_thickness_mm=0.035,
|
|
),
|
|
zones=[
|
|
LayoutZone(
|
|
net="antenna",
|
|
layer="F.Cu",
|
|
outlines=[[(0.0, 0.0), (10.0, 0.0), (10.0, 5.0), (0.0, 5.0)]],
|
|
),
|
|
],
|
|
)
|
|
recipe = build_design_recipe(g, layout, f0_mhz=2440.0, target_z_ohm=50.0)
|
|
assert recipe.status == "ready"
|
|
assert recipe.feed_line is not None
|
|
assert recipe.feed_line.w_mm is not None and recipe.feed_line.w_mm > 0
|
|
assert recipe.radiator is not None
|
|
assert recipe.radiator.length_mm_suggest is not None
|
|
assert recipe.zone is not None
|
|
assert recipe.zone.bbox_mm is not None
|