Skip to content

Commit c01d3e6

Browse files
authored
Refactor core calculation out of buck / boost converters (#407)
This makes the core component calculations more unit-testable and unifies the buck-boost converter (as well as allowing other variations-on-a-theme converters like forward-buck-reverse-boost as with some NVDC battery chargers with OTG with less code duplication) Changes the boost inductor calculation to avoid double-counting output tolerance. The inductance formula appears monotonic. Core infrastructural changes: - Refactor NumLikeExpr types (IntExpr, FloatExpr, RangeExpr) to centrally define castable-types and properly return NotImplemented on a type it cannot handle. This cleanly handles promotion of FloatLike / IntLike types to Range types. - Add hull and intersect to Range Follow-on PRs will add coupled current/inductance support (#405) and the MP2722 converter which this is building towards.
1 parent 99b9e76 commit c01d3e6

File tree

10 files changed

+399
-189
lines changed

10 files changed

+399
-189
lines changed

edg/abstract_parts/AbstractPowerConverters.py

Lines changed: 148 additions & 148 deletions
Large diffs are not rendered by default.
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import unittest
2+
3+
from .AbstractPowerConverters import BuckConverterPowerPath, BoostConverterPowerPath
4+
from ..core import Range
5+
6+
7+
class BuckConverterCalculationTest(unittest.TestCase):
8+
def test_buck_converter(self):
9+
values_ref = BuckConverterPowerPath.calculate_parameters(
10+
Range.exact(5), Range.exact(2.5), Range.exact(100e3), Range.exact(1),
11+
Range.exact(0.1), 0.01, 0.001,
12+
efficiency=Range.exact(1)
13+
)
14+
self.assertEqual(values_ref.dutycycle, Range.exact(0.5))
15+
# validated against https://www.omnicalculator.com/physics/buck-converter
16+
self.assertEqual(values_ref.inductance, Range.exact(125e-6))
17+
18+
# test that component values are calculated for worst-case conversion
19+
values = BuckConverterPowerPath.calculate_parameters(
20+
Range(4, 5), Range(2.5, 4), Range.exact(100e3), Range.exact(1),
21+
Range.exact(0.1), 0.01, 0.001,
22+
efficiency=Range.exact(1)
23+
)
24+
self.assertEqual(values_ref.inductance, values.inductance)
25+
self.assertEqual(values_ref.input_capacitance, values.input_capacitance)
26+
self.assertEqual(values_ref.output_capacitance, values.output_capacitance)
27+
28+
def test_buck_converter_example(self):
29+
# using the example from https://passive-components.eu/buck-converter-design-and-calculation/
30+
values = BuckConverterPowerPath.calculate_parameters(
31+
Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1),
32+
Range.exact(0.35), 1, 0.0165,
33+
efficiency=Range.exact(1)
34+
)
35+
self.assertAlmostEqual(values.dutycycle.upper, 0.298, places=3)
36+
self.assertAlmostEqual(values.inductance.upper, 14.8e-6, places=7)
37+
38+
# the example uses a ripple current of 0.346 for the rest of the calculations
39+
values = BuckConverterPowerPath.calculate_parameters(
40+
Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1),
41+
Range.exact(0.346), 1, 0.0165,
42+
efficiency=Range.exact(1)
43+
)
44+
self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.173, places=3)
45+
self.assertAlmostEqual(values.output_capacitance.lower, 5.24e-6, places=7)
46+
47+
def test_boost_converter(self):
48+
values_ref = BoostConverterPowerPath.calculate_parameters(
49+
Range.exact(5), Range.exact(10), Range.exact(100e3), Range.exact(1),
50+
Range.exact(0.1), 0.01, 0.001,
51+
efficiency=Range.exact(1)
52+
)
53+
self.assertEqual(values_ref.dutycycle, Range.exact(0.5))
54+
# validated against https://www.omnicalculator.com/physics/boost-converter
55+
self.assertEqual(values_ref.inductance, Range.exact(250e-6))
56+
57+
# test that component values are calculated for worst-case conversion
58+
values = BoostConverterPowerPath.calculate_parameters(
59+
Range(5, 8), Range(7, 10), Range.exact(100e3), Range.exact(1),
60+
Range.exact(0.1), 0.01, 0.001,
61+
efficiency=Range.exact(1)
62+
)
63+
self.assertEqual(values_ref.inductance, values.inductance)
64+
self.assertEqual(values_ref.input_capacitance, values.input_capacitance)
65+
self.assertEqual(values_ref.output_capacitance, values.output_capacitance)
66+
67+
def test_boost_converter_example(self):
68+
# using the example from https://passive-components.eu/boost-converter-design-and-calculation/
69+
# 0.4342A ripple current from .35 factor in example converted in output current terms
70+
values = BoostConverterPowerPath.calculate_parameters(
71+
Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5),
72+
Range.exact(0.4342), 1, 1,
73+
efficiency=Range.exact(1)
74+
)
75+
self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3)
76+
self.assertAlmostEqual(values.inductance.upper, 13.75e-6, places=7)
77+
78+
# the example continues with a normalized inductance of 15uH
79+
values = BoostConverterPowerPath.calculate_parameters(
80+
Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5),
81+
Range.exact(.4342*13.75/15), 0.01, 0.06,
82+
efficiency=Range.exact(1)
83+
)
84+
self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3)
85+
self.assertAlmostEqual(values.inductance.upper, 15.0e-6, places=7)
86+
self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.44, places=2)
87+
# the example calculation output is wrong, this is the correct result of the formula
88+
self.assertAlmostEqual(values.output_capacitance.lower, 9.95e-6, places=7)

