Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
a7e64ce
serialization
ducky64 Jun 8, 2025
a336537
serialization
ducky64 Jun 8, 2025
b453c32
Update PartsTable.py
ducky64 Jun 8, 2025
4098902
Update PartsTable.py
ducky64 Jun 9, 2025
c857bb0
wip
ducky64 Jun 9, 2025
3670302
cleaning
ducky64 Jun 9, 2025
0e75c76
wip prop through capacitance
ducky64 Jun 9, 2025
7bb7496
Update AbstractPowerConverters.py
ducky64 Jun 9, 2025
92087e7
plan?
ducky64 Jun 9, 2025
e67796e
wip refactor all the things
ducky64 Jun 11, 2025
c7a783a
clean up and unpeg inductor values
ducky64 Jun 11, 2025
a357318
fix switch current limits
ducky64 Jun 11, 2025
23ad690
fix constraintexpr
ducky64 Jun 11, 2025
86016ef
rebaseline netlists
ducky64 Jun 11, 2025
8c42733
Update ConstraintExpr.py
ducky64 Jun 11, 2025
a5bfd2e
define using switch currents
ducky64 Jun 11, 2025
70054f7
cleaning
ducky64 Jun 11, 2025
d34d138
slightly backsliding optimizations
ducky64 Jun 11, 2025
611d103
better inductance calculation
ducky64 Jun 12, 2025
b52d66f
rebaseline netlists
ducky64 Jun 13, 2025
2c0b802
revamp boost converters
ducky64 Jun 15, 2025
3a63c01
fix
ducky64 Jun 15, 2025
887cf2a
rebasseline
ducky64 Jun 15, 2025
fdb20c8
wip
ducky64 Jun 15, 2025
79504d9
cleaning and tuning
ducky64 Jun 15, 2025
727135d
Update AbstractPowerConverters.py
ducky64 Jun 15, 2025
0f6be78
improved docs
ducky64 Jun 15, 2025
d853bed
cleaning docs
ducky64 Jun 15, 2025
4f55931
cleaning
ducky64 Jun 15, 2025
8333668
Update AbstractPowerConverters.py
ducky64 Jun 15, 2025
2a40f46
Update AbstractPowerConverters.py
ducky64 Jun 15, 2025
cb84066
add fallback min-current limit
ducky64 Jun 15, 2025
23ea64f
lots of fixes
ducky64 Jun 16, 2025
46bf0e3
wip cleaning
ducky64 Jun 16, 2025
6f7b22c
fix all the things
ducky64 Jun 16, 2025
f27bd72
Remove ripple factor from params
ducky64 Jun 16, 2025
3815fc3
Update AbstractPowerConverters.py
ducky64 Jun 16, 2025
81e5816
constiency
ducky64 Jun 16, 2025
ec3e8e9
Update PartsTable.py
ducky64 Jun 16, 2025
c1eca25
Update PartsTable.py
ducky64 Jun 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 15 additions & 4 deletions edg/abstract_parts/AbstractInductor.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Dict, Optional, cast

from ..electronics_model import *
from .PartsTable import PartsTableColumn, PartsTableRow
from .PartsTable import PartsTableColumn, PartsTableRow, ExperimentalUserFnPartsTable
from .PartsTablePart import PartsTableSelector
from .Categories import *
from .StandardFootprint import StandardFootprint, HasStandardFootprint
Expand Down Expand Up @@ -94,7 +94,9 @@ def symbol_pinning(self, symbol_name: str) -> Dict[str, BasePort]:
def __init__(self, inductance: RangeLike,
current: RangeLike = RangeExpr.ZERO,
frequency: RangeLike = RangeExpr.ZERO,
resistance_dc: RangeLike = (0, 1)*Ohm # generic sane choice?
resistance_dc: RangeLike = (0, 1)*Ohm, # generic sane choice?
*,
experimental_filter_fn: StringLike = ""
) -> None:
super().__init__()

