Add RF antenna verify and design recipe to Impedance tab.
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>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user