edg/core/ConstraintExpr.py

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,8 @@ def then_else(self, then_val: IteType, else_val: IteType) -> IteType:
173173
class NumLikeExpr(ConstraintExpr[WrappedType, NumLikeCastable], Generic[WrappedType, NumLikeCastable]):
174174
"""Trait for numeric-like expressions, providing common arithmetic operations"""
175175

176+
_CASTABLE_TYPES: Tuple[Type[NumLikeCastable], ...] # NumLikeCastable for use in ininstance(), excluding self-cls
177+
176178
@classmethod
177179
@abstractmethod
178180
def _to_expr_type(cls: Type[NumLikeSelfType],
@@ -209,28 +211,44 @@ def __mul_inv__(self: NumLikeSelfType) -> NumLikeSelfType:
209211
return self._create_unary_op(self, NumericOp.invert)
210212

211213
def __add__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType:
212-
return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.add)
214+
if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__):
215+
return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.add)
216+
return NotImplemented
213217

214218
def __radd__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType:
215-
return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.add)
219+
if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__):
220+
return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.add)
221+
return NotImplemented
216222

217223
def __sub__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType:
218-
return self.__add__(self._to_expr_type(rhs).__neg__())
224+
if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__):
225+
return self.__add__(self._to_expr_type(rhs).__neg__())
226+
return NotImplemented
219227

220228
def __rsub__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType:
221-
return self.__neg__().__radd__(self._to_expr_type(lhs))
229+
if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__):
230+
return self.__neg__().__radd__(self._to_expr_type(lhs))
231+
return NotImplemented
222232

223233
def __mul__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType:
224-
return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.mul)
234+
if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__):
235+
return self._create_binary_op(self, self._to_expr_type(rhs), NumericOp.mul)
236+
return NotImplemented
225237

226238
def __rmul__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType:
227-
return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.mul)
239+
if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__):
240+
return self._create_binary_op(self._to_expr_type(lhs), self, NumericOp.mul)
241+
return NotImplemented
228242

229243
def __truediv__(self: NumLikeSelfType, rhs: NumLikeCastable) -> NumLikeSelfType:
230-
return self.__mul__(self._to_expr_type(rhs).__mul_inv__())
244+
if isinstance(rhs, self._CASTABLE_TYPES) or isinstance(rhs, self.__class__):
245+
return self.__mul__(self._to_expr_type(rhs).__mul_inv__())
246+
return NotImplemented
231247