Expand All @@ -105,6 +107,7 @@ def __init__(self, inductance: RangeLike,
self.current = self.ArgParameter(current) # defined as operating current range, non-directioned
self.frequency = self.ArgParameter(frequency) # defined as operating frequency range
self.resistance_dc = self.ArgParameter(resistance_dc)
self.experimental_filter_fn = self.ArgParameter(experimental_filter_fn)

self.actual_inductance = self.Parameter(RangeExpr())
self.actual_current_rating = self.Parameter(RangeExpr())
Expand Down Expand Up @@ -136,15 +139,23 @@ class TableInductor(PartsTableSelector, Inductor):
@init_in_parent
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.generator_param(self.inductance, self.current, self.frequency, self.resistance_dc)
self.generator_param(self.inductance, self.current, self.frequency, self.resistance_dc,
self.experimental_filter_fn)

def _row_filter(self, row: PartsTableRow) -> bool:
# TODO eliminate arbitrary DCR limit in favor of exposing max DCR to upper levels
filter_fn_str = self.get(self.experimental_filter_fn)
if filter_fn_str:
filter_fn = ExperimentalUserFnPartsTable.deserialize_fn(filter_fn_str)
else:
filter_fn = None

return super()._row_filter(row) and \
row[self.INDUCTANCE].fuzzy_in(self.get(self.inductance)) and \
self.get(self.current).fuzzy_in(row[self.CURRENT_RATING]) and \
row[self.DC_RESISTANCE].fuzzy_in(self.get(self.resistance_dc)) and \
self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING])
self.get(self.frequency).fuzzy_in(row[self.FREQUENCY_RATING]) and\
(filter_fn is None or filter_fn(row))

def _row_generate(self, row: PartsTableRow) -> None:
super()._row_generate(row)
Expand Down
381 changes: 240 additions & 141 deletions edg/abstract_parts/AbstractPowerConverters.py

Large diffs are not rendered by default.

97 changes: 91 additions & 6 deletions edg/abstract_parts/PartsTable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,12 @@

import csv
import itertools
import sys
from typing import TypeVar, Generic, Type, overload, Union, Callable, List, Dict, Any, KeysView, Optional, OrderedDict, \
cast
cast, Tuple, Sequence, Protocol

if sys.version_info[1] < 8:
from typing_extensions import Protocol
else:
from typing import Protocol
from typing_extensions import ParamSpec

from ..core import Range


# from https://stackoverflow.com/questions/47965083/comparable-types-with-mypy
Expand Down Expand Up @@ -152,3 +150,90 @@ def first(self, err="no elements in list") -> PartsTableRow:
if not self.rows:
raise IndexError(err)
return self.rows[0]


UserFnMetaParams = ParamSpec('UserFnMetaParams')
UserFnType = TypeVar('UserFnType', bound=Callable, covariant=True)
class UserFnSerialiable(Protocol[UserFnMetaParams, UserFnType]):
"""A protocol that marks functions as usable in deserialize, that they have been registered."""
_is_serializable: None # guard attribute

def __call__(self, *args: UserFnMetaParams.args, **kwargs: UserFnMetaParams.kwargs) -> UserFnType: ...
__name__: str


class ExperimentalUserFnPartsTable(PartsTable):
"""A PartsTable that can take in a user-defined function for filtering and (possibly) other operations.
These functions are serialized to a string by an internal name (cannot execute arbitrary code,
bounded to defined functions in the codebase), and some arguments can be serialized with the name
(think partial(...)).
Functions must be pre-registered using the @ExperimentalUserFnPartsTable.user_fn(...) decorator,
non-pre-registered functions will not be available.

This is intended to support searches on parts tables that are cross-coupled across multiple parameters,
but still restricted to within on table (e.g., no cross-optimizing RC filters).

EXPERIMENTAL - subject to change without notice."""

_FN_SERIALIZATION_SEPARATOR = ";"

_user_fns: Dict[str, Tuple[Callable, Sequence[Type]]] = {} # name -> fn, [arg types]
_fn_name_dict: Dict[Callable, str] = {}

@staticmethod
def user_fn(param_types: Sequence[Type] = []) -> Callable[[Callable[UserFnMetaParams, UserFnType]],
UserFnSerialiable[UserFnMetaParams, UserFnType]]:
def decorator(fn: Callable[UserFnMetaParams, UserFnType]) -> UserFnSerialiable[UserFnMetaParams, UserFnType]:
"""Decorator to register a user function that can be used in ExperimentalUserFnPartsTable."""
if fn.__name__ in ExperimentalUserFnPartsTable._user_fns or fn in ExperimentalUserFnPartsTable._fn_name_dict:
raise ValueError(f"Function {fn.__name__} already registered.")
ExperimentalUserFnPartsTable._user_fns[fn.__name__] = (fn, param_types)
ExperimentalUserFnPartsTable._fn_name_dict[fn] = fn.__name__
return fn # type: ignore
return decorator

