Skip to content
copperhead.sh
Get started

Examples

Antenna match

A 2.4 GHz chip antenna behind an L match, and the question of whether it is matched. It is the example of a part carrying its behaviour as data: a Touchstone file, read in-tree and composed in closed form. There is nothing to simulate, so the question is answered at the equation level.

the interconnect view
The interconnect view

antenna_match.py puts a 1.5 pF shunt capacitor at the connector and a 1.8 nH series inductor toward the antenna, and requires at least 10 dB of return loss at 2.44 GHz. The values come from the L-match formulas over the antenna’s impedance there, recorded as a calculation.

The antenna carries its model as a trait, with a provenance that says what the numbers are:

self.antenna.add_trait(
Touchstone(
source="chip_antenna.s1p",
ports=("FEED",),
provenance=assumed_provenance("a synthetic series RLC resonator, ..."),
)
)

chip_antenna.s1p is synthetic, and says so in its header: a series RLC resonator (22 Ohm, 4 nH, about 1.01 pF) written by hand to stand in for the file an antenna vendor publishes. A real design would cite the vendor’s file, or a network analyser’s measurement of the board, and its provenance would say which.

The question names the measure, the frequency, and the matching parts in order from the connector toward the antenna:

matched = Evaluates(
"match_spec",
measures={"return_loss": ReturnLoss("antenna.rf", at=2.44 * GHz,
through=("shunt_c", "series_l"))},
)

Which of the two parts is a shunt and which is in series is read from the graph (the capacitor has a terminal on ground) and each value is the one the graph holds.

4 parts, 3 nets, 36 entities, 1 check, undecided until the question is answered.

out/verification.txt is the file to read: the touchstone tool reads the model, interpolates it at 2.44 GHz, composes the two parts, and finds 38.5 dB, so the verification passes at confidence 0.5: half, because the model it rests on is an assumption. The number enters the graph through the commit gate as an inferred value whose source is the run’s evidence, and the evidence records the model file’s digest, because the snapshot does not hold the file.

A frequency outside the file’s 2.30 to 2.60 GHz is refused, naming the range; nothing is extrapolated. A matching part with no value leaves the question unanswered, naming the part.

Terminal window
fang verify examples/antenna_match/antenna_match.py

The reader is fang itself, so this needs no tool on the path.

examples/antenna_match/antenna_match.py
"""A 2.4 GHz chip antenna behind an L match, and whether it is matched.
Show 15 more lines
The antenna carries its network parameters as data: a one-port Touchstone
file, `chip_antenna.s1p`, which is synthetic and says so -- a series RLC
resonator written by hand, standing in for the vendor file a real design would
cite. Between the connector and the antenna sit two parts: a 1.5 pF shunt
capacitor at the connector and a 1.8 nH series inductor toward the antenna,
worked out by hand from the antenna's impedance at 2.44 GHz.
The requirement asks for 10 dB of return loss at 2.44 GHz. The program
declares the return loss as a parameter with no value and a question that
evaluates it: `fang verify` reads the file, interpolates it at 2.44 GHz,
composes the two parts with the values the graph holds, and returns the
number through the commit gate. It is answered at the equation level, in
closed form: there is nothing to simulate.
"""
from fang.interfaces import AnalogIn, AnalogOut, Pin, PinMap
from fang.lang import GHz, Parameter, Part, System, dB, nH, pF, require
from fang.parts import Capacitor, Inductor
from fang.rationale import Calculates, Requires
from fang.traits import Touchstone
from fang.verification import Evaluates, ReturnLoss, assumed_provenance
class ChipAntenna(Part):
"""A chip antenna: a feed and a ground, and a model of what it reflects."""
designator_prefix = "AE"
rf = AnalogIn()
FEED = Pin("FEED", role="analog", number="1")
GND = Pin("GND", role="ground", number="2")
pinmap = PinMap({"rf.signal": "FEED", "rf.ref": "GND"})
class RFConnector(Part):
"""Where the radio's 50 Ohm line arrives."""
designator_prefix = "J"
rf = AnalogOut()
SIG = Pin("SIG", role="analog", number="1")
GND = Pin("GND", role="ground", number="2")
pinmap = PinMap({"rf.signal": "SIG", "rf.ref": "GND"})
class AntennaMatch(System):
"""The antenna, its match, and the question that checks the match."""
match_spec = Requires(
"The antenna presents at least 10 dB of return loss at 2.44 GHz, the "
"middle of the 2.4 GHz band",
validation="analysis",
)
l_match = Calculates(
"Q = sqrt(R0 / RL - 1); X_series = Q RL - X_antenna; B_shunt = Q / R0",
inputs=("shunt_c", "series_l", "antenna"),
result="1.82 nH series and 1.47 pF shunt for 22 - j3.1 Ohm at 2.44 GHz; "
"fitted with 1.8 nH and 1.5 pF",
requirements=("match_spec",),
)
# No value: the program has not measured it, and a number written here
# would be a claim rather than a measurement.
return_loss = Parameter("dB", description="return loss at the connector, at 2.44 GHz")
feed = RFConnector(package="UFL")
shunt_c = Capacitor(capacitance=1.5 * pF, package="C_0402")
series_l = Inductor(inductance=1.8 * nH, package="L_0402")
antenna = ChipAntenna(package="Antenna_Chip_3216")
# The return loss looking in from the connector: through the shunt
# capacitor, then the series inductor, into the antenna's model.
matched = Evaluates(
"match_spec",
measures={
"return_loss": ReturnLoss(
"antenna.rf", at=2.44 * GHz, through=("shunt_c", "series_l")
),
},
)
def __init__(self, **overrides):
super().__init__(**overrides)
self.antenna.add_trait(
Touchstone(
source="chip_antenna.s1p",
ports=("FEED",),
provenance=assumed_provenance(
"a synthetic series RLC resonator, standing in for a vendor file"
),
)
)
def architecture(self):
self.feed.rf.signal >> self.shunt_c.p1
self.feed.rf.signal >> self.series_l.p1
self.shunt_c.p2 >> self.feed.rf.ref
self.series_l.p2 >> self.antenna.rf.signal
self.antenna.rf.ref >> self.feed.rf.ref
def constraints(self):
require(self.return_loss >= 10 * dB)

The parts, then the nets and the pads on them.

out/netlist.txt
AE1 ChipAntenna Package:Antenna_Chip_3216
C1 1.5 pF Package:C_0402
J1 RFConnector Package:UFL
L1 1.8 nH Package:L_0402
Net-(AE1-PadFEED) AE1.FEED L1.2
Net-(AE1-PadGND) AE1.GND C1.2 J1.GND
Net-(C1-Pad1) C1.1 J1.SIG L1.1

Every check that ran, and every one left undecided.

out/checks.txt
UNDECIDED constraint: declared on BLK-42978eb8675a
1 checks, 0 failed, 1 undecided

What the elaborated graph contains, by entity kind.

out/graph.txt
1 block
1 calculation
4 component
10 connection
1 constraint
3 interface
8 pin
6 port
1 requirement
1 verification
36 total
snapshot sha256:0929bae13a7c88649e7d19b4302b8f7ed88ee54540af0e38aede7ab4bfd2f235

All of it, including the KiCad netlist, is in examples/antenna_match/out/. Rebuild it with:

Terminal window
fang build examples/antenna_match/antenna_match.py