Examples
Equations
A divider written as the ratio it has to satisfy, rather than as two numbers someone computed offstage. It is then reused, by inheritance, at a different ratio.
The program
Section titled “The program”equations.py puts Ohm’s law in the graph:
division = self.bottom.resistance / (self.top.resistance + self.bottom.resistance)require(self.ratio_max >= division)require(self.ratio_min <= division)require(self.v_in <= self.i_bleed * (self.top.resistance + self.bottom.resistance))Arithmetic checks dimensions when the expression is constructed, so a
dimensionally invalid equation cannot be stored, let alone evaluated. What
survives to check time is arithmetic that already type-checks.
SenseDivider(FeedbackDivider) is the reuse. It keeps every equation and
replaces the two resistors, so 24 V onto a 3.3 V ADC is the same design at an
eighth of the ratio. The board then instantiates both with different bounds:
feedback = FeedbackDivider(v_in=12 * V, i_bleed=250 * uA, ratio_min=0.19 * ratio, ...)sense = SenseDivider(v_in=24 * V, i_bleed=200 * uA, ratio_min=0.09 * ratio, ...)ratio = UnitLiteral("1") is how a project adds a unit, dimensionless in this
case, without editing the language.
What comes out
Section titled “What comes out”7 parts, 6 nets, 72 entities, 12 checks, none failed and none undecided. Twelve decided checks over two dividers is the whole point. Every bound in both blocks has the numbers it needs to be answered.
Running it
Section titled “Running it”fang check examples/equations/equations.pyfang graph examples/equations/equations.pyThe whole program
Section titled “The whole program”"""Values chosen by equation: a divider stated as the ratio it must satisfy.Show 10 more lines
The resistors carry real values, but the values are not the design; theconstraints are. Ohm's law is written down as a constraint rather than left in acomment, so substituting a part re-checks the arithmetic instead of trusting it.
Inheritance is how a second divider reuses the first: `SenseDivider` keeps theequations and replaces the two values. A block owns its interior (its parts, itsparameters and the constraints over them), and the board connects to the pads atits edge."""
from fang.interfaces import AnalogIn, Pin, PinMap, PowerIn, PowerOutfrom fang.lang import ( MOhm, Module, Parameter, Part, System, UnitLiteral, V, kOhm, mA, mW, require, tolerance, uA,)from fang.parts import Resistor
#: A dimensionless literal, so a ratio is a quantity like every other number.#: A project adds its own units this way rather than editing the language.ratio = UnitLiteral("1")
class FeedbackDivider(Module): """Two resistors and the four facts that make them the right two."""
v_in = Parameter("V", description="the rail being divided") i_bleed = Parameter("A", description="the most the leg may draw from it") ratio_min = Parameter("", description="lower bound on tap / rail") ratio_max = Parameter("", description="upper bound on tap / rail")
top = Resistor( resistance=tolerance(82 * kOhm, "1%"), power_rating=100 * mW, package="R_0402" ) bottom = Resistor( resistance=tolerance(22 * kOhm, "1%"), power_rating=100 * mW, package="R_0402" )
def architecture(self): # The interior: the tap is the node between the two legs. self.top.p2 >> self.bottom.p1
def constraints(self): division = self.bottom.resistance / ( self.top.resistance + self.bottom.resistance ) # The reference is on the left of every comparison because a parameter # is what the expression is being judged against. require(self.ratio_max >= division) require(self.ratio_min <= division)
# Ohm's law, as a constraint: v_in <= i_bleed * R_total is the same # statement as "the leg draws no more than i_bleed", and it survives a # change to either resistor. require( self.v_in <= self.i_bleed * (self.top.resistance + self.bottom.resistance) )
# A leg stiff enough to ignore the tap's input current, and not so stiff # that board leakage competes with it. require(self.top.resistance <= 1 * MOhm) require(self.top.resistance >= 10 * kOhm)
class SenseDivider(FeedbackDivider): """The same equations, an eighth of the ratio: 24 V onto a 3.3 V ADC."""
top = Resistor( resistance=tolerance(180 * kOhm, "1%"), power_rating=100 * mW, package="R_0402" ) bottom = Resistor( resistance=tolerance(20 * kOhm, "1%"), power_rating=100 * mW, package="R_0402" )
class ADC(Part): """A two-channel converter. Its inputs are analog, and typed as analog."""
designator_prefix = "U"
power = PowerIn(voltage=3.3 * V, current_demand=2 * mA) channel_a = AnalogIn(voltage=3.3 * V, impedance=1 * MOhm) channel_b = AnalogIn(voltage=3.3 * V, impedance=1 * MOhm)
VDD = Pin("VDD", role="power", number="1") GND = Pin("GND", role="ground", number="2") AIN0 = Pin("AIN0", role="analog", number="3") AIN1 = Pin("AIN1", role="analog", number="4")
pinmap = PinMap( { "power.vcc": "VDD", "power.gnd": "GND", "channel_a.signal": "AIN0", "channel_b.signal": "AIN1", } )
class TerminalBlock(Part): """The board's edge: two measured rails, the logic supply, one return."""
designator_prefix = "J"
rail_a = PowerOut(voltage=12 * V, current_capability=2000 * mA) rail_b = PowerOut(voltage=24 * V, current_capability=2000 * mA) logic = PowerOut(voltage=3.3 * V, current_capability=100 * mA)
V12 = Pin("12V", role="power", number="1") V24 = Pin("24V", role="power", number="2") V3V3 = Pin("3V3", role="power", number="3") GND = Pin("GND", role="ground", number="4")
pinmap = PinMap( { "rail_a.vcc": "12V", "rail_a.gnd": "GND", "rail_b.vcc": "24V", "rail_b.gnd": "GND", "logic.vcc": "3V3", "logic.gnd": "GND", } )
class MeasuredRail(System): """A 12 V rail and a 24 V rail, both measured by the same converter."""
terminals = TerminalBlock(package="TerminalBlock_1x04_P5.08mm")
feedback = FeedbackDivider( v_in=12 * V, i_bleed=250 * uA, ratio_min=0.19 * ratio, ratio_max=0.23 * ratio, ) sense = SenseDivider( v_in=24 * V, i_bleed=200 * uA, ratio_min=0.09 * ratio, ratio_max=0.11 * ratio, ) adc = ADC(package="MSOP-10")
def architecture(self): self.terminals.logic >> self.adc.power
self.terminals.rail_a.vcc >> self.feedback.top.p1 self.terminals.rail_a.gnd >> self.feedback.bottom.p2 self.terminals.rail_b.vcc >> self.sense.top.p1 self.terminals.rail_b.gnd >> self.sense.bottom.p2
self.feedback.top.p2 >> self.adc.channel_a.signal self.sense.top.p2 >> self.adc.channel_b.signalThe files it writes
Section titled “The files it writes”The parts, then the nets and the pads on them.
J1 TerminalBlock Package:TerminalBlock_1x04_P5.08mmR1 22 +/- 0.01 kOhm Package:R_0402R2 82 +/- 0.01 kOhm Package:R_0402R3 20 +/- 0.01 kOhm Package:R_0402R4 180 +/- 0.01 kOhm Package:R_0402U1 ADC Package:MSOP-10Net-(J1-Pad12V) J1.12V R2.1Net-(J1-Pad24V) J1.24V R4.1Net-(J1-Pad3V3) J1.3V3 U1.VDDNet-(J1-PadGND) J1.GND R1.2 R3.2 U1.GNDNet-(R1-Pad1) R1.1 R2.2 U1.AIN0Net-(R3-Pad1) R3.1 R4.2 U1.AIN1Every check that ran, and every one left undecided.
12 checks, 0 failed, 0 undecidedWhat the elaborated graph contains, by entity kind.
3 block 6 component 19 connection 10 constraint 4 interface 16 pin 14 port 72 totalsnapshot sha256:2c5c70a2cb2fc5cbddd9d7c98ff1050f605243738b1f7370a03a829a91eb4161All of it, including the KiCad netlist, is in
examples/equations/out/. Rebuild it with:
fang build examples/equations/equations.py