232248
def __rtruediv__(self: NumLikeSelfType, lhs: NumLikeCastable) -> NumLikeSelfType:
233-
return self.__mul_inv__().__mul__(self._to_expr_type(lhs))
249+
if isinstance(lhs, self._CASTABLE_TYPES) or isinstance(lhs, self.__class__):
250+
return self.__mul_inv__().__mul__(self._to_expr_type(lhs))
251+
return NotImplemented
234252

235253
@classmethod
236254
def _create_bool_op(cls,
@@ -245,23 +263,35 @@ def _create_bool_op(cls,
245263
return BoolExpr()._bind(BinaryOpBinding(lhs, rhs, op))
246264

247265
def __ne__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore
248-
return self._create_bool_op(self, self._to_expr_type(other), EqOp.ne)
266+
if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__):
267+
return self._create_bool_op(self, self._to_expr_type(other), EqOp.ne)
268+
return NotImplemented
249269

250270
def __gt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore
251-
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.gt)
271+
if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__):
272+
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.gt)
273+
return NotImplemented
252274

253275
def __ge__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore
254-
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.ge)
276+
if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__):
277+
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.ge)
278+
return NotImplemented
255279

256280
def __lt__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore
257-
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.lt)
281+
if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__):
282+
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.lt)
283+
return NotImplemented
258284

259285
def __le__(self: NumLikeSelfType, other: NumLikeCastable) -> BoolExpr: #type: ignore
260-
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.le)
286+
if isinstance(other, self._CASTABLE_TYPES) or isinstance(other, self.__class__):
287+
return self._create_bool_op(self, self._to_expr_type(other), OrdOp.le)
288+
return NotImplemented
261289

262290

263291
IntLike = Union['IntExpr', int]
264292
class IntExpr(NumLikeExpr[int, IntLike]):
293+
_CASTABLE_TYPES = (int, )
294+
265295
@classmethod
266296
def _to_expr_type(cls, input: IntLike) -> IntExpr:
267297
if isinstance(input, IntExpr):
@@ -287,6 +317,8 @@ def _from_lit(cls, pb: edgir.ValueLit) -> int:
287317
FloatLit = Union[float, int]
288318
FloatLike = Union['FloatExpr', float, int]
289319
class FloatExpr(NumLikeExpr[float, Union[FloatLike, IntExpr]]):
320+
_CASTABLE_TYPES = (float, int)
321+
290322
@classmethod
291323
def _to_expr_type(cls, input: Union[FloatLike, IntExpr]) -> FloatExpr:
292324
if isinstance(input, FloatExpr):
@@ -320,6 +352,9 @@ def max(self, other: FloatLike) -> FloatExpr:
320352