@classmethod
def serialize_fn(cls, fn: UserFnSerialiable[UserFnMetaParams, UserFnType],
*args: UserFnMetaParams.args, **kwargs: UserFnMetaParams.kwargs) -> str:
"""Serializes a user function to a string."""
assert not kwargs, "kwargs not supported in serialization"
if fn not in cls._fn_name_dict:
raise ValueError(f"Function {fn} not registered.")
fn_ctor, fn_argtypes = cls._user_fns[fn.__name__]
def serialize_arg(tpe: Type, val: Any) -> str:
assert isinstance(val, tpe), f"in serialize {val}, expected {tpe}, got {type(val)}"
if tpe is bool:
return str(val)
elif tpe is int:
return str(val)
elif tpe is float:
return str(val)
elif tpe is Range:
return f"({val.lower},{val.upper})"
else:
raise TypeError(f"cannot serialize type {tpe} in user function serialization")
serialized_args = [serialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, args)]
return cls._FN_SERIALIZATION_SEPARATOR.join([fn.__name__] + serialized_args)

@classmethod
def deserialize_fn(cls, serialized: str) -> Callable:
"""Deserializes a user function from a string."""
split = serialized.split(cls._FN_SERIALIZATION_SEPARATOR)
if split[0] not in cls._user_fns:
raise ValueError(f"Function {serialized} not registered.")
fn_ctor, fn_argtypes = cls._user_fns[split[0]]
assert len(split) == len(fn_argtypes) + 1
def deserialize_arg(tpe: Type, val: str) -> Any:
if tpe is bool:
return val == 'True'
elif tpe is int:
return int(val)
elif tpe is float:
return float(val)
elif tpe is Range:
parts = val[1:-1].split(",")
return Range(float(parts[0]), float(parts[1])) # type: ignore
else:
raise TypeError(f"cannot deserialize type {tpe} in user function serialization")
deserialized_args = [deserialize_arg(tpe, arg) for tpe, arg in zip(fn_argtypes, split[1:])]
return fn_ctor(*deserialized_args)
46 changes: 46 additions & 0 deletions edg/abstract_parts/test_parts_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,49 @@ def test_map(self) -> None:

def test_first(self) -> None:
self.assertEqual(self.table.first().value, {'header1': '1', 'header2': 'foo', 'header3': '9'})


class UserFnPartsTableTest(unittest.TestCase):
@staticmethod
@ExperimentalUserFnPartsTable.user_fn()
def user_fn_false() -> Callable[[], bool]:
def inner() -> bool:
return False
return inner

@staticmethod
@ExperimentalUserFnPartsTable.user_fn([bool])
def user_fn_bool_pass(meta_arg: bool) -> Callable[[], bool]:
def inner() -> bool:
return meta_arg
return inner

@staticmethod
@ExperimentalUserFnPartsTable.user_fn([float])
def user_fn_float_pass(meta_arg: float) -> Callable[[], float]:
def inner() -> float:
return meta_arg
return inner

@staticmethod
def user_fn_unserialized() -> Callable[[], None]:
def inner() -> None:
return None
return inner

def test_serialize_deserialize(self) -> None:
self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false), 'user_fn_false')
self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn(
ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_false))(), False)

self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False),
'user_fn_bool_pass;False')
self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn(
ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, False))(), False)
self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn(
ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_bool_pass, True))(), True)

self.assertEqual(ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42),
'user_fn_float_pass;0.42')
self.assertEqual(ExperimentalUserFnPartsTable.deserialize_fn(
ExperimentalUserFnPartsTable.serialize_fn(self.user_fn_float_pass, 0.42))(), 0.42)
43 changes: 22 additions & 21 deletions edg/abstract_parts/test_switching_converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
from ..core import Range


