Skip to content

Commit 810c86d

Browse files
authored
Add shrink-multiply op to backend (#402)
Add the tolerance-shrinking multiply operation to the backend, so it can be done without generators which were constraining in terms of parameter dataflow and syntactic boilerplate. Renames the previous cancel_multiply (which was a bit too implementation-focused) to shrink_multiply (which is more use-case focused). Also better documentation in terms of target tolerance and contributing tolerance. Netlists should be the same, but there are ordering changes from changed order of parts. Resolves #393
1 parent d9e0036 commit 810c86d

30 files changed

+333
-339
lines changed

compiler/src/main/scala/edg/compiler/ExprEvaluate.scala

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,21 @@ object ExprEvaluate {
5656
case _ =>
5757
throw new ExprEvaluateException(s"Unknown binary operand types in $lhs ${binary.op} $rhs from $binary")
5858
}
59+
case Op.SHRINK_MULT => (lhs, rhs) match {
60+
case (RangeValue(targetMin, targetMax), RangeValue(contribMin, contribMax)) =>
61+
val lower = contribMax * targetMin
62+
val upper = contribMin * targetMax
63+
if (lower > upper) { // TODO this should store a nonfatal error result instead of crashing on-the-spot
64+
throw new ExprEvaluateException(s"Empty range result in $lhs ${binary.op} $rhs from $binary")
65+
} else {
66+
RangeValue(lower, upper)
67+
}
68+
case (RangeEmpty, RangeEmpty) => RangeEmpty
69+
case (lhs: RangeValue, RangeEmpty) => RangeEmpty
70+
case (RangeEmpty, rhs: RangeValue) => RangeEmpty
71+
case _ =>
72+
throw new ExprEvaluateException(s"Unknown binary operand types in $lhs ${binary.op} $rhs from $binary")
73+
}
5974

6075
case Op.AND => (lhs, rhs) match {
6176
case (BooleanValue(lhs), BooleanValue(rhs)) => BooleanValue(lhs && rhs)

compiler/src/main/scala/edg/compiler/ExprToString.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ class ExprToString() extends ValueExprMap[String] {
3939
def unapply(op: Op): Option[String] = op match {
4040
case Op.ADD => Some("+")
4141
case Op.MULT => Some("×")
42+
case Op.SHRINK_MULT => Some("↓×")
4243
case Op.AND => Some("&&")
4344
case Op.OR => Some("||")
4445
case Op.XOR => Some("^")
@@ -59,7 +60,7 @@ class ExprToString() extends ValueExprMap[String] {
5960
}
6061
object PrefixOp {
6162
def unapply(op: Op): Option[String] = op match {
62-
case Op.ADD | Op.MULT => None
63+
case Op.ADD | Op.MULT | Op.SHRINK_MULT => None
6364
case Op.AND | Op.OR | Op.XOR | Op.IMPLIES | Op.EQ | Op.NEQ => None
6465
case Op.GT | Op.GTE | Op.LT | Op.LTE => None
6566
case Op.MAX => Some("max")

compiler/src/test/scala/edg/compiler/ExprEvaluateTest.scala

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package edg.compiler
22

3+
import edg.ExprBuilder._
4+
import edg.wir.DesignPath
35
import org.scalatest._
46
import org.scalatest.flatspec.AnyFlatSpec
5-
import matchers.should.Matchers._
6-
import edg.wir.DesignPath
7-
import edg.ExprBuilder._
7+
import org.scalatest.matchers.should.Matchers._
88

99
class ExprEvaluateTest extends AnyFlatSpec {
1010
behavior.of("ExprEvaluate")
@@ -54,6 +54,30 @@ class ExprEvaluateTest extends AnyFlatSpec {
5454
) should equal(FloatValue(2.0))
5555
}
5656

57+
it should "handle shrink multiply" in {
58+
import edgir.expr.expr.BinaryExpr.Op
59+
evalTest.map( // test x * 1/x = 1 property
60+
ValueExpr.BinOp(
61+
Op.SHRINK_MULT,
62+
ValueExpr.Literal(10.0, 20.0),
63+
ValueExpr.Literal(1.0 / 20, 1.0 / 10)
64+
)) should equal(RangeValue(1.0, 1.0))
65+
evalTest.map( // ... but with negative numbers
66+
ValueExpr.BinOp(
67+
Op.SHRINK_MULT,
68+
ValueExpr.Literal(-20.0, -10.0),
69+
ValueExpr.Literal(-1.0 / 10, -1.0 / 20)
70+
)) should equal(RangeValue(1.0, 1.0))
71+
val rcResult = evalTest.map( // test RC low pass filter, need to account for floating tolerance
72+
ValueExpr.BinOp(
73+
Op.SHRINK_MULT, // note, 2*pi and inverts flattened out, just becomes R*C
74+
ValueExpr.Literal(90 * 1.0e-6 * 0.95, 110 * 1.0e-6 * 1.05),
75+
ValueExpr.Literal(1.0 / 110, 1.0 / 90)
76+
))
77+
rcResult.asInstanceOf[RangeValue].lower shouldBe ((1.0e-6f * 0.95f) +- 1.0e-12f)
78+
rcResult.asInstanceOf[RangeValue].upper shouldBe ((1.0e-6f * 1.05f) +- 1.0e-12f)
79+
}
80+
5781
it should "handle unary arithmetic ops" in {
5882
import edgir.expr.expr.UnaryExpr.Op
5983
evalTest.map(
@@ -248,8 +272,8 @@ class ExprEvaluateTest extends AnyFlatSpec {
248272
}
249273

250274
it should "handle array unary set ops" in {
251-
import edgir.expr.expr.UnarySetExpr.Op
252275
import edg.ExprBuilder.Literal
276+
import edgir.expr.expr.UnarySetExpr.Op
253277
evalTest.map(
254278
ValueExpr.UnarySetOp(
255279
Op.FLATTEN,
@@ -275,8 +299,8 @@ class ExprEvaluateTest extends AnyFlatSpec {
275299
}
276300

277301
it should "handle array-value (broadcast) ops" in {
278-
import edgir.expr.expr.BinarySetExpr.Op
279302
import edg.ExprBuilder.Literal
303+
import edgir.expr.expr.BinarySetExpr.Op
280304
evalTest.map(
281305
ValueExpr.BinSetOp(
282306
Op.ADD,

edg/abstract_parts/OpampCircuits.py

Lines changed: 2 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -313,42 +313,7 @@ def generate(self) -> None:
313313
self.assign(self.actual_ratio, self.rf.actual_resistance / self.r1.actual_resistance)
314314

315315

316-
class IntegratorValues(ESeriesRatioValue):
317-
def __init__(self, factor: Range, capacitance: Range):
318-
self.factor = factor # output scale factor, 1/RC in units of 1/s
319-
self.capacitance = capacitance # value of the capacitor
320-
321-
@staticmethod
322-
def from_resistors(r1_range: Range, r2_range: Range) -> 'IntegratorValues':
323-
"""r1 is the input resistor and r2 is the capacitor."""
324-
return IntegratorValues(
325-
1 / (r1_range * r2_range),
326-
r2_range
327-
)
328-
329-
def initial_test_decades(self) -> Tuple[int, int]:
330-
"""C is given per the spec, so we need factor = 1 / (R * C) => R = 1 / (factor * C)"""
331-
capacitance_decade = ceil(log10(self.capacitance.center()))
332-
allowed_resistances = Range.cancel_multiply(1 / self.capacitance, 1 / self.factor)
333-
resistance_decade = ceil(log10(allowed_resistances.center()))
334-
335-
return resistance_decade, capacitance_decade
336-
337-
def distance_to(self, spec: 'IntegratorValues') -> List[float]:
338-
if self.factor in spec.factor and self.capacitance in spec.capacitance:
339-
return []
340-
else:
341-
return [
342-
abs(self.factor.center() - spec.factor.center()),
343-
abs(self.capacitance.center() - spec.capacitance.center())
344-
]
345-
346-
def intersects(self, spec: 'IntegratorValues') -> bool:
347-
return self.factor.intersects(spec.factor) and \
348-
self.capacitance.intersects(spec.capacitance)
349-
350-
351-
class IntegratorInverting(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock, GeneratorBlock):
316+
class IntegratorInverting(OpampApplication, KiCadSchematicBlock, KiCadImportableBlock):
352317
"""Opamp integrator, outputs the negative integral of the input signal, relative to some reference signal.
353318
Will clip to the input voltage rails.
354319
@@ -384,7 +349,6 @@ def __init__(self, factor: RangeLike, capacitance: RangeLike, *,
384349

385350
self.factor = self.ArgParameter(factor) # output scale factor, 1/RC in units of 1/s
386351
self.capacitance = self.ArgParameter(capacitance)
387-
self.generator_param(self.factor, self.capacitance)
388352

389353
self.actual_factor = self.Parameter(RangeExpr())
390354

@@ -396,10 +360,7 @@ def contents(self) -> None:
396360
" <b>of spec:</b> ", DescriptionString.FormatUnits(self.factor, "")
397361
)
398362

399-
def generate(self) -> None:
400-
super().generate()
401-
402-
self.r = self.Block(Resistor(Range.cancel_multiply(1/self.get(self.capacitance), 1/self.get(self.factor))))
363+
self.r = self.Block(Resistor((1/self.factor).shrink_multiply(1/self.capacitance)))
403364
self.c = self.Block(Capacitor(
404365
capacitance=self.capacitance,
405366
voltage=self.output.link().voltage

edg/abstract_parts/PassiveFilters.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from .Categories import *
88

99

10-
class LowPassRc(AnalogFilter, GeneratorBlock):
10+
class LowPassRc(AnalogFilter, Block):
1111
"""Passive-typed low-pass RC specified by the resistor value (impedance) and -3dB (~70%) cutoff frequency."""
1212
@init_in_parent
1313
def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike, voltage: RangeLike):
@@ -20,16 +20,13 @@ def __init__(self, impedance: RangeLike, cutoff_freq: RangeLike, voltage: RangeL
2020
self.cutoff_freq = self.ArgParameter(cutoff_freq)
2121
self.voltage = self.ArgParameter(voltage)
2222

23-
self.generator_param(self.impedance, self.cutoff_freq)
24-
25-
def generate(self) -> None:
26-
super().generate()
23+
def contents(self) -> None:
24+
super().contents()
2725

2826
self.r = self.Block(Resistor(resistance=self.impedance)) # TODO maybe support power?
29-
# cutoff frequency is 1/(2 pi R C)
30-
capacitance = Range.cancel_multiply(1 / (2 * pi * self.get(self.impedance)), 1 / self.get(self.cutoff_freq))
31-
32-
self.c = self.Block(Capacitor(capacitance=capacitance*Farad, voltage=self.voltage))
27+
self.c = self.Block(Capacitor( # cutoff frequency is 1/(2 pi R C)
28+
capacitance=(1 / (2 * pi * self.cutoff_freq)).shrink_multiply(1 / self.impedance),
29+
voltage=self.voltage))
3330
self.connect(self.input, self.r.a)
3431
self.connect(self.r.b, self.c.pos, self.output) # TODO support negative voltages?
3532
self.connect(self.c.neg, self.gnd)

edg/abstract_parts/ResistiveDivider.py

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,8 @@ class VoltageDivider(Analog, BaseVoltageDivider):
172172
@init_in_parent
173173
def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike) -> None:
174174
super().__init__(impedance=impedance)
175-
176175
self.output_voltage = self.ArgParameter(output_voltage)
177-
178-
ratio_lower = self.output_voltage.lower() / self.input.link().voltage.lower()
179-
ratio_upper = self.output_voltage.upper() / self.input.link().voltage.upper()
180-
self.require(ratio_lower <= ratio_upper,
181-
"can't generate divider to create output voltage of tighter tolerance than input voltage")
182-
self.assign(self.ratio, (ratio_lower, ratio_upper))
176+
self.assign(self.ratio, self.output_voltage.shrink_multiply(1/self.input.link().voltage))
183177

184178

185179
class VoltageSenseDivider(Analog, BaseVoltageDivider):
@@ -193,14 +187,8 @@ class VoltageSenseDivider(Analog, BaseVoltageDivider):
193187
@init_in_parent
194188
def __init__(self, *, full_scale_voltage: RangeLike, impedance: RangeLike) -> None:
195189
super().__init__(impedance=impedance)
196-
197190
self.full_scale_voltage = self.ArgParameter(full_scale_voltage)
198-
199-
ratio_lower = self.full_scale_voltage.lower() / self.input.link().voltage.upper()
200-
ratio_upper = self.full_scale_voltage.upper() / self.input.link().voltage.upper()
201-
self.require(ratio_lower <= ratio_upper,
202-
"can't generate divider to create output voltage of tighter tolerance than input voltage")
203-
self.assign(self.ratio, (ratio_lower, ratio_upper))
191+
self.assign(self.ratio, self.full_scale_voltage / self.input.link().voltage.upper())
204192

205193

206194
class FeedbackVoltageDivider(Analog, BaseVoltageDivider):
@@ -213,10 +201,7 @@ def __init__(self, *, output_voltage: RangeLike, impedance: RangeLike,
213201

214202
self.output_voltage = self.ArgParameter(output_voltage)
215203
self.assumed_input_voltage = self.ArgParameter(assumed_input_voltage)
216-
self.actual_input_voltage = self.Parameter(RangeExpr(
217-
(self.output_voltage.lower() / self.actual_ratio.upper(),
218-
self.output_voltage.upper() / self.actual_ratio.lower())
219-
))
204+
self.actual_input_voltage = self.Parameter(RangeExpr())
220205

221206
def contents(self) -> None:
222207
super().contents()
@@ -227,11 +212,8 @@ def contents(self) -> None:
227212
"\n<b>impedance:</b> ", DescriptionString.FormatUnits(self.actual_impedance, "Ω"),
228213
" <b>of spec:</b> ", DescriptionString.FormatUnits(self.impedance, "Ω"))
229214

230-
ratio_lower = self.output_voltage.upper() / self.assumed_input_voltage.upper()
231-
ratio_upper = self.output_voltage.lower() / self.assumed_input_voltage.lower()
232-
self.require(ratio_lower <= ratio_upper,
233-
"can't generate feedback divider with input voltage of tighter tolerance than output voltage")
234-
self.assign(self.ratio, (ratio_lower, ratio_upper))
215+
self.assign(self.ratio, (1/self.assumed_input_voltage).shrink_multiply(self.output_voltage))
216+
self.assign(self.actual_input_voltage, self.output_voltage / self.actual_ratio)
235217

236218

237219
class SignalDivider(Analog, Block):

edg/core/ArrayExpr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
from typing import *
44

55
from .. import edgir
6-
from .Binding import EqOp, ArrayBinding
6+
from .Binding import EqOp, ArrayBinding, UnarySetOpBinding, BinarySetOpBinding
77
from .ConstraintExpr import ConstraintExpr, IntLike, FloatExpr, FloatLike, RangeExpr, RangeLike, \
88
BoolExpr, BoolLike, StringLike, \
9-
NumericOp, BoolOp, RangeSetOp, Binding, UnarySetOpBinding, BinarySetOpBinding, StringExpr, IntExpr
9+
NumericOp, BoolOp, RangeSetOp, Binding, StringExpr, IntExpr
1010
from .Core import Refable
1111
from .IdentityDict import IdentityDict
1212
from .Ports import BasePort

edg/core/Binding.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class NumericOp(Enum):
2929
# Multiplicative
3030
mul = auto()
3131
invert = auto()
32+
shrink_mul = auto()
3233

3334
class BoolOp(Enum):
3435
op_and = auto()
@@ -316,6 +317,7 @@ def __init__(self,
316317
# Numeric
317318
NumericOp.add: edgir.BinaryExpr.ADD,
318319
NumericOp.mul: edgir.BinaryExpr.MULT,
320+
NumericOp.shrink_mul: edgir.BinaryExpr.SHRINK_MULT,
319321
# Boolean
320322
BoolOp.op_and: edgir.BinaryExpr.AND,
321323
BoolOp.op_or: edgir.BinaryExpr.OR,

edg/core/ConstraintExpr.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
from __future__ import annotations
22

33
from abc import abstractmethod
4-
from functools import reduce
4+
from deprecated import deprecated
55
from itertools import chain
66
from typing import *
77

8-
from .. import edgir
98
from .Binding import Binding, ParamBinding, BoolLiteralBinding, IntLiteralBinding, \
109
FloatLiteralBinding, RangeLiteralBinding, StringLiteralBinding, RangeBuilderBinding, \
11-
UnaryOpBinding, UnarySetOpBinding, BinaryOpBinding, BinarySetOpBinding, IfThenElseBinding
10+
UnaryOpBinding, BinaryOpBinding, IfThenElseBinding
1211
from .Binding import NumericOp, BoolOp, EqOp, OrdOp, RangeSetOp
1312
from .Builder import builder
1413
from .Core import Refable
1514
from .IdentityDict import IdentityDict
1615
from .Range import Range
16+
from .. import edgir
1717

1818
if TYPE_CHECKING:
1919
from .Ports import BasePort
@@ -362,13 +362,9 @@ def _from_lit(cls, pb: edgir.ValueLit) -> Range:
362362
return Range(pb.range.minimum.floating.val, pb.range.maximum.floating.val)
363363

364364
@classmethod
365+
@deprecated("Use shrink_multiply")
365366
def cancel_multiply(cls, input_side: RangeLike, output_side: RangeLike) -> RangeExpr:
366-
"""See Range.cancel_multiply"""
367-
input_expr = cls._to_expr_type(input_side)
368-
output_expr = cls._to_expr_type(output_side)
369-
lower = input_expr.upper() * output_expr.lower()
370-
upper = input_expr.lower() * output_expr.upper()
371-
return cls._to_expr_type((lower, upper)) # rely on internally to check for non-empty range
367+
return RangeExpr._to_expr_type(output_side).shrink_multiply(input_side)
372368

373369
def __init__(self, initializer: Optional[RangeLike] = None) -> None:
374370
# must cast non-empty initializer type, because range supports wider initializers
@@ -448,6 +444,11 @@ def __truediv__(self, rhs: Union[RangeLike, FloatLike, IntLike]) -> RangeExpr:
448444
rhs_cast = self._to_expr_type(rhs) # type: ignore
449445
return self * rhs_cast.__mul_inv__()
450446

447+
def shrink_multiply(self, contributing: RangeLike) -> RangeExpr:
448+
"""RangeExpr version of Range.shrink_multiply.
449+
See docs for Range.shrink_multiply."""
450+
return RangeExpr._create_binary_op(self, self._to_expr_type(contributing), NumericOp.shrink_mul)
451+
451452
def abs(self) -> RangeExpr:
452453
"""Returns a RangeExpr that is the absolute value of this.
453454
Intuitively, this returns a range that contains the absolute value of every point in the input."""

0 commit comments

Comments
 (0)