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 program
Section titled “The program”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.
What comes out
Section titled “What comes out”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.
Running it
Section titled “Running it”fang verify examples/antenna_match/antenna_match.pyThe reader is fang itself, so this needs no tool on the path.
The whole program
Section titled “The whole program”"""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 Touchstonefile, `chip_antenna.s1p`, which is synthetic and says so -- a series RLCresonator written by hand, standing in for the vendor file a real design wouldcite. Between the connector and the antenna sit two parts: a 1.5 pF shuntcapacitor 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 programdeclares the return loss as a parameter with no value and a question thatevaluates it: `fang verify` reads the file, interpolates it at 2.44 GHz,composes the two parts with the values the graph holds, and returns thenumber through the commit gate. It is answered at the equation level, inclosed form: there is nothing to simulate."""
from fang.interfaces import AnalogIn, AnalogOut, Pin, PinMapfrom fang.lang import GHz, Parameter, Part, System, dB, nH, pF, requirefrom fang.parts import Capacitor, Inductorfrom fang.rationale import Calculates, Requiresfrom fang.traits import Touchstonefrom 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 files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
AE1 ChipAntenna Package:Antenna_Chip_3216C1 1.5 pF Package:C_0402J1 RFConnector Package:UFLL1 1.8 nH Package:L_0402Net-(AE1-PadFEED) AE1.FEED L1.2Net-(AE1-PadGND) AE1.GND C1.2 J1.GNDNet-(C1-Pad1) C1.1 J1.SIG L1.1Every check that ran, and every one left undecided.
UNDECIDED constraint: declared on BLK-42978eb8675a1 checks, 0 failed, 1 undecidedWhat the elaborated graph contains, by entity kind.
1 block 1 calculation 4 component 10 connection 1 constraint 3 interface 8 pin 6 port 1 requirement 1 verification 36 totalsnapshot sha256:0929bae13a7c88649e7d19b4302b8f7ed88ee54540af0e38aede7ab4bfd2f235All of it, including the KiCad netlist, is in
examples/antenna_match/out/. Rebuild it with:
fang build examples/antenna_match/antenna_match.py