class BuckConverterCalculationTest(unittest.TestCase):
class SwitchingConverterCalculationTest(unittest.TestCase):
def test_buck_converter(self):
values_ref = BuckConverterPowerPath.calculate_parameters(
values_ref = BuckConverterPowerPath._calculate_parameters(
Range.exact(5), Range.exact(2.5), Range.exact(100e3), Range.exact(1),
Range.exact(0.1), 0.01, 0.001,
Range.exact(1), Range.exact(0.1), 0.01, 0.001,
efficiency=Range.exact(1)
)
self.assertEqual(values_ref.dutycycle, Range.exact(0.5))
# validated against https://www.omnicalculator.com/physics/buck-converter
self.assertEqual(values_ref.inductance, Range.exact(125e-6))

# test that component values are calculated for worst-case conversion
values = BuckConverterPowerPath.calculate_parameters(
values = BuckConverterPowerPath._calculate_parameters(
Range(4, 5), Range(2.5, 4), Range.exact(100e3), Range.exact(1),
Range.exact(0.1), 0.01, 0.001,
Range.exact(1), Range.exact(0.1), 0.01, 0.001,
efficiency=Range.exact(1)
)
self.assertEqual(values_ref.inductance, values.inductance)
Expand All @@ -27,37 +27,38 @@ def test_buck_converter(self):

def test_buck_converter_example(self):
# using the example from https://passive-components.eu/buck-converter-design-and-calculation/
values = BuckConverterPowerPath.calculate_parameters(
values = BuckConverterPowerPath._calculate_parameters(
Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1),
Range.exact(0.35), 1, 0.0165,
Range.exact(2), Range.exact(0.35), 1, 0.0165,
efficiency=Range.exact(1)
)
self.assertAlmostEqual(values.dutycycle.upper, 0.298, places=3)
self.assertAlmostEqual(values.inductance.upper, 14.8e-6, places=7)

# the example uses a ripple current of 0.346 for the rest of the calculations
values = BuckConverterPowerPath.calculate_parameters(
values = BuckConverterPowerPath._calculate_parameters(
Range.exact(12 + 0.4), Range.exact(3.3 + 0.4), Range.exact(500e3), Range.exact(1),
Range.exact(0.346), 1, 0.0165,
Range.exact(2), Range.exact(0.346), 1, 0.0165,
efficiency=Range.exact(1)
)
self.assertAlmostEqual(values.inductor_peak_currents.upper, 1.173, places=3)
self.assertAlmostEqual(values.output_capacitance.lower, 5.24e-6, places=7)

def test_boost_converter(self):
values_ref = BoostConverterPowerPath.calculate_parameters(
Range.exact(5), Range.exact(10), Range.exact(100e3), Range.exact(1),
Range.exact(0.1), 0.01, 0.001,
values_ref = BoostConverterPowerPath._calculate_parameters(
Range.exact(5), Range.exact(10), Range.exact(100e3), Range.exact(0.5),
Range.exact(2), Range.exact(0.4), 0.01, 0.001,
efficiency=Range.exact(1)
)
self.assertEqual(values_ref.dutycycle, Range.exact(0.5))
# validated against https://www.omnicalculator.com/physics/boost-converter
self.assertEqual(values_ref.inductance, Range.exact(250e-6))
self.assertEqual(values_ref.inductance, Range.exact(62.5e-6))
self.assertEqual(values_ref.inductor_avg_current, Range.exact(1))

# test that component values are calculated for worst-case conversion
values = BoostConverterPowerPath.calculate_parameters(
Range(5, 8), Range(7, 10), Range.exact(100e3), Range.exact(1),
Range.exact(0.1), 0.01, 0.001,
values = BoostConverterPowerPath._calculate_parameters(
Range(5, 8), Range(7, 10), Range.exact(100e3), Range.exact(0.5),
Range.exact(2), Range.exact(0.4), 0.01, 0.001,
efficiency=Range.exact(1)
)
self.assertEqual(values_ref.inductance, values.inductance)
Expand All @@ -66,19 +67,19 @@ def test_boost_converter(self):

def test_boost_converter_example(self):
# using the example from https://passive-components.eu/boost-converter-design-and-calculation/
# 0.4342A ripple current from .35 factor in example converted in output current terms
values = BoostConverterPowerPath.calculate_parameters(
values = BoostConverterPowerPath._calculate_parameters(
Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5),
Range.exact(0.4342), 1, 1,
Range.exact(2), Range.exact(0.35), 1, 1,
efficiency=Range.exact(1)
)
self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3)
self.assertAlmostEqual(values.inductance.upper, 13.75e-6, places=7)
self.assertAlmostEqual(values.inductor_avg_current.upper, 1.24, places=2)

# the example continues with a normalized inductance of 15uH
values = BoostConverterPowerPath.calculate_parameters(
values = BoostConverterPowerPath._calculate_parameters(
Range.exact(5), Range.exact(12 + 0.4), Range.exact(500e3), Range.exact(0.5),
Range.exact(.4342*13.75/15), 0.01, 0.06,
Range.exact(2), Range.exact(0.321), 0.01, 0.06,
efficiency=Range.exact(1)
)
self.assertAlmostEqual(values.dutycycle.upper, 0.597, places=3)
Expand Down
35 changes: 27 additions & 8 deletions edg/core/ConstraintExpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class ConstraintExpr(Refable, Generic[WrappedType, CastableType]):
"""Base class for constraint expressions. Basically a container for operations.
Actual meaning is held in the Binding.
"""
_CASTABLE_TYPES: Tuple[Type[CastableType], ...] # for use in ininstance(), excluding self-cls

def __repr__(self) -> str:
if self.binding is not None and self.initializer is not None:
return f"{super().__repr__()}({self.binding})={self.initializer}"
Expand Down Expand Up @@ -101,6 +103,9 @@ def __eq__(self: SelfType, other: ConstraintExprCastable) -> BoolExpr: #type: i
BoolLike = Union[bool, 'BoolExpr']
class BoolExpr(ConstraintExpr[bool, BoolLike]):
"""Boolean expression, can be used as a constraint"""

_CASTABLE_TYPES = (bool, )

@classmethod
def _to_expr_type(cls, input: BoolLike) -> BoolExpr:
if isinstance(input, BoolExpr):
Expand Down Expand Up @@ -159,11 +164,23 @@ def implies(self, target: BoolLike) -> BoolExpr:
def __invert__(self) -> BoolExpr:
return self._new_bind(UnaryOpBinding(self, BoolOp.op_not))


# does not seem possible to restrict type-params of type vars pre-Python3.12
# so where a single cast is needed we're stuck with Any
IteType = TypeVar('IteType', bound=ConstraintExpr)
def then_else(self, then_val: IteType, else_val: IteType) -> IteType:
assert isinstance(then_val, type(else_val)) and isinstance(else_val, type(then_val)), \
f"if-then-else results must be of same type, got then={then_val}, else={else_val}"
@overload
def then_else(self, then_val: IteType, else_val: IteType) -> IteType: ... # optional strongest-typed version
@overload
def then_else(self, then_val: IteType, else_val: Any) -> IteType: ...
@overload
def then_else(self, then_val: Any, else_val: IteType) -> IteType: ...

def then_else(self, then_val: Any, else_val: Any) -> ConstraintExpr: # type: ignore
if isinstance(then_val, ConstraintExpr):
else_val = then_val._to_expr_type(else_val)
elif isinstance(else_val, ConstraintExpr):
then_val = else_val._to_expr_type(then_val)
else:
raise ValueError("either then_val or else_val must be ConstraintExpr, TODO support dual-casting")
assert self._is_bound() and then_val._is_bound() and else_val._is_bound()
return then_val._new_bind(IfThenElseBinding(self, then_val, else_val))

Expand All @@ -173,8 +190,6 @@ def then_else(self, then_val: IteType, else_val: IteType) -> IteType:
class NumLikeExpr(ConstraintExpr[WrappedType, NumLikeCastable], Generic[WrappedType, NumLikeCastable]):
"""Trait for numeric-like expressions, providing common arithmetic operations"""

_CASTABLE_TYPES: Tuple[Type[NumLikeCastable], ...] # NumLikeCastable for use in ininstance(), excluding self-cls

@classmethod
@abstractmethod
def _to_expr_type(cls: Type[NumLikeSelfType],
Expand Down Expand Up @@ -479,6 +494,9 @@ def abs(self) -> RangeExpr:
StringLike = Union['StringExpr', str]
class StringExpr(ConstraintExpr[str, StringLike]):
"""String expression, can be used as a constraint"""

_CASTABLE_TYPES = (str, )

@classmethod
def _to_expr_type(cls, input: StringLike) -> StringExpr:
if isinstance(input, StringExpr):
Expand Down Expand Up @@ -596,7 +614,8 @@ def __rmul__(self, other: Union[float, Range, Tuple[float, float]]) -> Union[Flo
return FloatExpr._to_expr_type(other * self.scale)
elif isinstance(other, Range):
return RangeExpr._to_expr_type(other * self.scale)
elif isinstance(other, tuple) and isinstance(other[0], (int, float)) and isinstance(other[1], (int, float)):
return RangeExpr._to_expr_type(Range(other[0], other[1]) * self.scale)
elif isinstance(other, tuple) and isinstance(other[0], (int, float, IntExpr, FloatExpr)) \
and isinstance(other[1], (int, float, IntExpr, FloatExpr)):
return RangeExpr._to_expr_type((other[0] * self.scale, other[1] * self.scale))
else:
raise TypeError(f"expected Float or Range Literal, got {other} of type {type(other)}")
Loading