321353
RangeLike = Union['RangeExpr', Range, Tuple[FloatLike, FloatLike]]
322354
class RangeExpr(NumLikeExpr[Range, Union[RangeLike, FloatLike, IntExpr]]):
355+
# mypy doesn't like the unbounded tuple
356+
_CASTABLE_TYPES = (float, int, FloatExpr, IntExpr, Range, tuple) # type: ignore
357+
323358
# Some range literals for defaults
324359
POSITIVE: Range = Range.from_lower(0.0)
325360
NEGATIVE: Range = Range.from_upper(0.0)
@@ -424,26 +459,6 @@ def _create_range_float_binary_op(cls,
424459
assert lhs._is_bound() and rhs._is_bound()
425460
return lhs._new_bind(BinaryOpBinding(lhs, rhs, op))
426461

427-
# special option to allow range * float
428-
def __mul__(self, rhs: Union[RangeLike, FloatLike, IntLike]) -> RangeExpr:
429-
if isinstance(rhs, (int, float)): # TODO clean up w/ literal to expr pass, then type based on that
430-
rhs_cast: Union[FloatExpr, IntExpr, RangeExpr] = FloatExpr._to_expr_type(rhs)
431-
elif isinstance(rhs, (FloatExpr, IntExpr)):
432-
rhs_cast = rhs
433-
else:
434-
rhs_cast = self._to_expr_type(rhs) # type: ignore
435-
return self._create_range_float_binary_op(self, rhs_cast, NumericOp.mul)
436-
437-
# special option to allow range / float
438-
def __truediv__(self, rhs: Union[RangeLike, FloatLike, IntLike]) -> RangeExpr:
439-
if isinstance(rhs, (int, float)): # TODO clean up w/ literal to expr pass, then type based on that
440-
rhs_cast: Union[FloatExpr, IntExpr, RangeExpr] = FloatExpr._to_expr_type(rhs)
441-
elif isinstance(rhs, (FloatExpr, IntExpr)):
442-
rhs_cast = rhs
443-
else:
444-
rhs_cast = self._to_expr_type(rhs) # type: ignore
445-
return self * rhs_cast.__mul_inv__()
446-
447462
def shrink_multiply(self, contributing: RangeLike) -> RangeExpr:
448463
"""RangeExpr version of Range.shrink_multiply.
449464
See docs for Range.shrink_multiply."""

edg/core/Range.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,18 @@ def __contains__(self, item: Union['Range', float]) -> bool:
144144
else:
145145
return NotImplemented
146146

147+
def hull(self, other: 'Range') -> 'Range':
148+
return Range(min(self.lower, other.lower), max(self.upper, other.upper))
149+
147150
def intersects(self, other: 'Range') -> bool:
148151
return (self.upper >= other.lower) and (self.lower <= other.upper)
149152

153+
def intersect(self, other: 'Range') -> 'Range':
154+
# TODO make behavior more consistent w/ compiler and returning empty that props as a unit
155+
if not self.intersects(other):
156+
raise ValueError("cannot intersect ranges that do not intersect")
157+
return Range(max(self.lower, other.lower), min(self.upper, other.upper))
158+
150159
def __add__(self, other: Union['Range', float]) -> 'Range':
151160
if isinstance(other, Range):
152161
return Range(self.lower + other.lower, self.upper + other.upper)

edg/core/test_range.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def test_ops(self) -> None:
4747

4848
self.assertEqual(Range(1, 5).center(), 3)
4949

50-
def test_intersect(self) -> None:
50+
def test_intersects(self) -> None:
5151
self.assertTrue(Range(-1, 2).intersects(Range(2, 3)))
5252
self.assertTrue(Range(-1, 2).intersects(Range(0, 3)))
5353
self.assertTrue(Range(-1, 2).intersects(Range(-2, -1)))
@@ -56,6 +56,22 @@ def test_intersect(self) -> None:
5656
self.assertFalse(Range(-1, 2).intersects(Range(3, 4)))
5757
self.assertFalse(Range(-1, 2).intersects(Range(-3, -2)))
5858

59+
def test_intersect(self):
60+
self.assertEqual(Range(-1, 2).intersect(Range(2, 3)), Range(2, 2))
61+
self.assertEqual(Range(-1, 2).intersect(Range(0, 3)), Range(0, 2))
62+
self.assertEqual(Range(-1, 2).intersect(Range(-2, -1)), Range(-1, -1))
63+
self.assertEqual(Range(-1, 2).intersect(Range(-2, 0)), Range(-1, 0))
64+
self.assertEqual(Range(-1, 2).intersect(Range(0, 1)), Range(0, 1))
65+
with self.assertRaises(ValueError):
66+
Range(-1, 2).intersect(Range(3, 4))
67+
68+
def test_hull(self):
69+
self.assertEqual(Range(-1, 2).hull(Range(2, 3)), Range(-1, 3))
70+
self.assertEqual(Range(-1, 2).hull(Range(0, 3)), Range(-1, 3))
71+
self.assertEqual(Range(-1, 2).hull(Range(-2, -1)), Range(-2, 2))
72+
self.assertEqual(Range(-1, 2).hull(Range(-2, 0)), Range(-2, 2))
73+
self.assertEqual(Range(-1, 2).hull(Range(0, 1)), Range(-1, 2))
74+
5975
def test_shrink_property(self) -> None:
6076
range1 = Range(10, 20)
6177
self.assertEqual(range1.shrink_multiply(1/range1), Range(1, 1))

examples/Fcml/Fcml.net

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@
279279
(value "4.1A 4.7uH ±30% 23.4mΩ SMD,8x8x4.2mm Power Inductors ROHS")
280280
(footprint "Inductor_SMD:L_Taiyo-Yuden_NR-80xx")
281281
(property (name "Sheetname") (value "power_path"))
282-
(property (name "Sheetfile") (value "edg.abstract_parts.AbstractPowerConverters.BuckConverterPowerPath"))
282+
(property (name "Sheetfile") (value "examples.test_fcml.FcmlPowerPath"))
283283
(property (name "edg_path") (value "conv.power_path.inductor"))
284284
(property (name "edg_short_path") (value "conv.power_path.inductor"))
285285
(property (name "edg_refdes") (value "L2"))

examples/Fcml/Fcml.ref.net

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,7 @@
279279
(value "4.1A 4.7uH ±30% 23.4mΩ SMD,8x8x4.2mm Power Inductors ROHS")
280280
(footprint "Inductor_SMD:L_Taiyo-Yuden_NR-80xx")
281281
(property (name "Sheetname") (value "power_path"))
282-
(property (name "Sheetfile") (value "edg.abstract_parts.AbstractPowerConverters.BuckConverterPowerPath"))
282+
(property (name "Sheetfile") (value "examples.test_fcml.FcmlPowerPath"))
283283
(property (name "edg_path") (value "conv.power_path.inductor"))
284284
(property (name "edg_short_path") (value "conv.power_path.inductor"))
285285
(property (name "edg_refdes") (value "L2"))

examples/Simon/Simon.net

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,14 @@
276276
(sheetpath (names "/pwr/fb/") (tstamps "/02b3015a/013000c9/"))
277277
(tstamps "175b043f"))
278278
(comp (ref "pwr.power_path.inductor")
279-
(value "390mA 47uH ±10% 1210 Power Inductors ROHS")
279+
(value "250mA 33uH ±10% 800mΩ 1210 Inductors (SMD) ROHS")
280280
(footprint "Inductor_SMD:L_1210_3225Metric")
281281
(property (name "Sheetname") (value "power_path"))
282282
(property (name "Sheetfile") (value "edg.abstract_parts.AbstractPowerConverters.BoostConverterPowerPath"))
283283
(property (name "edg_path") (value "pwr.power_path.inductor"))
284284
(property (name "edg_short_path") (value "pwr.power_path.inductor"))
285285
(property (name "edg_refdes") (value "L1"))
286-
(property (name "edg_part") (value "BRL3225T470K (Taiyo Yuden)"))
286+
(property (name "edg_part") (value "CMI322513J330KT (FH(Guangdong Fenghua Advanced Tech))"))
287287
(sheetpath (names "/pwr/power_path/") (tstamps "/02b3015a/1786043a/"))
288288
(tstamps "0f2b0369"))
289289
(comp (ref "pwr.power_path.in_cap")

examples/Simon/Simon.ref.net

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,14 @@
276276
(sheetpath (names "/pwr/fb/") (tstamps "/02b3015a/013000c9/"))
277277
(tstamps "175b043f"))
278278
(comp (ref "L1")
279-
(value "390mA 47uH ±10% 1210 Power Inductors ROHS")
279+
(value "250mA 33uH ±10% 800mΩ 1210 Inductors (SMD) ROHS")
280280
(footprint "Inductor_SMD:L_1210_3225Metric")
281281
(property (name "Sheetname") (value "power_path"))
282282
(property (name "Sheetfile") (value "edg.abstract_parts.AbstractPowerConverters.BoostConverterPowerPath"))
283283
(property (name "edg_path") (value "pwr.power_path.inductor"))
284284
(property (name "edg_short_path") (value "pwr.power_path.inductor"))
285285
(property (name "edg_refdes") (value "L1"))
286-
(property (name "edg_part") (value "BRL3225T470K (Taiyo Yuden)"))
286+
(property (name "edg_part") (value "CMI322513J330KT (FH(Guangdong Fenghua Advanced Tech))"))
287287
(sheetpath (names "/pwr/power_path/") (tstamps "/02b3015a/1786043a/"))
288288
(tstamps "0f2b0369"))
289289
(comp (ref "C4")

0 commit comments

Comments
 (0)