diff --git a/examples/iterative_design_nand.py b/examples/iterative_design_nand.py index 23ad81cd..0b43bb57 100644 --- a/examples/iterative_design_nand.py +++ b/examples/iterative_design_nand.py @@ -33,10 +33,10 @@ class PowerSource(Module): class XOR_with_NANDS(F.LogicGates.XOR): - nands = L.list_field(4, lambda: F.LogicGates.NAND(F.Constant(2))) + nands = L.list_field(4, lambda: F.LogicGates.NAND(2)) def __init__(self): - super().__init__(F.Constant(2)) + super().__init__(2) def __preinit__(self): A = self.inputs[0] @@ -72,7 +72,7 @@ def App(): logic_in = F.Logic() logic_out = F.Logic() - xor = F.LogicGates.XOR(F.Constant(2)) + xor = F.LogicGates.XOR(2) logic_out.connect(xor.get_trait(F.LogicOps.can_logic_xor).xor(logic_in, on)) # led @@ -124,9 +124,11 @@ def App(): F.ElectricLogic.has_pulls ): for pull_resistor in (r for r in t.get_pulls() if r): - pull_resistor.resistance.merge(F.Range.from_center_rel(100 * P.kohm, 0.05)) - power_source.power.voltage.merge(3 * P.V) - led.led.led.brightness.merge( + pull_resistor.resistance.constrain_subset( + L.Range.from_center_rel(100 * P.kohm, 0.05) + ) + power_source.power.voltage.constrain_subset(L.Range.from_center_rel(3 * P.V, 0.05)) + led.led.led.brightness.constrain_subset( TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value ) diff --git a/examples/mcu.py b/examples/mcu.py index bbba714b..7a0d9edc 100644 --- a/examples/mcu.py +++ b/examples/mcu.py @@ -29,8 +29,8 @@ class App(Module): def __preinit__(self) -> None: # Parametrize - self.led.led.led.color.merge(F.LED.Color.YELLOW) - self.led.led.led.brightness.merge( + self.led.led.led.color.constrain_subset(F.LED.Color.YELLOW) + self.led.led.led.brightness.constrain_subset( TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value ) diff --git a/examples/minimal_led.py b/examples/minimal_led.py index a48349a7..9858521f 100644 --- a/examples/minimal_led.py +++ b/examples/minimal_led.py @@ -26,8 +26,8 @@ def __preinit__(self) -> None: self.led.power.connect(self.battery.power) # Parametrize - self.led.led.color.merge(F.LED.Color.YELLOW) - self.led.led.brightness.merge( + self.led.led.color.constrain_subset(F.LED.Color.YELLOW) + self.led.led.brightness.constrain_subset( TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value ) diff --git a/examples/minimal_led_orderable.py b/examples/minimal_led_orderable.py index b440a909..745fdb9f 100644 --- a/examples/minimal_led_orderable.py +++ b/examples/minimal_led_orderable.py @@ -19,7 +19,7 @@ from faebryk.exporters.pcb.layout.typehierarchy import LayoutTypeHierarchy from faebryk.libs.app.checks import run_checks from faebryk.libs.app.manufacturing import export_pcba_artifacts -from faebryk.libs.app.parameters import replace_tbd_with_any, resolve_dynamic_parameters +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.brightness import TypicalLuminousIntensity from faebryk.libs.examples.buildutil import BUILD_DIR, PCB_FILE, apply_design_to_pcb from faebryk.libs.examples.pickers import add_example_pickers @@ -58,8 +58,8 @@ def __preinit__(self) -> None: self.led.power.connect_via(self.power_button, self.battery.power) # Parametrize - self.led.led.color.merge(F.LED.Color.YELLOW) - self.led.led.brightness.merge( + self.led.led.color.constrain_subset(F.LED.Color.YELLOW) + self.led.led.brightness.constrain_subset( TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value ) @@ -133,7 +133,6 @@ def main(): resolve_dynamic_parameters(G) # picking ---------------------------------------------------------------- - replace_tbd_with_any(app, recursive=True) modules = app.get_children_modules(types=Module) CachePicker.add_to_modules(modules, prio=-20) try: diff --git a/examples/pcb_layout.py b/examples/pcb_layout.py index b75ce9cb..b56ba4da 100644 --- a/examples/pcb_layout.py +++ b/examples/pcb_layout.py @@ -22,6 +22,7 @@ from faebryk.exporters.pcb.layout.typehierarchy import LayoutTypeHierarchy from faebryk.libs.brightness import TypicalLuminousIntensity from faebryk.libs.examples.buildutil import apply_design_to_pcb +from faebryk.libs.library import L from faebryk.libs.logging import setup_basic_logging from faebryk.libs.units import P @@ -37,23 +38,25 @@ def __preinit__(self) -> None: self.leds.power.connect(self.battery.power) # Parametrize - self.leds.led.color.merge(F.LED.Color.YELLOW) - self.leds.led.brightness.merge( + self.leds.led.color.constrain_subset(F.LED.Color.YELLOW) + self.leds.led.brightness.constrain_subset( TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value ) - self.eeprom.power.voltage.merge(3.3 * P.V) + self.eeprom.power.voltage.constrain_subset( + L.Range.from_center_rel(3.3 * P.V, 0.05) + ) self.eeprom.set_address(0x0) # Layout Point = F.has_pcb_position.Point - L = F.has_pcb_position.layer_type + Ly = F.has_pcb_position.layer_type layout = LayoutTypeHierarchy( layouts=[ LayoutTypeHierarchy.Level( mod_type=F.PoweredLED, - layout=LayoutAbsolute(Point((0, 0, 0, L.TOP_LAYER))), + layout=LayoutAbsolute(Point((0, 0, 0, Ly.TOP_LAYER))), children_layout=LayoutTypeHierarchy( layouts=[ LayoutTypeHierarchy.Level( @@ -65,16 +68,16 @@ def __preinit__(self) -> None: ), LayoutTypeHierarchy.Level( mod_type=F.Battery, - layout=LayoutAbsolute(Point((0, 20, 0, L.BOTTOM_LAYER))), + layout=LayoutAbsolute(Point((0, 20, 0, Ly.BOTTOM_LAYER))), ), LayoutTypeHierarchy.Level( mod_type=F.M24C08_FMN6TP, - layout=LayoutAbsolute(Point((15, 10, 0, L.TOP_LAYER))), + layout=LayoutAbsolute(Point((15, 10, 0, Ly.TOP_LAYER))), ), ] ) self.add(F.has_pcb_layout_defined(layout)) - self.add(F.has_pcb_position_defined(Point((50, 50, 0, L.NONE)))) + self.add(F.has_pcb_position_defined(Point((50, 50, 0, Ly.NONE)))) LayoutHeuristicElectricalClosenessDecouplingCaps.add_to_all_suitable_modules( self diff --git a/examples/route.py b/examples/route.py index b53b3ab9..410ad0b8 100644 --- a/examples/route.py +++ b/examples/route.py @@ -33,7 +33,9 @@ def __init__(self, extrude_y: float): def __preinit__(self): for resistor in self.resistors: - resistor.resistance.merge(F.Range.from_center_rel(1000 * P.ohm, 0.05)) + resistor.resistance.constrain_subset( + L.Range.from_center_rel(1000 * P.ohm, 0.05) + ) resistor.unnamed[0].connect(self.unnamed[0]) resistor.unnamed[1].connect(self.unnamed[1]) diff --git a/examples/signal_processing.py b/examples/signal_processing.py index 5ebab92a..4dc603db 100644 --- a/examples/signal_processing.py +++ b/examples/signal_processing.py @@ -12,6 +12,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.examples.buildutil import apply_design_to_pcb +from faebryk.libs.library import L from faebryk.libs.logging import setup_basic_logging from faebryk.libs.units import P @@ -25,8 +26,10 @@ def __preinit__(self) -> None: # TODO actually do something with the filter # Parametrize - self.lowpass.cutoff_frequency.merge(200 * P.Hz) - self.lowpass.response.merge(F.Filter.Response.LOWPASS) + self.lowpass.cutoff_frequency.constrain_subset( + L.Range.from_center_rel(200 * P.Hz, 0.05) + ) + self.lowpass.response.constrain_subset(F.Filter.Response.LOWPASS) # Specialize special = self.lowpass.specialize(F.FilterElectricalLC()) @@ -34,11 +37,16 @@ def __preinit__(self) -> None: # set reference voltage # TODO: this will be automatically set by the power supply # once this example is more complete - special.in_.reference.voltage.merge(3 * P.V) - special.out.reference.voltage.merge(3 * P.V) - + special.in_.reference.voltage.constrain_subset( + L.Range.from_center_rel(3 * P.V, 0.05) + ) + special.out.reference.voltage.constrain_subset( + L.Range.from_center_rel(3 * P.V, 0.05) + ) + + # TODO # Construct - special.get_trait(F.has_construction_dependency).construct() + # special.get_trait(F.has_construction_dependency).construct() def main(): diff --git a/src/faebryk/core/defaultsolver.py b/src/faebryk/core/defaultsolver.py new file mode 100644 index 00000000..b32d2901 --- /dev/null +++ b/src/faebryk/core/defaultsolver.py @@ -0,0 +1,979 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import logging +from collections import defaultdict +from collections.abc import Iterable +from statistics import median +from typing import Any, cast + +from more_itertools import partition + +from faebryk.core.graphinterface import Graph, GraphInterfaceSelf +from faebryk.core.parameter import ( + Add, + Arithmetic, + Constrainable, + Divide, + Expression, + Is, + IsSubset, + Log, + Multiply, + Parameter, + ParameterOperatable, + Power, + Predicate, + Sqrt, + Subtract, +) +from faebryk.core.solver import Solver +from faebryk.libs.sets import Range, Ranges +from faebryk.libs.units import Quantity, dimensionless +from faebryk.libs.util import EquivalenceClasses, unique + +logger = logging.getLogger(__name__) + + +def debug_print(repr_map: dict[ParameterOperatable, ParameterOperatable]): + import sys + + if getattr(sys, "gettrace", lambda: None)(): + log = print + else: + log = logger.info + for s, d in repr_map.items(): + if isinstance(d, Expression): + if isinstance(s, Expression): + log(f"{s}[{s.operands}] -> {d}[{d.operands} | G {d.get_graph()!r}]") + else: + log(f"{s} -> {d}[{d.operands} | G {d.get_graph()!r}]") + else: + log(f"{s} -> {d} | G {d.get_graph()!r}") + graphs = unique(map(lambda p: p.get_graph(), repr_map.values()), lambda g: g()) + log(f"{len(graphs)} graphs") + + +def parameter_ops_alias_classes( + G: Graph, +) -> dict[ParameterOperatable, set[ParameterOperatable]]: + # TODO just get passed + param_ops = { + p + for p in G.nodes_of_type(ParameterOperatable) + if get_constrained_predicates_involved_in(p) + }.difference(G.nodes_of_type(Predicate)) + full_eq = EquivalenceClasses[ParameterOperatable](param_ops) + + is_exprs = [e for e in G.nodes_of_type(Is) if e.constrained] + + for is_expr in is_exprs: + full_eq.add_eq(*is_expr.operands) + + obvious_eq = defaultdict(list) + for p in param_ops: + obvious_eq[p.obviously_eq_hash()].append(p) + logger.info(f"obvious eq: {obvious_eq}") + + for candidates in obvious_eq.values(): + if len(candidates) > 1: + logger.debug(f"#obvious eq candidates: {len(candidates)}") + for i, p in enumerate(candidates): + for q in candidates[:i]: + if p.obviously_eq(q): + full_eq.add_eq(p, q) + break + return full_eq.classes + + +def get_params_for_expr(expr: Expression) -> set[Parameter]: + param_ops = {op for op in expr.operatable_operands if isinstance(op, Parameter)} + expr_ops = {op for op in expr.operatable_operands if isinstance(op, Expression)} + + return param_ops | {op for e in expr_ops for op in get_params_for_expr(e)} + + +def get_constrained_predicates_involved_in( + p: ParameterOperatable, +) -> set[Predicate]: + # p.self -> p.operated_on -> e1.operates_on -> e1.self + dependants = p.bfs_node( + lambda path, _: isinstance(path[-1].node, ParameterOperatable) + and ( + # self + isinstance(path[-1], GraphInterfaceSelf) + # operated on + or path[-1].node.operated_on is path[-1] + # operated on -> operates on + or ( + len(path) >= 2 + and isinstance(path[-2].node, ParameterOperatable) + and path[-2].node.operated_on is path[-2] + and isinstance(path[-1].node, Expression) + and path[-1].node.operates_on is path[-1] + ) + ) + ) + res = {p for p in dependants if isinstance(p, Predicate) and p.constrained} + return res + + +def parameter_dependency_classes(G: Graph) -> list[set[Parameter]]: + # TODO just get passed + params = [ + p + for p in G.nodes_of_type(Parameter) + if get_constrained_predicates_involved_in(p) + ] + + related = EquivalenceClasses[Parameter](params) + + eq_exprs = [e for e in G.nodes_of_type(Predicate) if e.constrained] + + for eq_expr in eq_exprs: + params = get_params_for_expr(eq_expr) + related.add_eq(*params) + + return related.get() + + +# TODO make part of Expression class +def create_new_expr( + old_expr: Expression, *operands: ParameterOperatable.All +) -> Expression: + new_expr = type(old_expr)(*operands) + for op in operands: + if isinstance(op, ParameterOperatable): + assert op.get_graph() == new_expr.get_graph() + if isinstance(old_expr, Constrainable): + cast(Constrainable, new_expr).constrained = old_expr.constrained + return new_expr + + +def copy_param(p: Parameter) -> Parameter: + return Parameter( + units=p.units, + within=p.within, + domain=p.domain, + soft_set=p.soft_set, + guess=p.guess, + tolerance_guess=p.tolerance_guess, + likely_constrained=p.likely_constrained, + ) + + +def copy_operand_recursively( + o: ParameterOperatable.All, repr_map: dict[ParameterOperatable, ParameterOperatable] +) -> ParameterOperatable.All: + if o in repr_map: + return repr_map[o] + if isinstance(o, Expression): + new_ops = [] + for op in o.operands: + new_op = copy_operand_recursively(op, repr_map) + if isinstance(op, ParameterOperatable): + repr_map[op] = new_op + new_ops.append(new_op) + expr = create_new_expr(o, *new_ops) + repr_map[o] = expr + return expr + elif isinstance(o, Parameter): + param = copy_param(o) + repr_map[o] = param + return param + else: + return o + + +# units -> base units (dimensionless) +# within -> constrain is subset +# scalar to single +def normalize_graph(G: Graph) -> dict[ParameterOperatable, ParameterOperatable]: + def set_to_base_units(s: Ranges | Range | None) -> Ranges | Range | None: + if s is None: + return None + if isinstance(s, Ranges): + return Ranges._from_ranges(s._ranges, dimensionless) + return Range._from_range(s._range, dimensionless) + + def scalar_to_base_units(q: int | float | Quantity | None) -> Quantity | None: + if q is None: + return None + if isinstance(q, Quantity): + return q.to_base_units().magnitude * dimensionless + return q * dimensionless + + param_ops = G.nodes_of_type(ParameterOperatable) + + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + + for po in cast( + Iterable[ParameterOperatable], + ParameterOperatable.sort_by_depth(param_ops, ascending=True), + ): + if isinstance(po, Parameter): + new_param = Parameter( + units=dimensionless, + within=None, + domain=po.domain, + soft_set=set_to_base_units(po.soft_set), + guess=scalar_to_base_units(po.guess), + tolerance_guess=po.tolerance_guess, + likely_constrained=po.likely_constrained, + ) + repr_map[po] = new_param + if po.within is not None: + new_param.constrain_subset(set_to_base_units(po.within)) + elif isinstance(po, Expression): + new_ops = [] + for op in po.operands: + if isinstance(op, ParameterOperatable): + assert op in repr_map + new_ops.append(repr_map[op]) + elif isinstance(op, int | float | Quantity): + new_ops.append(scalar_to_base_units(op)) + else: + new_ops.append(set_to_base_units(op)) + repr_map[po] = create_new_expr(po, *new_ops) + + return repr_map + + +def resolve_alias_classes( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + dirty = False + params_ops = [ + p + for p in G.nodes_of_type(ParameterOperatable) + if get_constrained_predicates_involved_in(p) + ] + exprs = G.nodes_of_type(Expression) + predicates = {e for e in exprs if isinstance(e, Predicate)} + exprs.difference_update(predicates) + exprs = {e for e in exprs if get_constrained_predicates_involved_in(e)} + + p_alias_classes = parameter_ops_alias_classes(G) + dependency_classes = parameter_dependency_classes(G) + + infostr = ( + f"{len(params_ops)} parametersoperable" + f"\n {len(p_alias_classes)} alias classes" + f"\n {len(dependency_classes)} dependency classes" + "\n" + ) + logger.info(infostr) + + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + + # Make new param repre for alias classes + for param_op in ParameterOperatable.sort_by_depth(params_ops, ascending=True): + if param_op in repr_map or param_op not in p_alias_classes: + continue + + alias_class = p_alias_classes[param_op] + + # TODO short-cut if len() == 1 ? + param_alias_class = [p for p in alias_class if isinstance(p, Parameter)] + expr_alias_class = [p for p in alias_class if isinstance(p, Expression)] + + # TODO non unit/numeric params, i.e. enums, bools + # single unit + unit_candidates = {p.units for p in alias_class} + if len(unit_candidates) > 1: + raise ValueError("Incompatible units in alias class") + if len(param_alias_class) > 0: + dirty |= len(param_alias_class) > 1 + + # single domain + domain_candidates = {p.domain for p in param_alias_class} + if len(domain_candidates) > 1: + raise ValueError("Incompatible domains in alias class") + + # intersect ranges + within_ranges = { + p.within for p in param_alias_class if p.within is not None + } + within = None + if within_ranges: + within = Ranges.op_intersect_ranges(*within_ranges) + + # heuristic: + # intersect soft sets + soft_sets = { + p.soft_set for p in param_alias_class if p.soft_set is not None + } + soft_set = None + if soft_sets: + soft_set = Ranges.op_intersect_ranges(*soft_sets) + + # heuristic: + # get median + guesses = {p.guess for p in param_alias_class if p.guess is not None} + guess = None + if guesses: + guess = median(guesses) # type: ignore + + # heuristic: + # max tolerance guess + tolerance_guesses = { + p.tolerance_guess + for p in param_alias_class + if p.tolerance_guess is not None + } + tolerance_guess = None + if tolerance_guesses: + tolerance_guess = max(tolerance_guesses) + + likely_constrained = any(p.likely_constrained for p in param_alias_class) + + representative = Parameter( + units=unit_candidates.pop(), + within=within, + soft_set=soft_set, + guess=guess, + tolerance_guess=tolerance_guess, + likely_constrained=likely_constrained, + ) + repr_map.update({p: representative for p in param_alias_class}) + elif len(expr_alias_class) > 1: + dirty = True + representative = Parameter(units=unit_candidates.pop()) + + if len(expr_alias_class) > 0: + for e in expr_alias_class: + copy_expr = copy_operand_recursively(e, repr_map) + repr_map[e] = ( + representative # copy_expr TODO make sure this makes sense + ) + # TODO, if it doesn't have implicit constraints and it's operands don't + # aren't constraint, we can get rid of it + assert isinstance(copy_expr, Constrainable) + copy_expr.alias_is(representative) + + # replace parameters in expressions and predicates + for expr in cast( + Iterable[Expression], + ParameterOperatable.sort_by_depth(exprs | predicates, ascending=True), + ): + + def try_replace(o: ParameterOperatable.All): + if not isinstance(o, ParameterOperatable): + return o + if o in repr_map: + return repr_map[o] + raise Exception() + + # filter alias class Is + if isinstance(expr, Is): + continue + + assert all( + o in repr_map or not isinstance(o, ParameterOperatable) + for o in expr.operands + ) + repr_map[expr] = copy_operand_recursively(expr, repr_map) + + return repr_map, dirty + + +def subset_of_literal( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + dirty = False + params = G.nodes_of_type(Parameter) + removed = set() + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + + for param in params: + + def other_set(e: Is) -> ParameterOperatable.All: + if e.operands[0] is param: + return e.operands[1] + return e.operands[0] + + is_subsets = [ + e + for e in param.get_operations() + if isinstance(e, IsSubset) + and len(e.get_operations()) == 0 + and not isinstance(other_set(e), ParameterOperatable) + ] + if len(is_subsets) > 1: + other_sets = [other_set(e) for e in is_subsets] + intersected = other_sets[0] + for s in other_sets[1:]: + intersected = intersected.op_intersect_ranges(Ranges(s)) + removed.update(is_subsets) + new_param = copy_param(param) + new_param.constrain_subset(intersected) + repr_map[param] = new_param + dirty = True + else: + repr_map[param] = copy_param(param) + + exprs = ( + ParameterOperatable.sort_by_depth( # TODO, do we need the sort here? same above + ( + p + for p in G.nodes_of_type(Expression) + if p not in repr_map and p not in removed + ), + ascending=True, + ) + ) + for expr in exprs: + copy_operand_recursively(expr, repr_map) + + return repr_map, dirty + + +def is_replacable( + repr_map: dict[ParameterOperatable, ParameterOperatable], + e: Expression, + parent_expr: Expression, +) -> bool: + if e in repr_map: # overly restrictive: equivalent replacement would be ok + return False + if e.get_operations() != {parent_expr}: + return False + return True + + +def compress_associative_add_mul( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + dirty = False + add_muls = cast(set[Add | Multiply], G.nodes_of_types((Add, Multiply))) + # get out deepest expr in compressable tree + parent_add_muls = { + e for e in add_muls if type(e) not in {type(n) for n in e.get_operations()} + } + + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + removed = set() + + # (A + B) + C + # X -> Y + # compress(Y) + # compress(X) -> [A, B] + # -> [A, B, C] + + def flatten_operands_of_ops_with_same_type[T: Add | Multiply]( + e: T, + ) -> tuple[list[T], bool]: + dirty = False + operands = e.operands + noncomp, compressible = partition( + lambda o: type(o) is type(e) and is_replacable(repr_map, o, e), operands + ) + out = [] + for c in compressible: + dirty = True + removed.add(c) + sub_out, sub_dirty = flatten_operands_of_ops_with_same_type(c) + dirty |= sub_dirty + out += sub_out + if len(out) > 0: + logger.info(f"FLATTENED {type(e).__name__} {e} -> {out}") + return out + list(noncomp), dirty + + for expr in cast( + Iterable[Add | Multiply], + ParameterOperatable.sort_by_depth(parent_add_muls, ascending=True), + ): + operands, sub_dirty = flatten_operands_of_ops_with_same_type(expr) + if sub_dirty: + dirty = True + copy_operands = [copy_operand_recursively(o, repr_map) for o in operands] + + new_expr = create_new_expr( + expr, + *copy_operands, + ) + repr_map[expr] = new_expr + + # copy other param ops + other_param_op = ParameterOperatable.sort_by_depth( + ( + p + for p in G.nodes_of_type(ParameterOperatable) + if p not in repr_map and p not in removed + ), + ascending=True, + ) + for o in other_param_op: + copy_operand_recursively(o, repr_map) + + return repr_map, dirty + + +def compress_associative_sub( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + logger.info("Compressing Subtracts") + dirty = False + subs = cast(set[Subtract], G.nodes_of_type(Subtract)) + # get out deepest expr in compressable tree + parent_subs = { + e for e in subs if type(e) not in {type(n) for n in e.get_operations()} + } + + removed = set() + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + + def flatten_sub( + e: Subtract, + ) -> tuple[ + ParameterOperatable.All, + list[ParameterOperatable.All], + list[ParameterOperatable.All], + bool, + ]: + const_subtrahend = ( + [] if isinstance(e.operands[1], ParameterOperatable) else [e.operands[1]] + ) + nonconst_subtrahend = [] if const_subtrahend else [e.operands[1]] + if isinstance(e.operands[0], Subtract) and is_replacable( + repr_map, e.operands[0], e + ): + removed.add(e.operands[0]) + minuend, const_subtrahends, nonconst_subtrahends, _ = flatten_sub( + e.operands[0] + ) + return ( + minuend, + const_subtrahends + const_subtrahend, + nonconst_subtrahends + nonconst_subtrahend, + True, + ) + else: + return e.operands[0], const_subtrahend, nonconst_subtrahend, False + + for expr in cast( + Iterable[Subtract], + ParameterOperatable.sort_by_depth(parent_subs, ascending=True), + ): + minuend, const_subtrahends, nonconst_subtrahends, sub_dirty = flatten_sub(expr) + if ( + isinstance(minuend, Add) + and is_replacable(repr_map, minuend, expr) + and len(const_subtrahends) > 0 + ): + copy_minuend = Add( + *(copy_operand_recursively(s, repr_map) for s in minuend.operands), + *(-1 * c for c in const_subtrahends), + ) + repr_map[expr] = copy_minuend + const_subtrahends = [] + sub_dirty = True + elif sub_dirty: + copy_minuend = copy_operand_recursively(minuend, repr_map) + if sub_dirty: + dirty = True + copy_subtrahends = [ + copy_operand_recursively(s, repr_map) + for s in nonconst_subtrahends + const_subtrahends + ] + if len(copy_subtrahends) > 0: + new_expr = Subtract( + copy_minuend, + Add(*copy_subtrahends), + ) + else: + new_expr = copy_minuend + removed.add(expr) + repr_map[expr] = new_expr + logger.info(f"REPRMAP {expr} -> {new_expr}") + + # copy other param ops + other_param_op = ParameterOperatable.sort_by_depth( + ( + p + for p in G.nodes_of_type(ParameterOperatable) + if p not in repr_map and p not in removed + ), + ascending=True, + ) + for o in other_param_op: + copy_o = copy_operand_recursively(o, repr_map) + logger.info(f"REMAINING {o} -> {copy_o}") + repr_map[o] = copy_o + + return repr_map, dirty + + +def compress_arithmetic_expressions( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + dirty = False + arith_exprs = cast(set[Arithmetic], G.nodes_of_type(Arithmetic)) + + repr_map: dict[ParameterOperatable, ParameterOperatable] = {} + removed = set() + + for expr in cast( + Iterable[Arithmetic], + ParameterOperatable.sort_by_depth(arith_exprs, ascending=True), + ): + if expr in repr_map or expr in removed: + continue + + operands = expr.operands + const_ops, nonconst_ops = partition( + lambda o: isinstance(o, ParameterOperatable), operands + ) + non_replacable_nonconst_ops, replacable_nonconst_ops = partition( + lambda o: o not in repr_map, nonconst_ops + ) + multiplicity = {} + for n in replacable_nonconst_ops: + if n in multiplicity: + multiplicity[n] += 1 + else: + multiplicity[n] = 1 + + if isinstance(expr, Add): + try: + const_sum = [next(const_ops)] + for c in const_ops: + dirty = True + const_sum[0] += c + if const_sum[0] == 0 * expr.units: # TODO make work with all the types + dirty = True + const_sum = [] + except StopIteration: + const_sum = [] + if any(m > 1 for m in multiplicity.values()): + dirty = True + if dirty: + copied = { + n: copy_operand_recursively(n, repr_map) for n in multiplicity + } + nonconst_prod = [ + Multiply(copied[n], m) if m > 1 else copied[n] + for n, m in multiplicity.items() + ] + new_operands = [ + *nonconst_prod, + *const_sum, + *( + copy_operand_recursively(o, repr_map) + for o in non_replacable_nonconst_ops + ), + ] + if len(new_operands) > 1: + new_expr = Add(*new_operands) + elif len(new_operands) == 1: + new_expr = new_operands[0] + removed.add(expr) + else: + raise ValueError("No operands, should not happen") + repr_map[expr] = new_expr + + elif isinstance(expr, Multiply): + try: + const_prod = [next(const_ops)] + for c in const_ops: + dirty = True + const_prod[0] *= c + if ( + const_prod[0] == 1 * dimensionless + ): # TODO make work with all the types + dirty = True + const_prod = [] + except StopIteration: + const_prod = [] + if ( + len(const_prod) == 1 and const_prod[0].magnitude == 0 + ): # TODO make work with all the types + dirty = True + repr_map[expr] = 0 * expr.units + else: + if any(m > 1 for m in multiplicity.values()): + dirty = True + if dirty: + copied = { + n: copy_operand_recursively(n, repr_map) for n in multiplicity + } + nonconst_power = [ + Power(copied[n], m) if m > 1 else copied[n] + for n, m in multiplicity.items() + ] + new_operands = [ + *nonconst_power, + *const_prod, + *( + copy_operand_recursively(o, repr_map) + for o in non_replacable_nonconst_ops + ), + ] + if len(new_operands) > 1: + new_expr = Multiply(*new_operands) + elif len(new_operands) == 1: + new_expr = new_operands[0] + removed.add(expr) + else: + raise ValueError("No operands, should not happen") + repr_map[expr] = new_expr + elif isinstance(expr, Subtract): + if sum(1 for _ in const_ops) == 2: + dirty = True + repr_map[expr] = expr.operands[0] - expr.operands[1] + removed.add(expr) + elif expr.operands[0] is expr.operands[1]: + dirty = True + repr_map[expr] = 0 * expr.units + removed.add(expr) + elif expr.operands[1] == 0 * expr.operands[1].units: + dirty = True + repr_map[expr.operands[0]] = repr_map.get( + expr.operands[0], + copy_operand_recursively(expr.operands[0], repr_map), + ) + repr_map[expr] = repr_map[expr.operands[0]] + removed.add(expr) + else: + repr_map[expr] = copy_operand_recursively(expr, repr_map) + elif isinstance(expr, Divide): + if sum(1 for _ in const_ops) == 2: + if not expr.operands[1].magnitude == 0: + dirty = True + repr_map[expr] = expr.operands[0] / expr.operands[1] + removed.add(expr) + else: + # no valid solution but might not matter e.g. [phi(a,b,...) + # OR a/0 == b] + repr_map[expr] = copy_operand_recursively(expr, repr_map) + elif expr.operands[1] is expr.operands[0]: + dirty = True + repr_map[expr] = 1 * dimensionless + removed.add(expr) + elif expr.operands[1] == 1 * expr.operands[1].units: + dirty = True + repr_map[expr.operands[0]] = repr_map.get( + expr.operands[0], + copy_operand_recursively(expr.operands[0], repr_map), + ) + repr_map[expr] = repr_map[expr.operands[0]] + removed.add(expr) + else: + repr_map[expr] = copy_operand_recursively(expr, repr_map) + else: + repr_map[expr] = copy_operand_recursively(expr, repr_map) + + other_param_op = ( + ParameterOperatable.sort_by_depth( # TODO, do we need the sort here? same above + ( + p + for p in G.nodes_of_type(ParameterOperatable) + if p not in repr_map and p not in removed + ), + ascending=True, + ) + ) + for o in other_param_op: + copy_operand_recursively(o, repr_map) + + return { + k: v for k, v in repr_map.items() if isinstance(v, ParameterOperatable) + }, dirty + + +# TODO move to expression? +# TODO recursive? +def has_implicit_constraint(po: ParameterOperatable) -> bool: + if isinstance(po, Parameter | Add | Subtract | Multiply | Power): # TODO others + return False + if isinstance(po, Divide): + return True # implicit constraint: divisor not zero + if isinstance(po, Sqrt | Log): + return True # implicit constraint: non-negative + return True + + +def remove_obvious_tautologies( + G: Graph, +) -> tuple[dict[ParameterOperatable, ParameterOperatable], bool]: + removed = set() + dirty = False + for pred_is in ParameterOperatable.sort_by_depth( + G.nodes_of_type(Is), ascending=True + ): + + def known_unconstrained(po: ParameterOperatable) -> bool: + no_other_constraints = ( + len(get_constrained_predicates_involved_in(po).difference({pred_is})) + == 0 + ) + return no_other_constraints and not has_implicit_constraint(po) + + pred_is = cast(Is, pred_is) + if pred_is.operands[0] is pred_is.operands[1] and not known_unconstrained( + pred_is.operands[0] + ): + removed.add(pred_is) + dirty = True + elif known_unconstrained(pred_is.operands[0]) or known_unconstrained( + pred_is.operands[1] + ): + removed.add(pred_is) + dirty = True + repr_map = {} + for p in G.nodes_of_type(ParameterOperatable): + if p not in removed: + repr_map[p] = copy_operand_recursively(p, repr_map) + return repr_map, dirty + + +class DefaultSolver(Solver): + timeout: int = 1000 + + def phase_one_no_guess_solving(self, g: Graph) -> None: + logger.info(f"Phase 1 Solving: No guesses {'-' * 80}") + + # strategies + # https://miro.com/app/board/uXjVLV3O2BQ=/ + # compress expressions inside alias classes + # x / y => x / x + # associativity + # (x + a) + b => x + a + b [for +,*] + # compress expressions that are using literals + # x + 1 + 5 => x + 6 + # x + 0 => x + # x * 1 => x + # x * 0 => 0 + # x / 1 => x + # compress calculatable expressions + # x / x => 1 + # x + x => 2*x + # x - x => 0 + # x * x => x^2 + # k*x + l*x => (k+l)*x + # sqrt(x^2) => abs(x) + # sqrt(x) * sqrt(x) => x + + # as long as progress iterate + + logger.info("Phase 0 Solving: normalize graph") + repr_map = normalize_graph(g) + debug_print(repr_map) + graphs = unique(map(lambda p: p.get_graph(), repr_map.values()), lambda g: g()) + # TODO assert all new graphs + + dirty = True + iter = 0 + + while dirty and len(graphs) > 0: + iter += 1 + logger.info(f"Iteration {iter}") + logger.info("Phase 1 Solving: Alias classes") + repr_map = {} + for g in graphs: + alias_repr_map, alias_dirty = resolve_alias_classes(g) + repr_map.update(alias_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 2a Solving: Add/Mul associative expressions") + repr_map = {} + for g in graphs: + assoc_add_mul_repr_map, assoc_add_mul_dirty = ( + compress_associative_add_mul(g) + ) + repr_map.update(assoc_add_mul_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 2a Solving: Add/Mul associative expressions") + repr_map = {} + for g in graphs: + assoc_add_mul_repr_map, assoc_add_mul_dirty = ( + compress_associative_add_mul(g) + ) + repr_map.update(assoc_add_mul_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 2b Solving: Subtract associative expressions") + repr_map = {} + for g in graphs: + assoc_sub_repr_map, assoc_sub_dirty = compress_associative_sub(g) + repr_map.update(assoc_sub_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 3 Solving: Arithmetic expressions") + repr_map = {} + for g in graphs: + arith_repr_map, arith_dirty = compress_arithmetic_expressions(g) + repr_map.update(arith_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 4 Solving: Remove obvious tautologies") + repr_map = {} + for g in graphs: + tautology_repr_map, tautology_dirty = remove_obvious_tautologies(g) + repr_map.update(tautology_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + logger.info("Phase 5 Solving: Subset of literals") + repr_map = {} + for g in graphs: + subset_repr_map, subset_dirty = subset_of_literal(g) + repr_map.update(subset_repr_map) + debug_print(repr_map) + graphs = unique( + map(lambda p: p.get_graph(), repr_map.values()), lambda g: g() + ) + # TODO assert all new graphs + + dirty = ( + alias_dirty + or assoc_add_mul_dirty + or assoc_sub_dirty + or arith_dirty + or tautology_dirty + or subset_dirty + ) + + def get_any_single( + self, + operatable: ParameterOperatable, + lock: bool, + suppose_constraint: Predicate | None = None, + minimize: Expression | None = None, + ) -> Any: + raise NotImplementedError() + + def assert_any_predicate[ArgType]( + self, + predicates: list["Solver.PredicateWithInfo[ArgType]"], + lock: bool, + suppose_constraint: Predicate | None = None, + minimize: Expression | None = None, + ) -> Solver.SolveResultAny[ArgType]: + raise NotImplementedError() + + def find_and_lock_solution(self, G: Graph) -> Solver.SolveResultAll: + raise NotImplementedError() diff --git a/src/faebryk/core/module.py b/src/faebryk/core/module.py index bf1287e0..cef392ff 100644 --- a/src/faebryk/core/module.py +++ b/src/faebryk/core/module.py @@ -102,7 +102,7 @@ def get_node_prop_matrix[N: Node](sub_type: type[N]): continue if dst is None: raise Exception(f"Special module misses parameter: {src.get_name()}") - dst.merge(src) + dst.alias_is(src) # TODO this cant work # for t in self.traits: diff --git a/src/faebryk/core/node.py b/src/faebryk/core/node.py index a48882a8..c66317af 100644 --- a/src/faebryk/core/node.py +++ b/src/faebryk/core/node.py @@ -559,7 +559,7 @@ def pretty_params(self) -> str: from faebryk.core.parameter import Parameter params = { - not_none(p.get_parent())[1]: p.get_most_narrow() + not_none(p.get_parent())[1]: p for p in self.get_children(direct_only=True, types=Parameter) } params_str = "\n".join(f"{k}: {v}" for k, v in params.items()) diff --git a/src/faebryk/core/parameter.py b/src/faebryk/core/parameter.py index 7f610202..f43c42ef 100644 --- a/src/faebryk/core/parameter.py +++ b/src/faebryk/core/parameter.py @@ -1,508 +1,895 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import logging -from typing import ( - Any, - Callable, - Concatenate, - Optional, - Sequence, -) -from typing_extensions import Self +import logging +from collections.abc import Iterable +from enum import Enum, auto +from types import NotImplementedType +from typing import Any, Callable, Self, override +from faebryk.core.core import Namespace from faebryk.core.graphinterface import GraphInterface -from faebryk.core.node import Node +from faebryk.core.node import Node, f_field from faebryk.core.trait import Trait -from faebryk.libs.units import Quantity, UnitsContainer -from faebryk.libs.util import ( - Tree, - TwistArgs, - cast_assert, - is_type_pair, - try_avoid_endless_recursion, -) +from faebryk.libs.sets import P_Set, Range, Ranges +from faebryk.libs.units import HasUnit, Quantity, Unit, dimensionless +from faebryk.libs.util import abstract, cast_assert logger = logging.getLogger(__name__) -def _resolved[PV, O]( - func: Callable[["Parameter", "Parameter"], O], -) -> Callable[ - [ - "PV | set | tuple[PV, PV] | Parameter", - "PV | set | tuple[PV, PV] | Parameter", - ], - O, -]: - def wrap(*args): - args = [Parameter.from_literal(arg).get_most_narrow() for arg in args] - return func(*args) - - return wrap - - -def _resolved_self[PV, O, **P]( - func: Callable[Concatenate["Parameter", P], O], -) -> Callable[Concatenate["PV | set | tuple[PV, PV] | Parameter", P], O]: - def wrap( - p: "PV | set | tuple[PV, PV] | Parameter", - *args: P.args, - **kwargs: P.kwargs, - ): - return func(Parameter.from_literal(p).get_most_narrow(), *args, **kwargs) +# When we make this generic, two types, type T of elements, and type S of known subsets +# boolean: T == S == bool +# enum: T == S == Enum +# number: T == Number type, S == Range[Number] +class ParameterOperatable(Node): + type QuantityLike = Quantity | Unit | NotImplementedType + type Number = int | float | QuantityLike - return wrap + type NonParamNumber = Number | P_Set[Number] + type NumberLike = ParameterOperatable | NonParamNumber + type NonParamBoolean = bool | P_Set[bool] + type BooleanLike = ParameterOperatable | NonParamBoolean + type NonParamEnum = Enum | P_Set[Enum] + type EnumLike = ParameterOperatable | NonParamEnum + type All = NumberLike | BooleanLike | EnumLike + type NonParamSet = NonParamNumber | NonParamBoolean | NonParamEnum + type Sets = All -class Parameter(Node): - type PV = Any - type LIT = PV | set | tuple[PV, PV] - type LIT_OR_PARAM = LIT | "Parameter" + operated_on: GraphInterface - class TraitT(Trait): ... + def get_operations(self) -> set["Expression"]: + res = self.operated_on.get_connected_nodes(types=Expression) + return res + + @staticmethod + def sort_by_depth( + exprs: Iterable["ParameterOperatable"], ascending: bool + ) -> list["ParameterOperatable"]: + def key(e: ParameterOperatable): + if isinstance(e, Expression): + return e.depth() + return 0 + + return sorted(exprs, key=key, reverse=not ascending) + + def _is_constrains(self) -> list["Is"]: + return [ + i for i in self.operated_on.get_connected_nodes(types=Is) if i.constrained + ] + + def obviously_eq(self, other: "ParameterOperatable.All") -> bool: + if self == other: + return True + if other in self._is_constrains(): + return True + return False + + def obviously_eq_hash(self) -> int: + if hasattr(self, "__hash"): + return self.__hash - narrowed_by: GraphInterface - narrows: GraphInterface + ises = [i for i in self._is_constrains() if not isinstance(i, Expression)] - class MergeException(Exception): ... + def keyfn(i: Is): + if isinstance(i, Parameter): + return 1 << 63 + return hash(i) % (1 << 63) - class SupportsSetOps: - def __contains__(self, other: "Parameter.LIT_OR_PARAM") -> bool: ... + sorted_ises = sorted(ises, key=keyfn) + if len(sorted_ises) > 0: + self.__hash = hash(sorted_ises[0]) + else: + self.__hash = id(self) + return self.__hash - @staticmethod - def check(other: "Parameter.LIT_OR_PARAM") -> bool: - return hasattr(other, "__contains__") + def operation_add(self, other: NumberLike): + return Add(self, other) - class is_dynamic(TraitT): - def execute(self) -> None: ... + def operation_subtract(self: NumberLike, other: NumberLike): + return Subtract(minuend=self, subtrahend=other) - def try_compress(self) -> "Parameter": - return self + def operation_multiply(self, other: NumberLike): + return Multiply(self, other) - @classmethod - def from_literal(cls, value: LIT_OR_PARAM) -> '"Parameter"': - from faebryk.library.Constant import Constant - from faebryk.library.Range import Range - from faebryk.library.Set import Set + def operation_divide(self: NumberLike, other: NumberLike): + return Divide(numerator=self, denominator=other) - if isinstance(value, Parameter): - return value - elif isinstance(value, set): - return Set(value) - elif isinstance(value, tuple): - return Range(*value) - else: - return Constant(value) - - def _merge(self, other: "Parameter") -> "Parameter": - from faebryk.library.ANY import ANY - from faebryk.library.Operation import Operation - from faebryk.library.Set import Set - from faebryk.library.TBD import TBD - - def _is_pair[T, U](type1: type[T], type2: type[U]) -> Optional[tuple[T, U]]: - return is_type_pair(self, other, type1, type2) - - if self is other: - return self - - try: - if self == other: - return self - except ValueError: - ... - - if pair := _is_pair(Parameter, TBD): - return pair[0] - - if pair := _is_pair(Parameter, ANY): - return pair[0] - - # TODO remove as soon as possible - if pair := _is_pair(Parameter, Operation): - # TODO make MergeOperation that inherits from Operation - # and return that instead, application can check if result is MergeOperation - # if it was checking mergeability - raise self.MergeException("cant merge range with operation") - - if any(Parameter.SupportsSetOps.check(x) for x in (self, other)): - pair = (self, other) - # if pair := _is_pair(Parameter, Parameter.SupportsSetOps): - out = self.intersect(*pair) - if isinstance(out, Operation): - raise self.MergeException("not resolvable") - if out == Set([]) and not pair[0] == pair[1] == Set([]): - raise self.MergeException( - f"conflicting sets/ranges: {self!r} {other!r}" - ) - return out - - raise NotImplementedError - - def _narrowed(self, other: "Parameter"): - if self is other: - return - - if self.narrowed_by.is_connected_to(other.narrows): - return - self.narrowed_by.connect(other.narrows) - - @_resolved - def is_mergeable_with(self: "Parameter", other: "Parameter") -> bool: - try: - self._merge(other) + def operation_power(self, other: NumberLike): + return Power(base=self, exponent=other) + + def operation_log(self): + return Log(self) + + def operation_sqrt(self): + return Sqrt(self) + + def operation_abs(self): + return Abs(self) + + def operation_floor(self): + return Floor(self) + + def operation_ceil(self): + return Ceil(self) + + def operation_round(self): + return Round(self) + + def operation_sin(self): + return Sin(self) + + def operation_cos(self): + return Cos(self) + + def operation_union(self, other: Sets): + return Union(self, other) + + def operation_intersection(self, other: Sets): + return Intersection(self, other) + + def operation_difference(self, other: Sets): + return Difference(minuend=self, subtrahend=other) + + def operation_symmetric_difference(self, other: Sets): + return SymmetricDifference(self, other) + + def operation_and(self, other: BooleanLike): + return And(self, other) + + def operation_or(self, other: BooleanLike): + return Or(self, other) + + def operation_not(self): + return Not(self) + + def operation_xor(self, other: BooleanLike): + return Xor(left=self, right=other) + + def operation_implies(self, other: BooleanLike): + return Implies(condition=self, implication=other) + + def operation_is_le(self, other: NumberLike): + return LessOrEqual(left=self, right=other) + + def operation_is_ge(self, other: NumberLike): + return GreaterOrEqual(left=self, right=other) + + def operation_is_lt(self, other: NumberLike): + return LessThan(left=self, right=other) + + def operation_is_gt(self, other: NumberLike): + return GreaterThan(left=self, right=other) + + def operation_is_ne(self, other: NumberLike): + return NotEqual(left=self, right=other) + + def operation_is_subset(self, other: Sets): + return IsSubset(left=self, right=other) + + def operation_is_superset(self, other: Sets): + return IsSuperset(left=self, right=other) + + # TODO implement + def inspect_known_min(self: NumberLike) -> Number: + raise Exception("not implemented") + # raise NotImplementedError() + + def inspect_known_max(self: NumberLike) -> Number: + raise Exception("not implemented") + # raise NotImplementedError() + + def inspect_known_values(self: BooleanLike) -> P_Set[bool]: + raise Exception("not implemented") + # raise NotImplementedError() + + # Run by the solver on finalization + inspect_solution: Callable[[Self], None] = lambda _: None + + def inspect_add_on_solution(self, fun: Callable[[Self], None]) -> None: + current = self.inspect_solution + + def new(self2): + current(self2) + fun(self2) + + self.inspect_solution = new + + # Could be exponentially many + def inspect_known_supersets_are_few(self) -> bool: + raise Exception("not implemented") + + def inspect_get_known_supersets(self) -> Iterable[P_Set]: + raise Exception("not implemented") + + def inspect_get_known_superranges(self: NumberLike) -> Iterable[Ranges]: + raise Exception("not implemented") + + # ---------------------------------------------------------------------------------- + def __add__(self, other: NumberLike): + return self.operation_add(other) + + def __radd__(self, other: NumberLike): + return self.operation_add(other) + + def __sub__(self, other: NumberLike): + # TODO could be set difference + return self.operation_subtract(other) + + def __rsub__(self, other: NumberLike): + return type(self).operation_subtract(other, self) + + def __mul__(self, other: NumberLike): + return self.operation_multiply(other) + + def __rmul__(self, other: NumberLike): + return self.operation_multiply(other) + + def __truediv__(self, other: NumberLike): + return self.operation_divide(other) + + def __rtruediv__(self, other: NumberLike): + return type(self).operation_divide(other, self) + + def __pow__(self, other: NumberLike): + return self.operation_power(other) + + def __abs__(self): + return self.operation_abs() + + def __round__(self): + return self.operation_round() + + def __floor__(self): + return self.operation_floor() + + def __ceil__(self): + return self.operation_ceil() + + def __le__(self, other: NumberLike): + return self.operation_is_le(other) + + def __ge__(self, other: NumberLike): + return self.operation_is_ge(other) + + def __lt__(self, other: NumberLike): + return self.operation_is_lt(other) + + def __gt__(self, other: NumberLike): + return self.operation_is_gt(other) + + def __ne__(self, other: NumberLike): + return self.operation_is_ne(other) + + # bitwise and + def __and__(self, other: BooleanLike): + # TODO could be set intersection + return self.operation_and(other) + + def __rand__(self, other: BooleanLike): + return self.operation_and(other) + + def __or__(self, other: BooleanLike): + # TODO could be set union + return self.operation_or(other) + + def __ror__(self, other: BooleanLike): + return self.operation_or(other) + + def __xor__(self, other: BooleanLike): + return self.operation_xor(other) + + def __rxor__(self, other: BooleanLike): + return self.operation_xor(other) + + # ---------------------------------------------------------------------------------- + + # should be eager, in the sense that, if the outcome is known, the callable is + # called immediately, without storing an expression + # we must force a value (at the end of solving at the least) + def if_then_else( + self, + if_true: Callable[[], Any], + if_false: Callable[[], Any], + preference: bool | None = None, + ) -> None: + IfThenElse(self, if_true, if_false, preference) + + # def assert_true( + # self, error: Callable[[], None] = lambda: raise_(ValueError()) + # ) -> None: + # self.if_then_else(lambda: None, error, True) + + # def assert_false( + # self, error: Callable[[], None] = lambda: raise_(ValueError()) + # ) -> None: + # self.if_then_else(error, lambda: None, False) + + # TODO + # def switch_case( + # self, + # cases: list[tuple[?, Callable[[], Any]]], + # ) -> None: ... + + +def obviously_eq(a: ParameterOperatable.All, b: ParameterOperatable.All) -> bool: + if a == b: + return True + if isinstance(a, ParameterOperatable): + return a.obviously_eq(b) + elif isinstance(b, ParameterOperatable): + return b.obviously_eq(a) + return False + + +# TODO mixes two things, those that a constraining predicate can be called on, +# and the predicate, which can have it's constrained be set?? +class Constrainable: + type All = ParameterOperatable.All + type Sets = ParameterOperatable.Sets + type NumberLike = ParameterOperatable.NumberLike + + def __init__(self): + super().__init__() + self.constrained: bool = False + + def _constrain(self, constraint: "Predicate"): + constraint.constrain() + + def _get(self) -> ParameterOperatable: + return cast_assert(ParameterOperatable, self) + + # Generic + def alias_is(self, other: All): + return self._constrain(Is(left=self, right=other)) + + # Numberlike + def constrain_le(self, other: NumberLike): + return self._constrain(self._get().operation_is_le(other)) + + def constrain_ge(self, other: NumberLike): + return self._constrain(self._get().operation_is_ge(other)) + + def constrain_lt(self, other: NumberLike): + return self._constrain(self._get().operation_is_lt(other)) + + def constrain_gt(self, other: NumberLike): + return self._constrain(self._get().operation_is_gt(other)) + + def constrain_ne(self, other: NumberLike): + return self._constrain(self._get().operation_is_ne(other)) + + # Setlike + def constrain_subset(self, other: Sets): + return self._constrain(self._get().operation_is_subset(other)) + + def constrain_superset(self, other: Sets): + return self._constrain(self._get().operation_is_superset(other)) + + def constrain_cardinality(self, other: int): + return self._constrain(Cardinality(self._get(), other)) + + # shortcuts + def constrain(self): + self.constrained = True + + +@abstract +class Expression(ParameterOperatable): + operates_on: GraphInterface + + def __init__(self, *operands: ParameterOperatable.All): + super().__init__() + self.operands = tuple(operands) + self.operatable_operands = { + op for op in operands if isinstance(op, ParameterOperatable) + } + + def __preinit__(self): + for op in self.operatable_operands: + self.operates_on.connect(op.operated_on) + + def get_operatable_operands(self) -> set[ParameterOperatable]: + return self.operates_on.get_connected_nodes(types=ParameterOperatable) + + def depth(self) -> int: + if hasattr(self, "_depth"): + return self._depth + self._depth = 1 + max( + op.depth() if isinstance(op, Expression) else 0 for op in self.operands + ) + return self._depth + + # TODO caching + @override + def obviously_eq(self, other: ParameterOperatable.All) -> bool: + if super().obviously_eq(other): + return True + if type(self) is type(other): + for s, o in zip(self.operands, other.operands): + if not obviously_eq(s, o): + return False return True - except self.MergeException: - return False - except NotImplementedError: - return False + return False + + def obviously_eq_hash(self) -> int: + return hash((type(self), self.operands)) - @_resolved - def is_subset_of(self: "Parameter", other: "Parameter") -> bool: - from faebryk.library.ANY import ANY - from faebryk.library.Operation import Operation - from faebryk.library.TBD import TBD - lhs = self - rhs = other +# TODO are any expressions not constrainable? +# parameters are contstrainable, too, so all parameter-operatables are constrainable? +@abstract +class ConstrainableExpression(Expression, Constrainable): + def __init__(self, *operands: ParameterOperatable.All): + Expression.__init__(self, *operands) + Constrainable.__init__(self) - def is_either_instance(t: type["Parameter"]): - return isinstance(lhs, t) or isinstance(rhs, t) - # Not resolveable - if isinstance(rhs, ANY): +@abstract +class Arithmetic(ConstrainableExpression, HasUnit): + def __init__(self, *operands: ParameterOperatable.NumberLike): + super().__init__(*operands) + types = int, float, Quantity, Unit, Parameter, Arithmetic, Ranges + if any(not isinstance(op, types) for op in operands): + raise ValueError( + "operands must be int, float, Quantity, Parameter, or Expression" + ) + if any( + not isinstance(param.domain, (Numbers, ESeries)) + for param in operands + if isinstance(param, Parameter) + ): + raise ValueError("parameters must have domain Numbers or ESeries") + + +@abstract +class Additive(Arithmetic): + def __init__(self, *operands): + super().__init__(*operands) + units = [HasUnit.get_units_or_dimensionless(op) for op in operands] + self.units = units[0] + if not all(u.is_compatible_with(self.units) for u in units): + raise ValueError("All operands must have compatible units") + + +def _associative_obviously_eq(self: Expression, other: Expression) -> bool: + remaining = list(other.operands) + for op in self.operands: + for r in remaining: + if obviously_eq(op, r): + remaining.remove(r) + break + return not remaining + + +class Add(Additive): + def __init__(self, *operands): + super().__init__(*operands) + + # TODO caching + @override + def obviously_eq(self, other: ParameterOperatable.All) -> bool: + if ParameterOperatable.obviously_eq(self, other): return True - if isinstance(lhs, ANY): - return False - if is_either_instance(TBD): - return False - if is_either_instance(Operation): - return False + if isinstance(other, Add): + return _associative_obviously_eq(self, other) + return False - # Sets - return lhs & rhs == lhs + def obviously_eq_hash(self) -> int: + op_hash = sum(hash(op) for op in self.operands) + return hash((type(self), op_hash)) - @_resolved - def merge(self: "Parameter", other: "Parameter") -> "Parameter": - if self is other: - return self - out = self._merge(other) - self._narrowed(out) - other._narrowed(out) +class Subtract(Additive): + def __init__(self, minuend, subtrahend): + super().__init__(minuend, subtrahend) - return out - @_resolved - def override(self: "Parameter", other: "Parameter") -> "Parameter": - if not other.is_subset_of(self): - raise self.MergeException("override not possible") +class Multiply(Arithmetic): + def __init__(self, *operands): + super().__init__(*operands) + units = [HasUnit.get_units_or_dimensionless(op) for op in operands] + self.units = units[0] + for u in units[1:]: + self.units = cast_assert(Unit, self.units * u) - self._narrowed(other) - return other + # TODO caching + @override + def obviously_eq(self, other: ParameterOperatable.All) -> bool: + if ParameterOperatable.obviously_eq(self, other): + return True + if isinstance(other, Add): + return _associative_obviously_eq(self, other) + return False - # TODO: replace with graph-based - @staticmethod - def arithmetic_op(op1: "Parameter", op2: "Parameter", op: Callable) -> "Parameter": - from faebryk.library.ANY import ANY - from faebryk.library.Constant import Constant - from faebryk.library.Operation import Operation - from faebryk.library.Range import Range - from faebryk.library.Set import Set - from faebryk.library.TBD import TBD - - def _is_pair[T, U]( - type1: type[T], type2: type[U] - ) -> Optional[tuple[T, U, Callable]]: - if isinstance(op1, type1) and isinstance(op2, type2): - return op1, op2, op - if isinstance(op1, type2) and isinstance(op2, type1): - return op2, op1, TwistArgs(op) - - return None - - if pair := _is_pair(Constant, Constant): - return Constant(op(pair[0].value, pair[1].value)) - - if pair := _is_pair(Range, Range): - try: - p0_min, p0_max = pair[0].min, pair[0].max - p1_min, p1_max = pair[1].min, pair[1].max - except Range.MinMaxError: - return Operation(pair[:2], op) - return Range( - *( - op(lhs, rhs) - for lhs, rhs in [ - (p0_min, p1_min), - (p0_max, p1_max), - (p0_min, p1_max), - (p0_max, p1_min), - ] - ) - ) + def obviously_eq_hash(self) -> int: + op_hash = sum(hash(op) for op in self.operands) + return hash((type(self), op_hash)) + + +class Divide(Arithmetic): + def __init__(self, numerator, denominator): + super().__init__(numerator, denominator) + self.units = numerator.units / denominator.units + + +class Sqrt(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + self.units = operand.units**0.5 + + +class Power(Arithmetic): + def __init__(self, base, exponent: int): + super().__init__(base, exponent) + if isinstance(exponent, HasUnit) and not exponent.units.is_compatible_with( + dimensionless + ): + raise ValueError("exponent must have dimensionless unit") + units = HasUnit.get_units_or_dimensionless(base) ** exponent + assert isinstance(units, Unit) + self.units = units + + +class Log(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + if not operand.unit.is_compatible_with(dimensionless): + raise ValueError("operand must have dimensionless unit") + self.units = dimensionless + + +class Sin(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + if not operand.unit.is_compatible_with(dimensionless): + raise ValueError("operand must have dimensionless unit") + self.units = dimensionless + + +class Cos(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + if not operand.unit.is_compatible_with(dimensionless): + raise ValueError("operand must have dimensionless unit") + self.units = dimensionless + + +class Abs(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + self.units = operand.units + + +class Round(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + self.units = operand.units + + +class Floor(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + self.units = operand.units + + +class Ceil(Arithmetic): + def __init__(self, operand): + super().__init__(operand) + self.units = operand.units + + +class Logic(ConstrainableExpression): + def __init__(self, *operands): + super().__init__(*operands) + types = bool, Parameter, Logic, Predicate + if any(not isinstance(op, types) for op in operands): + raise ValueError("operands must be bool, Parameter, Logic, or Predicate") + if any( + param.domain != Boolean or not param.units.is_compatible_with(dimensionless) + for param in operands + if isinstance(param, Parameter) + ): + raise ValueError("parameters must have domain Boolean without a unit") + + +class And(Logic): + pass + + +class Or(Logic): + pass + + +class Not(Logic): + def __init__(self, operand): + super().__init__(operand) + + +class Xor(Logic): + def __init__(self, left, right): + super().__init__(left, right) + + +class Implies(Logic): + def __init__(self, condition, implication): + super().__init__(condition, implication) + + +class IfThenElse(Expression): + def __init__(self, condition, if_true, if_false, preference: bool | None = None): + super().__init__(condition) + self.preference = preference + self.if_true = if_true + self.if_false = if_false + + +class Setic(ConstrainableExpression): + def __init__(self, *operands): + super().__init__(*operands) + types = [Parameter, ParameterOperatable.Sets] + if any(type(op) not in types for op in operands): + raise ValueError("operands must be Parameter or Set") + units = [HasUnit.get_units_or_dimensionless(op) for op in operands] + self.units = units[0] + for u in units[1:]: + if not self.units.is_compatible_with(u): + raise ValueError("all operands must have compatible units") + # TODO domain? - if pair := _is_pair(Constant, Range): - sop = pair[2] - try: - return Range(*(sop(pair[0], bound) for bound in pair[1].bounds)) - except Range.MinMaxError: - return Operation(pair[:2], op) - - if pair := _is_pair(Parameter, ANY): - sop = pair[2] - return Operation(pair[:2], sop) - - if pair := _is_pair(Parameter, Operation): - sop = pair[2] - return Operation(pair[:2], sop) - - if pair := _is_pair(Parameter, TBD): - sop = pair[2] - return Operation(pair[:2], sop) - - if pair := _is_pair(Parameter, Set): - sop = pair[2] - return Set( - Parameter.arithmetic_op(nested, pair[0], sop) - for nested in pair[1].params + +class Union(Setic): + pass + + +class Intersection(Setic): + pass + + +class Difference(Setic): + def __init__(self, minuend, subtrahend): + super().__init__(minuend, subtrahend) + + +class SymmetricDifference(Setic): + pass + + +class Domain: + pass + + +class ESeries(Domain): + class SeriesType(Enum): + E6 = auto() + E12 = auto() + E24 = auto() + E48 = auto() + E96 = auto() + E192 = auto() + + def __init__(self, series: SeriesType): + self.series = series + + +class Numbers(Domain): + def __init__( + self, *, negative: bool = True, zero_allowed: bool = True, integer: bool = False + ) -> None: + super().__init__() + self.negative = negative + self.zero_allowed = zero_allowed + self.integer = integer + + +class Boolean(Domain): + pass + + +class EnumDomain(Domain): + def __init__(self, enum_t: type[Enum]): + super().__init__() + self.enum_t = enum_t + + +class Predicate(ConstrainableExpression): + def __init__(self, left, right): + super().__init__(left, right) + l_units = HasUnit.get_units_or_dimensionless(left) + r_units = HasUnit.get_units_or_dimensionless(right) + if not l_units.is_compatible_with(r_units): + raise ValueError("operands must have compatible units") + + def __bool__(self): + raise ValueError("Predicate cannot be converted to bool") + + +class NumericPredicate(Predicate): + def __init__(self, left, right): + super().__init__(left, right) + if isinstance(left, Parameter) and not isinstance( + left.domain, (Numbers, ESeries) + ): + raise ValueError( + "left operand must have domain Numbers or ESeries," + f" not {type(left.domain)}" + ) + if isinstance(right, Parameter) and not isinstance( + right.domain, (Numbers, ESeries) + ): + raise ValueError( + "right operand must have domain Numbers or ESeries," + f" not {type(right.domain)}" ) - raise NotImplementedError - @staticmethod - def intersect(op1: "Parameter", op2: "Parameter") -> "Parameter": - from faebryk.library.Constant import Constant - from faebryk.library.Operation import Operation - from faebryk.library.Range import Range - from faebryk.library.Set import Set - - if op1 == op2: - return op1 - - def _is_pair[T, U]( - type1: type[T], type2: type[U] - ) -> Optional[tuple[T, U, Callable]]: - if isinstance(op1, type1) and isinstance(op2, type2): - return op1, op2, op - if isinstance(op1, type2) and isinstance(op2, type1): - return op2, op1, TwistArgs(op) - - return None - - def op(a, b): - return a & b - - # same types - if pair := _is_pair(Constant, Constant): - return Set([]) - if pair := _is_pair(Set, Set): - return Set(pair[0].params.intersection(pair[1].params)) - if pair := _is_pair(Range, Range): - try: - min_ = max(pair[0].min, pair[1].min) - max_ = min(pair[0].max, pair[1].max) - if min_ > max_: - return Set([]) - if min_ == max_: - return Constant(min_) - return Range(max_, min_) - except Range.MinMaxError: - return Operation(pair[:2], op) - - # diff types - if pair := _is_pair(Constant, Range): - try: - if pair[0] in pair[1]: - return pair[0] - else: - return Set([]) - except Range.MinMaxError: - return Operation(pair[:2], op) - if pair := _is_pair(Constant, Set): - if pair[0] in pair[1]: - return pair[0] - else: - return Set([]) - if pair := _is_pair(Range, Set): - try: - return Set(i for i in pair[1].params if i in pair[0]) - except Range.MinMaxError: - return Operation(pair[:2], op) - - return Operation((op1, op2), op) - - @_resolved - def __add__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: a + b) - - @_resolved - def __radd__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: b + a) - - @_resolved - def __sub__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: a - b) - - @_resolved - def __rsub__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: b - a) - - # TODO PV | float - @_resolved - def __mul__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: a * b) - - @_resolved - def __rmul__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: b * a) - - # TODO PV | float - @_resolved - def __truediv__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: a / b) - - @_resolved - def __rtruediv__(self: "Parameter", other: "Parameter"): - return self.arithmetic_op(self, other, lambda a, b: b / a) - - @_resolved - def __pow__(self: "Parameter", other: "Parameter") -> "Parameter": - return self.arithmetic_op(self, other, lambda a, b: a**b) - - @_resolved - def __rpow__(self: "Parameter", other: "Parameter") -> "Parameter": - return self.arithmetic_op(self, other, lambda a, b: b**a) - - @_resolved - def __and__(self: "Parameter", other: "Parameter") -> "Parameter": - return self.intersect(self, other) - - @_resolved - def __rand__(self: "Parameter", other: "Parameter") -> "Parameter": - return self.intersect(other, self) - - def get_most_narrow(self) -> "Parameter": - out = self.get_narrowing_chain()[-1] - - com = out.try_compress() - if com is not out: - com = com.get_most_narrow() - out._narrowed(com) - out = com - - return out +class LessThan(NumericPredicate): + pass - @staticmethod - def resolve_all(params: "Sequence[Parameter]") -> "Parameter": - from faebryk.library.TBD import TBD - - params_set = list(params) - if not params_set: - return TBD() - it = iter(params_set) - most_specific = next(it) - for param in it: - most_specific = most_specific.merge(param) - - return most_specific - - @try_avoid_endless_recursion - def __str__(self) -> str: - narrowest = self.get_most_narrow() - if narrowest is self: - return super().__str__() - return str(narrowest) - - # @try_avoid_endless_recursion - # def __repr__(self) -> str: - # narrowest = self.get_most_narrow() - # if narrowest is self: - # return super().__repr__() - # # return f"{super().__repr__()} -> {repr(narrowest)}" - # return repr(narrowest) - - def get_narrowing_chain(self) -> list["Parameter"]: - out: list[Parameter] = [self] - narrowers = self.narrowed_by.get_connected_nodes([Parameter]) - if narrowers: - assert len(narrowers) == 1, "Narrowing tree diverged" - out += cast_assert(Parameter, next(iter(narrowers))).get_narrowing_chain() - assert id(self) not in map(id, out[1:]), "Narrowing tree cycle" - return out - - def get_narrowed_siblings(self) -> set["Parameter"]: - return self.narrows.get_connected_nodes([Parameter]) # type: ignore - - def __copy__(self) -> Self: - return type(self)() - - def __deepcopy__(self, memo) -> Self: - return self.__copy__() - - def get_tree_param(self, include_root: bool = True) -> Tree["Parameter"]: - out = Tree[Parameter]( - {p: p.get_tree_param() for p in self.get_narrowed_siblings()} - ) - if include_root: - out = Tree[Parameter]({self: out}) - return out - - # util functions ------------------------------------------------------------------- - @_resolved_self - def enum_parameter_representation(self: "Parameter", required: bool = False) -> str: - return self._enum_parameter_representation(required=required) - - def _enum_parameter_representation(self, required: bool = False) -> str: - return self.as_unit("", required=required) - - @_resolved_self - def as_unit( - self: "Parameter", - unit: UnitsContainer, - base: int = 1000, - required: bool = False, - ) -> str: - if base != 1000: - raise NotImplementedError("Only base 1000 supported") - - return self._as_unit(unit, base=base, required=required) - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - raise ValueError(f"Unsupported {self}") - - @_resolved_self - def as_unit_with_tolerance( - self: "Parameter", - unit: UnitsContainer, - base: int = 1000, - required: bool = False, - ) -> str: - return self._as_unit_with_tolerance(unit, base=base, required=required) - - def _as_unit_with_tolerance( - self, unit: UnitsContainer, base: int, required: bool - ) -> str: - return self._as_unit(unit, base=base, required=required) - - @_resolved_self - def get_max(self: "Parameter") -> PV: - return self._max() - - def _max(self): - raise ValueError(f"Can't get max for {self}") - - def with_same_unit( - self: "Quantity | float | int | LIT_OR_PARAM", - to_convert: float | int, + +class GreaterThan(NumericPredicate): + pass + + +class LessOrEqual(NumericPredicate): + pass + + +class GreaterOrEqual(NumericPredicate): + pass + + +class NotEqual(NumericPredicate): + pass + + +class SeticPredicate(Predicate): + def __init__(self, left, right): + super().__init__(left, right) + # types = ParameterOperatable, P_Set + # TODO + # if any(not isinstance(op, types) for op in self.operands): + # raise ValueError("operands must be Parameter or Set") + units = [op.units for op in self.operands] + for u in units[1:]: + if not units[0].is_compatible_with(u): + raise ValueError("all operands must have compatible units") + # TODO domain? + + +class IsSubset(SeticPredicate): + pass + + +class IsSuperset(SeticPredicate): + pass + + +class Cardinality(SeticPredicate): + def __init__( + self, set: ParameterOperatable.Sets, cardinality: ParameterOperatable.NumberLike + ): + super().__init__(set, cardinality) + + +class Is(Predicate): + def __init__(self, left, right): + super().__init__(left, right) + + +# TODO rename? +class R(Namespace): + """ + Namespace holding Expressions, Domains and Predicates for Parameters. + R = paRameters + """ + + class Predicates(Namespace): + class Element(Namespace): + LT = LessThan + GT = GreaterThan + LE = LessOrEqual + GE = GreaterOrEqual + NE = NotEqual + + class Set(Namespace): + IS_SUBSET = IsSubset + IS_SUPERSET = IsSuperset + + class Domains(Namespace): + class ESeries(Namespace): + E6 = lambda: ESeries(ESeries.SeriesType.E6) # noqa: E731 + E12 = lambda: ESeries(ESeries.SeriesType.E12) # noqa: E731 + E24 = lambda: ESeries(ESeries.SeriesType.E24) # noqa: E731 + E48 = lambda: ESeries(ESeries.SeriesType.E48) # noqa: E731 + E96 = lambda: ESeries(ESeries.SeriesType.E96) # noqa: E731 + E192 = lambda: ESeries(ESeries.SeriesType.E192) # noqa: E731 + + class Numbers(Namespace): + REAL = Numbers + NATURAL = lambda: Numbers(integer=True, negative=False) # noqa: E731 + + BOOL = Boolean + ENUM = EnumDomain + + class Expressions(Namespace): + class Arithmetic(Namespace): + ADD = Add + SUBTRACT = Subtract + MULTIPLY = Multiply + DIVIDE = Divide + POWER = Power + LOG = Log + SQRT = Sqrt + LOG = Log + ABS = Abs + FLOOR = Floor + CEIL = Ceil + ROUND = Round + SIN = Sin + COS = Cos + + class Logic(Namespace): + AND = And + OR = Or + NOT = Not + XOR = Xor + IMPLIES = Implies + + class Set(Namespace): + UNION = Union + INTERSECTION = Intersection + DIFFERENCE = Difference + SYMMETRIC_DIFFERENCE = SymmetricDifference + + +class Parameter(ParameterOperatable, Constrainable): + class TraitT(Trait): ... + + def __init__( + self, + *, + units: Unit | Quantity | None = dimensionless, + # hard constraints + within: Ranges | Range | None = None, + domain: Domain = Numbers(negative=False), + # soft constraints + soft_set: Ranges | Range | None = None, + guess: Quantity + | int + | float + | None = None, # TODO actually allowed to be anything from domain + tolerance_guess: float | None = None, + # hints + likely_constrained: bool = False, # TODO rename expect_constraits or similiar ): - from faebryk.library.Constant import Constant - - if isinstance(self, Constant) and isinstance(self.value, Quantity): - return Quantity(to_convert, self.value.units) - if isinstance(self, Quantity): - return Quantity(to_convert, self.units) - if isinstance(self, (float, int)): - return to_convert - raise NotImplementedError(f"Unsupported {self=}") + super().__init__() + if within is not None and not within.units.is_compatible_with(units): + raise ValueError("incompatible units") + + if isinstance(within, Range): + within = Ranges(within) + + if isinstance(soft_set, Range): + soft_set = Ranges(soft_set) + + if not isinstance(units, Unit): + raise TypeError("units must be a Unit") + self.units = units + self.within = within + self.domain = domain + self.soft_set = soft_set + self.guess = guess + self.tolerance_guess = tolerance_guess + self.likely_constrained = likely_constrained + + # Type forwards + type All = ParameterOperatable.All + type NumberLike = ParameterOperatable.NumberLike + type Sets = ParameterOperatable.Sets + type BooleanLike = ParameterOperatable.BooleanLike + type Number = ParameterOperatable.Number + + +p_field = f_field(Parameter) diff --git a/src/faebryk/core/solver.py b/src/faebryk/core/solver.py new file mode 100644 index 00000000..9b616034 --- /dev/null +++ b/src/faebryk/core/solver.py @@ -0,0 +1,99 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import logging +from dataclasses import dataclass +from typing import Any, Protocol + +from faebryk.core.graphinterface import Graph +from faebryk.core.parameter import ( + Expression, + ParameterOperatable, + Predicate, +) + +logger = logging.getLogger(__name__) + + +class Solver(Protocol): + # TODO booleanlike is very permissive + type PredicateWithInfo[ArgType] = tuple[ParameterOperatable.BooleanLike, ArgType] + + class SolverError(Exception): ... + + class TimeoutError(SolverError): ... + + class DivisionByZeroError(SolverError): ... + + @dataclass + class SolveResult: + timed_out: bool + + @dataclass + class SolveResultAny[ArgType](SolveResult): + true_predicates: list["Solver.PredicateWithInfo[ArgType]"] + false_predicates: list["Solver.PredicateWithInfo[ArgType]"] + unknown_predicates: list["Solver.PredicateWithInfo[ArgType]"] + + @dataclass + class SolveResultAll(SolveResult): + has_solution: bool + + # timeout per solve call in milliseconds + timeout: int + # threads: int + # in megabytes + # memory: int + + def get_any_single( + self, + operatable: ParameterOperatable, + lock: bool, + suppose_constraint: Predicate | None = None, + minimize: Expression | None = None, + ) -> Any: + """ + Solve for a single value for the given expression. + + Args: + operatable: The expression or parameter to solve. + suppose_constraint: An optional constraint that can be added to make solving + easier. It is only in effect for the duration of the + solve call. + minimize: An optional expression to minimize while solving. + lock: If True, ensure the result is part of the solution set of + the expression. + + Returns: + A SolveResultSingle object containing the chosen value. + """ + ... + + def assert_any_predicate[ArgType]( + self, + predicates: list["Solver.PredicateWithInfo[ArgType]"], + lock: bool, + suppose_constraint: Predicate | None = None, + minimize: Expression | None = None, + ) -> SolveResultAny[ArgType]: + """ + Make at least one of the passed predicates true, unless that is impossible. + + Args: + predicates: A list of predicates to solve. + suppose_constraint: An optional constraint that can be added to make solving + easier. It is only in effect for the duration of the + solve call. + minimize: An optional expression to minimize while solving. + lock: If True, add the solutions as constraints. + + Returns: + A SolveResult object containing the true, false, and unknown predicates. + + Note: + There is no specific order in which the predicates are solved. + """ + ... + + # run deferred work + def find_and_lock_solution(self, G: Graph) -> SolveResultAll: ... diff --git a/src/faebryk/exporters/esphome/esphome.py b/src/faebryk/exporters/esphome/esphome.py index b638a86d..1799ebba 100644 --- a/src/faebryk/exporters/esphome/esphome.py +++ b/src/faebryk/exporters/esphome/esphome.py @@ -2,75 +2,34 @@ # SPDX-License-Identifier: MIT import logging -from typing import Any, Callable import yaml import faebryk.library._F as F from faebryk.core.graph import Graph, GraphFunctions from faebryk.core.parameter import Parameter +from faebryk.core.solver import Solver +from faebryk.libs.units import Quantity +from faebryk.libs.util import cast_assert, dict_value_visitor, merge_dicts logger = logging.getLogger(__name__) -# TODO move to util -def dict_map_values(d: dict, function: Callable[[Any], Any]) -> dict: - """recursively map all values in a dict""" - - result = {} - for key, value in d.items(): - if isinstance(value, dict): - result[key] = dict_map_values(value, function) - elif isinstance(value, list): - result[key] = [dict_map_values(v, function) for v in value] - else: - result[key] = function(value) - return result - - -def merge_dicts(*dicts: dict) -> dict: - """merge a list of dicts into a single dict, - if same key is present and value is list, lists are merged - if same key is dict, dicts are merged recursively - """ - result = {} - for d in dicts: - for k, v in d.items(): - if k in result: - if isinstance(v, list): - assert isinstance( - result[k], list - ), f"Trying to merge list into key '{k}' of type {type(result[k])}" - result[k] += v - elif isinstance(v, dict): - assert isinstance(result[k], dict) - result[k] = merge_dicts(result[k], v) - else: - result[k] = v - else: - result[k] = v - return result - - -def make_esphome_config(G: Graph) -> dict: +def make_esphome_config(G: Graph, solver: Solver) -> dict: esphome_components = GraphFunctions(G).nodes_with_trait(F.has_esphome_config) esphome_config = merge_dicts(*[t.get_config() for _, t in esphome_components]) - def instantiate_param(param: Parameter | Any): - if not isinstance(param, Parameter): - return param + # deep find parameters in dict and solve + def solve_parameter(v): + if not isinstance(v, Parameter): + return v - if not isinstance(param, F.Constant): - raise Exception( - f"Parameter {param} is not a F.Constant, but {type(param)}" - f"Config: {esphome_config}" - ) - return param.value + return str(cast_assert(Quantity, solver.get_any_single(v, lock=True).value)) - instantiated = dict_map_values(esphome_config, instantiate_param) + dict_value_visitor(esphome_config, lambda _, v: solve_parameter(v)) - return instantiated + return esphome_config def dump_esphome_config(config: dict) -> str: diff --git a/src/faebryk/exporters/parameters/parameters_to_file.py b/src/faebryk/exporters/parameters/parameters_to_file.py index 4c4dabdd..678883f0 100644 --- a/src/faebryk/exporters/parameters/parameters_to_file.py +++ b/src/faebryk/exporters/parameters/parameters_to_file.py @@ -3,13 +3,146 @@ import logging from pathlib import Path +from typing import Callable, Iterable +from faebryk.core.graphinterface import Graph from faebryk.core.module import Module -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import Expression, Is, Parameter, Predicate +from faebryk.libs.util import EquivalenceClasses, groupby, ind, typename logger = logging.getLogger(__name__) +def parameter_alias_classes(G: Graph) -> list[set[Parameter]]: + full_eq = EquivalenceClasses[Parameter](G.nodes_of_type(Parameter)) + + is_exprs = [e for e in G.nodes_of_type(Is) if e.constrained] + + for is_expr in is_exprs: + params_ops = [op for op in is_expr.operands if isinstance(op, Parameter)] + full_eq.add_eq(*params_ops) + + return full_eq.get() + + +def get_params_for_expr(expr: Expression) -> set[Parameter]: + param_ops = {op for op in expr.operatable_operands if isinstance(op, Parameter)} + expr_ops = {op for op in expr.operatable_operands if isinstance(op, Expression)} + + return param_ops | {op for e in expr_ops for op in get_params_for_expr(e)} + + +def parameter_dependency_classes(G: Graph) -> list[set[Parameter]]: + related = EquivalenceClasses[Parameter](G.nodes_of_type(Parameter)) + + eq_exprs = [e for e in G.nodes_of_type(Predicate) if e.constrained] + + for eq_expr in eq_exprs: + params = get_params_for_expr(eq_expr) + related.add_eq(*params) + + return related.get() + + +def parameter_report(G: Graph, path: Path): + params = G.nodes_of_type(Parameter) + exprs = G.nodes_of_type(Expression) + predicates = {e for e in exprs if isinstance(e, Predicate)} + exprs.difference_update(predicates) + alias_classes = parameter_alias_classes(G) + eq_classes = parameter_dependency_classes(G) + unused = [ + p + for p in params + if not any(isinstance(e.node, Expression) for e in p.operated_on.edges) + ] + + def non_empty(classes: list[set[Parameter]]): + return [c for c in classes if len(c) > 1] + + def bound(classes: list[set[Parameter]]): + return sum(len(c) for c in non_empty(classes)) + + infostr = ( + f"{len(params)} parameters" + f"\n {len(non_empty(alias_classes))}({bound(alias_classes)}) alias classes" + f"\n {len(non_empty(eq_classes))}({bound(eq_classes)}) equivalence classes" + f"\n {len(unused)} unused" + "\n" + ) + infostr += f"{len(exprs)} expressions, {len(predicates)} predicates" + + logger.info(f"Found {infostr}") + + out = "" + out += infostr + "\n" + + def block( + header: str, + f: Callable[[], str] | None = None, + lines: list[str] | list[list[str]] | None = None, + ): + nonlocal out + out_str = "" + if f: + out_str += f() + if lines: + lines = [n for n in lines if isinstance(n, str)] + [ + n for nested in lines if isinstance(nested, list) for n in nested + ] + out_str += "\n".join(lines) + + out += f"{header}{'-'*80}\n{ind(out_str)}\n" + + block( + "Parameters", + lines=sorted([p.get_full_name(types=True) for p in params]), + ) + + block( + "Unused", + lines=sorted([p.get_full_name(types=True) for p in unused]), + ) + + def Eq(classes: list[set[Parameter]]): + stream = "" + for eq_class in classes: + if len(eq_class) <= 1: + continue + stream += "\n ".join( + sorted([p.get_full_name(types=True) for p in eq_class]) + ) + stream += "\n" + return stream.removesuffix("\n") + + block( + "Fully aliased", + f=lambda: Eq(alias_classes), + ) + + block( + "Equivalence classes", + f=lambda: Eq(eq_classes), + ) + + def type_group(name: str, container: Iterable): + type_grouped = sorted( + groupby(container, lambda x: type(x)).items(), key=lambda x: typename(x[0]) + ) + block( + name, + lines=[ + f"{typename(type_)}: {len(list(group))}" + for type_, group in type_grouped + ], + ) + + type_group("Expressions", exprs) + type_group("Predicates", predicates) + + path.write_text(out) + + def export_parameters_to_file(module: Module, path: Path): """Write all parameters of the given module to a file.""" # {module_name: [{param_name: param_value}, {param_name: param_value},...]} diff --git a/src/faebryk/exporters/visualize/interactive_params.py b/src/faebryk/exporters/visualize/interactive_params.py new file mode 100644 index 00000000..7db6046e --- /dev/null +++ b/src/faebryk/exporters/visualize/interactive_params.py @@ -0,0 +1,264 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +from dataclasses import dataclass +from pathlib import Path +from typing import cast + +import dash_core_components as dcc +import dash_cytoscape as cyto +from dash import Dash, html +from dash.dependencies import Input, Output, State + +# import faebryk.library._F as F +from faebryk.core.graph import Graph, GraphFunctions +from faebryk.core.link import LinkSibling +from faebryk.core.node import Node +from faebryk.core.parameter import Expression, Parameter +from faebryk.exporters.parameters.parameters_to_file import parameter_report +from faebryk.exporters.visualize.interactive_params_base import ( + _GROUP_TYPES, + Layout, + _Layout, +) +from faebryk.libs.util import ( + KeyErrorAmbiguous, + find_or, + typename, +) + +Operand = Parameter | Expression + + +@dataclass(eq=True, frozen=True) +class ParamLink: + operator: Expression + operand: Operand + + +def _node(node: Operand): + try: + subtype = find_or(_GROUP_TYPES, lambda t: isinstance(node, t), default=Node) + except KeyErrorAmbiguous as e: + subtype = e.duplicates[0] + + hier = node.get_hierarchy() + type_hier = [t for t, _ in hier] + name_hier = [n for _, n in hier] + name = ".".join(name_hier) + types = "|".join(typename(t) for t in type_hier) + label = f"{name}\n({types})" + + return { + "data": { + "id": str(id(node)), + "label": label, + "type": typename(subtype), + } + } + + +def _link(link: ParamLink): + return { + "data": { + "source": str(id(link.operand)), + "target": str(id(link.operator)), + } + } + + +class _Stylesheet: + _BASE = [ + { + "selector": "node", + "style": { + "content": "data(label)", + "text-opacity": 0.8, + "text-valign": "center", + "text-halign": "center", + "font-size": "0.3em", + "background-color": "#BFD7B5", + "text-outline-color": "#FFFFFF", + "text-outline-width": 0.5, + "border-width": 1, + "border-color": "#888888", + "border-opacity": 0.5, + # group + "font-weight": "bold", + # "font-size": "1.5em", + # "text-valign": "top", + # "text-outline-color": "#FFFFFF", + # "text-outline-width": 1.5, + "text-wrap": "wrap", + # "border-width": 4, + }, + }, + { + "selector": "edge", + "style": { + "width": 1, + "line-color": "#A3C4BC", + "curve-style": "bezier", + "target-arrow-shape": "triangle", + "arrow-scale": 1, + "target-arrow-color": "#A3C4BC", + "text-outline-color": "#FFFFFF", + "text-outline-width": 2, + }, + }, + ] + + def __init__(self): + self.stylesheet = list(self._BASE) + + def add_node_type(self, node_type: str, color: str): + self.stylesheet.append( + { + "selector": f'node[type = "{node_type}"]', + "style": {"background-color": color}, + } + ) + + +DAGRE_LAYOUT = { + # Dagre algorithm options (uses default value if undefined) + "name": "dagre", + # Separation between adjacent nodes in the same rank + # "nodeSep": None, + # Separation between adjacent edges in the same rank + # "edgeSep": None, + # Separation between each rank in the layout + # "rankSep": None, + # 'TB' for top to bottom flow, 'LR' for left to right + # "rankDir": None, + # Alignment for rank nodes. Can be 'UL', 'UR', 'DL', or 'DR' + # "align": None, + # If 'greedy', uses heuristic to find feedback arc set + # "acyclicer": None, + # Algorithm to assign rank to nodes: 'network-simplex', 'tight-tree' + # or 'longest-path' + # "ranker": "tight-tree", + # Number of ranks to keep between source and target of the edge + # "minLen": lambda edge: 1, + # Higher weight edges are generally made shorter and straighter + # "edgeWeight": lambda edge: 1, + # General layout options + # Whether to fit to viewport + # "fit": True, + # Fit padding + # "padding": 30, + # Factor to expand/compress overall area nodes take up + # "spacingFactor": None, + # Include labels in node space calculation + # "nodeDimensionsIncludeLabels": False, + # Whether to transition node positions + # "animate": False, + # Whether to animate specific nodes + # "animateFilter": lambda node, i: True, + # Duration of animation in ms if enabled + # "animationDuration": 500, + # Easing of animation if enabled + # "animationEasing": None, + # Constrain layout bounds: {x1, y1, x2, y2} or {x1, y1, w, h} + # "boundingBox": None, + # Function to transform final node position + # "transform": lambda node, pos: pos, + # Callback on layoutready + # "ready": lambda: None, + # Sorting function to order nodes and edges + # "sort": None, + # Callback on layoutstop + # "stop": lambda: None, +} + + +def buttons(layout: Layout): + app = layout.app + + layout_chooser = dcc.RadioItems( + id="layout-radio", + options=[ + {"label": "fcose", "value": "fcose"}, + {"label": "dagre", "value": "dagre"}, + ], + value="dagre", + ) + + dagre_ranker = dcc.RadioItems( + id="layout-dagre-ranker", + options=[ + {"label": "network-simplex", "value": "network-simplex"}, + {"label": "tight-tree", "value": "tight-tree"}, + {"label": "longest-path", "value": "longest-path"}, + ], + value="tight-tree", + ) + + html_controls = html.Div( + className="controls", + style={"padding": "10px", "background-color": "#f0f0f0"}, + children=[ + html.Table( + [ + html.Tr([html.Td("Layout:"), html.Td(layout_chooser)]), + html.Tr([html.Td("Dagre Ranker:"), html.Td(dagre_ranker)]), + ] + ) + ], + ) + layout.div_children.insert(-2, html_controls) + + @app.callback( + Output("graph-view", "layout"), + Input("layout-radio", "value"), + Input("layout-dagre-ranker", "value"), + State("graph-view", "layout"), + ) + def absolute_layout(layout_radio, layout_dagre_ranker, current_layout): + # print(layout_radio, layout_dagre_ranker) + layout.set_type(layout_radio, current_layout) + + if layout_dagre_ranker: + current_layout["ranker"] = layout_dagre_ranker + + return current_layout + + +def visualize_parameters(G: Graph, height: int | None = None): + Operand_ = (Parameter, Expression) + nodes = GraphFunctions(G).nodes_of_types(Operand_) + nodes = cast(list[Operand], nodes) + + edges = { + ParamLink(n, e.node) + for n in nodes + if isinstance(n, Expression) + for e, li in n.operates_on.edges.items() + if not isinstance(li, LinkSibling) + and e.node is not n + and isinstance(e.node, Operand_) + } + + # TODO filter equivalency classes + + parameter_report(G, Path("./build/params.txt")) + + elements = [_node(n) for n in nodes] + [_link(li) for li in edges] + stylesheet = _Stylesheet() + + node_types_colors = [ + (typename(group_type), color) for group_type, color in _GROUP_TYPES.items() + ] + + for node_type, color in node_types_colors: + stylesheet.add_node_type(node_type, color) + + cyto.load_extra_layouts() + app = Dash(__name__) + app.layout = _Layout(stylesheet, elements, extra=DAGRE_LAYOUT) + + # Extra layouting + layout = Layout(app, elements, list(nodes)) + buttons(layout) + + app.run(jupyter_height=height or 1400) diff --git a/src/faebryk/exporters/visualize/interactive_params_base.py b/src/faebryk/exporters/visualize/interactive_params_base.py new file mode 100644 index 00000000..b259501f --- /dev/null +++ b/src/faebryk/exporters/visualize/interactive_params_base.py @@ -0,0 +1,713 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +# TODO this used to be interactive_graph.py +# merge it back into it + +from itertools import pairwise +from typing import Any, Callable, Collection, Iterable + +import dash_core_components as dcc +import dash_cytoscape as cyto +from dash import Dash, html +from dash.dependencies import Input, Output, State +from rich.console import Console +from rich.table import Table + +# import faebryk.library._F as F +from faebryk.core.graph import Graph, GraphFunctions +from faebryk.core.graphinterface import GraphInterface +from faebryk.core.link import Link, LinkSibling +from faebryk.core.module import Module +from faebryk.core.moduleinterface import ModuleInterface +from faebryk.core.node import Node +from faebryk.core.parameter import Expression, Parameter, Predicate +from faebryk.core.trait import Trait +from faebryk.exporters.visualize.util import generate_pastel_palette +from faebryk.libs.util import KeyErrorAmbiguous, cast_assert, find_or, groupby, typename + + +# Transformers ------------------------------------------------------------------------- +def _gif(gif: GraphInterface): + return { + "data": { + "id": str(id(gif)), + "label": gif.name, + "type": typename(gif), + "parent": str(id(gif.node)), + } + } + + +def _link(source, target, link: Link): + return { + "data": { + "source": str(id(source)), + "target": str(id(target)), + "type": typename(link), + } + } + + +_GROUP_TYPES = { + Predicate: "#FCF3CF", # Very light goldenrod + Expression: "#D1F2EB", # Very soft turquoise + Parameter: "#FFD9DE", # Very light pink + Module: "#E0F0FF", # Very light blue + Trait: "#FCFCFF", # Almost white + # F.Electrical: "#D1F2EB", # Very soft turquoise + # F.ElectricPower: "#FCF3CF", # Very light goldenrod + # F.ElectricLogic: "#EBE1F1", # Very soft lavender + # Defaults + ModuleInterface: "#DFFFE4", # Very light green + Node: "#FCFCFF", # Almost white +} + + +def _group(node: Node, root: bool): + try: + subtype = find_or(_GROUP_TYPES, lambda t: isinstance(node, t), default=Node) + except KeyErrorAmbiguous as e: + subtype = e.duplicates[0] + + if root: + hier = node.get_hierarchy() + type_hier = [t for t, _ in hier] + name_hier = [n for _, n in hier] + name = ".".join(name_hier) + types = "|".join(typename(t) for t in type_hier) + label = f"{name}\n({types})" + else: + label = f"{node.get_name(accept_no_parent=True)}\n({typename(node)})" + + return { + "data": { + "id": str(id(node)), + "label": label, + "type": "group", + "subtype": typename(subtype), + "parent": str(id(p[0])) if (p := node.get_parent()) else None, + } + } + + +# Style -------------------------------------------------------------------------------- + + +def _with_pastels(iterable: Collection[str]): + return zip(sorted(iterable), generate_pastel_palette(len(iterable))) + + +class _Stylesheet: + _BASE = [ + { + "selector": "node", + "style": { + "content": "data(label)", + "text-opacity": 0.8, + "text-valign": "center", + "text-halign": "center", + "font-size": "0.5em", + "background-color": "#BFD7B5", + "text-outline-color": "#FFFFFF", + "text-outline-width": 0.5, + "border-width": 1, + "border-color": "#888888", + "border-opacity": 0.5, + }, + }, + { + "selector": "edge", + "style": { + "width": 1, + "line-color": "#A3C4BC", + "curve-style": "bezier", + "target-arrow-shape": "triangle", + "arrow-scale": 1, + "target-arrow-color": "#A3C4BC", + "text-outline-color": "#FFFFFF", + "text-outline-width": 2, + }, + }, + { + "selector": 'node[type = "group"]', + "style": { + "background-color": "#D3D3D3", + "font-weight": "bold", + "font-size": "1.5em", + "text-valign": "top", + "text-outline-color": "#FFFFFF", + "text-outline-width": 1.5, + "text-wrap": "wrap", + "border-width": 4, + }, + }, + ] + + def __init__(self): + self.stylesheet = list(self._BASE) + + def add_node_type(self, node_type: str, color: str): + self.stylesheet.append( + { + "selector": f'node[type = "{node_type}"]', + "style": {"background-color": color}, + } + ) + + def add_link_type(self, link_type: str, color: str): + self.stylesheet.append( + { + "selector": f'edge[type = "{link_type}"]', + "style": { + "line-color": color, + "target-arrow-color": color, + # "target-arrow-shape": "none", + # "source-arrow-shape": "none", + }, + } + ) + + def add_group_type(self, group_type: str, color: str): + self.stylesheet.append( + { + "selector": f'node[subtype = "{group_type}"]', + "style": {"background-color": color}, + } + ) + + +def _Layout( + stylesheet: _Stylesheet, elements: list[dict[str, dict]], extra: dict | None = None +): + if not extra: + extra = {} + return html.Div( + style={ + "position": "fixed", + "display": "flex", + "flex-direction": "column", + "height": "100%", + "width": "100%", + }, + children=[ + html.Div( + className="cy-container", + style={"flex": "1", "position": "relative"}, + children=[ + cyto.Cytoscape( + id="graph-view", + stylesheet=stylesheet.stylesheet, + style={ + "position": "absolute", + "width": "100%", + "height": "100%", + "zIndex": 999, + }, + elements=elements, + layout={ + "name": "fcose", + } + | extra, + ) + ], + ), + ], + ) + + +def _get_layout(app: Dash) -> dict[str, Any]: + for html_node in cast_assert(list, cast_assert(html.Div, app.layout).children): + if not isinstance(html_node, html.Div): + continue + for child in cast_assert(list, html_node.children): + if not isinstance(child, cyto.Cytoscape): + continue + return child.layout + raise ValueError("No Cytoscape found in layout") + + +# -------------------------------------------------------------------------------------- + + +class Layout: + type ID_or_OBJECT = object | str + + def __init__(self, app: Dash, elements: list[dict], nodes: list[Node]): + self.app = app + self.layout = _get_layout(app) + self.ids = { + element["data"]["id"] for element in elements if "id" in element["data"] + } + + self.div_children = cast_assert( + list, cast_assert(html.Div, app.layout).children + ) + self.nodes = nodes + + def is_filtered(self, elem: ID_or_OBJECT) -> bool: + if not isinstance(elem, str): + elem = self.id_of(elem) + return elem not in self.ids + + def nodes_of_type[T: Node](self, node_type: type[T]) -> set[T]: + return { + n + for n in self.nodes + if isinstance(n, node_type) and not self.is_filtered(n.self_gif) + } + + @staticmethod + def id_of(obj: ID_or_OBJECT) -> str: + if isinstance(obj, str): + return obj + return str(id(obj)) + + def add_rel_constraint( + self, + source: ID_or_OBJECT, + target: ID_or_OBJECT, + gap_x: int | None = None, + gap_y: int | None = None, + layout: dict | None = None, + ): + if not layout: + layout = self.layout + + if "relativePlacementConstraint" not in layout: + layout["relativePlacementConstraint"] = [] + rel_placement_constraints = cast_assert( + list, layout["relativePlacementConstraint"] + ) + + if self.is_filtered(source) or self.is_filtered(target): + return + if gap_y is not None: + top, bot = (source, target) if gap_y >= 0 else (target, source) + + # if isinstance(top, GraphInterface) and isinstance(bot, GraphInterface): + # print(f"{top}\n v\n{bot}") + + rel_placement_constraints.append( + { + "top": self.id_of(top), + "bottom": self.id_of(bot), + "gap": abs(gap_y), + } + ) + if gap_x is not None: + left, right = (source, target) if gap_x >= 0 else (target, source) + + # if isinstance(left, GraphInterface) and isinstance(right, GraphInterface): + # print(f"{left} > {right}") + + rel_placement_constraints.append( + { + "left": self.id_of(left), + "right": self.id_of(right), + "gap": abs(gap_x), + } + ) + + def add_rel_top_bot( + self, + top: ID_or_OBJECT, + bot: ID_or_OBJECT, + gap: int = 0, + layout: dict | None = None, + ): + assert gap >= 0 + self.add_rel_constraint(top, bot, gap_y=gap, layout=layout) + + def add_rel_left_right( + self, + left: ID_or_OBJECT, + right: ID_or_OBJECT, + gap: int = 0, + layout: dict | None = None, + ): + assert gap >= 0 + self.add_rel_constraint(left, right, gap_x=gap, layout=layout) + + def add_order( + self, + *nodes: ID_or_OBJECT, + horizontal: bool, + gap: int = 0, + layout: dict | None = None, + ): + if not layout: + layout = self.layout + for n1, n2 in pairwise(nodes): + if horizontal: + self.add_rel_left_right(n1, n2, gap=gap, layout=layout) + else: + self.add_rel_top_bot(n1, n2, gap=gap, layout=layout) + + def add_align( + self, *nodes: ID_or_OBJECT, horizontal: bool, layout: dict | None = None + ): + if not layout: + layout = self.layout + direction = "horizontal" if horizontal else "vertical" + nodes = tuple(n for n in nodes if not self.is_filtered(n)) + if len(nodes) <= 1: + return + + # if all(isinstance(n, GraphInterface) for n in nodes): + # print(f"align {direction}: {nodes}") + + if "alignmentConstraint" not in layout: + layout["alignmentConstraint"] = {} + if direction not in layout["alignmentConstraint"]: + layout["alignmentConstraint"][direction] = [] + + align = cast_assert(dict, layout["alignmentConstraint"]) + align[direction].append([self.id_of(n) for n in nodes]) + + def add_same_height[T: Node]( + self, + nodes: Iterable[T], + gif_key: Callable[[T], GraphInterface], + layout: dict | None = None, + ): + if not layout: + layout = self.layout + self.add_align(*(gif_key(n) for n in nodes), horizontal=True, layout=layout) + + def set_type(self, t: str, layout: dict | None = None): + if not layout: + layout = self.layout + if t == "fcose" or t is None: + _layout = { + "name": "fcose", + # 'draft', 'default' or 'proof' + # - "draft" only applies spectral layout + # - "default" improves the quality with incremental layout + # (fast cooling rate) + # - "proof" improves the quality with incremental layout + # (slow cooling rate) + "quality": "proof", + # Whether or not to animate the layout + "animate": False, + # Use random node positions at beginning of layout + # if this is set to false, + # then quality option must be "proof" + "randomize": False, + # Fit the viewport to the repositioned nodes + "fit": True, + # Padding around layout + "padding": 50, + # Whether to include labels in node dimensions. + # Valid in "proof" quality + "nodeDimensionsIncludeLabels": True, + # Whether or not simple nodes (non-compound nodes) + # are of uniform dimensions + "uniformNodeDimensions": True, + # Whether to pack disconnected components - + # cytoscape-layout-utilities extension should + # be registered and initialized + "packComponents": False, # Graph is never disconnected + # Node repulsion (non overlapping) multiplier + "nodeRepulsion": 100, + # Ideal edge (non nested) length + "idealEdgeLength": 100, + # Divisor to compute edge forces + "edgeElasticity": 0.2, + # Nesting factor (multiplier) to compute ideal edge length + # for nested edges + "nestingFactor": 0.0001, + # Maximum number of iterations to perform - + # this is a suggested value and might be adjusted by the + # algorithm as required + "numIter": 2500 * 4, + # For enabling tiling + "tile": False, # No unconnected nodes in Graph + # Gravity force (constant) + "gravity": 0, + # Gravity range (constant) + "gravityRange": 3.8, + # Gravity force (constant) for compounds + "gravityCompound": 20, + # Gravity range (constant) for compounds + "gravityRangeCompound": 0.5, + # Initial cooling factor for incremental layout + "initialEnergyOnIncremental": 0.5, + "componentSpacing": 40, + } + elif t == "dagre": + _layout = { + "name": "dagre", + } + else: + raise ValueError(f"Unknown layout: {t}") + + layout.clear() + layout.update(_layout) + + +def buttons(layout: Layout): + app = layout.app + html_controls = html.Div( + className="controls", + style={"padding": "10px", "background-color": "#f0f0f0"}, + children=[ + # html.Label("Node Repulsion:"), + # dcc.Slider( + # id="node-repulsion-slider", + # min=500, + # max=5000, + # step=100, + # value=1000, + # marks={i: str(i) for i in range(500, 5001, 500)}, + # ), + # html.Label("Edge Elasticity:"), + # dcc.Slider( + # id="edge-elasticity-slider", + # min=0, + # max=1, + # step=0.05, + # value=0.45, + # marks={i / 10: str(i / 10) for i in range(0, 11, 1)}, + # ), + dcc.RadioItems( + id="layout-radio", + options=[ + {"label": "fcose", "value": "fcose"}, + {"label": "dagre", "value": "dagre"}, + ], + ), + dcc.RadioItems( + id="layout-dagre-ranker", + options=[ + {"label": "network-simplex", "value": "network-simplex"}, + {"label": "tight-tree", "value": "tight-tree"}, + {"label": "longest-path", "value": "longest-path"}, + ], + ), + dcc.Checklist( + id="layout-checkbox", + options=[{"label": "Parameters", "value": "parameters"}], + ), + html.Button("Apply Changes", id="apply-changes-button"), + ], + ) + layout.div_children.insert(-2, html_controls) + + @app.callback( + Output("graph-view", "layout"), + Input("apply-changes-button", "n_clicks"), + State("layout-checkbox", "value"), + State("layout-radio", "value"), + State("layout-dagre-ranker", "value"), + State("graph-view", "layout"), + ) + def absolute_layout( + n_clicks, layout_checkbox, layout_radio, layout_dagre_ranker, current_layout + ): + print(layout_checkbox, layout_radio, layout_dagre_ranker) + layout.set_type(layout_radio, current_layout) + + if layout_radio == "fcose": + layout_constraints(layout, current_layout) + + if "parameters" in (layout_checkbox or []): + params_top(layout, current_layout) + + if layout_dagre_ranker: + current_layout["ranker"] = layout_dagre_ranker + + return current_layout + + +def params_top(layout: Layout, _layout: dict | None = None): + params = layout.nodes_of_type(Parameter) + expressions = layout.nodes_of_type(Expression) + predicates = layout.nodes_of_type(Predicate) + non_predicate_expressions = expressions - predicates + + def depth(expr: Expression) -> int: + operates_on = expressions & { + e.node + for e, li in expr.operates_on.edges.items() + if not isinstance(li, LinkSibling) and e.node is not expr + } + + # direct parameter or constants only + if not operates_on: + return 1 + return 1 + max(depth(o) for o in operates_on) + + expressions_by_depth = groupby(non_predicate_expressions, depth) + + def same_height[T: Parameter | Expression](nodes: Iterable[T]): + layout.add_same_height(nodes, lambda pe: pe.self_gif, layout=_layout) + layout.add_same_height(nodes, lambda pe: pe.operated_on, layout=_layout) + + # All params same height + same_height(params) + + # predicates same height + same_height(predicates) + + for _, exprs in expressions_by_depth.items(): + same_height(exprs) + + # predicate expressions below other expressions + if predicates: + for expr in non_predicate_expressions: + layout.add_rel_top_bot( + expr.operates_on, next(iter(predicates)).self_gif, gap=200 + ) + + # Expressions below params + if params: + for expr in expressions: + layout.add_rel_top_bot( + next(iter(params)).operated_on, expr.self_gif, gap=200 + ) + + # Expression tree + for expr in expressions: + operates_on = (params | expressions) & { + e.node + for e, li in expr.operates_on.edges.items() + if not isinstance(li, LinkSibling) and e.node is not expr + } + for o in operates_on: + layout.add_rel_top_bot(o.operated_on, expr.self_gif, gap=100) + + +def layout_constraints(layout: Layout, _layout: dict | None = None): + for n in layout.nodes: + # only to save on some printing + if layout.is_filtered(n.self_gif): + continue + + siblings = { + o + for o, li in n.self_gif.edges.items() + if isinstance(li, LinkSibling) and not layout.is_filtered(o) + } + + # siblings below self + for o in siblings: + layout.add_rel_top_bot(n.self_gif, o, gap=50, layout=_layout) + + # siblings on same level within node + layout.add_align(*siblings, horizontal=True, layout=_layout) + + order = list(sorted(siblings, key=lambda o: o.name)) + middle_i = len(order) // 2 + if len(siblings) % 2 == 1: + # sibling directly below self + layout.add_align( + n.self_gif, order[middle_i], horizontal=False, layout=_layout + ) + order.pop(middle_i) + + # self inbetween siblings + order.insert(middle_i, n.self_gif) + layout.add_order(*order, horizontal=True, gap=25, layout=_layout) + + +# -------------------------------------------------------------------------------------- + + +def interactive_subgraph( + edges: Iterable[tuple[GraphInterface, GraphInterface, Link]], + gifs: list[GraphInterface], + nodes: Iterable[Node], + height: int | None = None, +): + links = [link for _, _, link in edges] + link_types = {typename(link) for link in links} + gif_types = {typename(gif) for gif in gifs} + + def node_has_parent_in_graph(node: Node) -> bool: + p = node.get_parent() + if not p: + return False + return p[0] in nodes + + elements = ( + [_gif(gif) for gif in gifs] + + [_link(*edge) for edge in edges] + + [_group(node, root=not node_has_parent_in_graph(node)) for node in nodes] + ) + + # Build stylesheet + stylesheet = _Stylesheet() + + gif_type_colors = list(_with_pastels(gif_types)) + link_type_colors = list(_with_pastels(link_types)) + group_types_colors = [ + (typename(group_type), color) for group_type, color in _GROUP_TYPES.items() + ] + + for gif_type, color in gif_type_colors: + stylesheet.add_node_type(gif_type, color) + + for link_type, color in link_type_colors: + stylesheet.add_link_type(link_type, color) + + for group_type, color in group_types_colors: + stylesheet.add_group_type(group_type, color) + + # Register the fcose layout + cyto.load_extra_layouts() + app = Dash(__name__) + app.layout = _Layout(stylesheet, elements) + + # Extra layouting + layout = Layout(app, elements, list(nodes)) + buttons(layout) + + # Print legend --------------------------------------------------------------------- + console = Console() + + for typegroup, colors in [ + ("GIF", gif_type_colors), + ("Link", link_type_colors), + ("Node", group_types_colors), + ]: + table = Table(title="Legend") + table.add_column("Type", style="cyan") + table.add_column("Color", style="green") + table.add_column("Name") + + for text, color in colors: + table.add_row(typegroup, f"[on {color}] [/]", text) + + console.print(table) + + # Run ------------------------------------------------------------------------------ + + app.run(jupyter_height=height or 1000) + + +def interactive_graph( + G: Graph, + node_types: tuple[type[Node], ...] | None = None, + depth: int = 0, + filter_unconnected: bool = True, + height: int | None = None, +): + if node_types is None: + node_types = (Node,) + + # Build elements + nodes = GraphFunctions(G).nodes_of_types(node_types) + if depth > 0: + nodes = [node for node in nodes if len(node.get_hierarchy()) <= depth] + + gifs = {gif for gif in G.get_gifs() if gif.node in nodes} + if filter_unconnected: + gifs = [gif for gif in gifs if len(gif.edges.keys() & gifs) > 1] + + edges = [ + (edge[0], edge[1], edge[2]) + for edge in G.edges + if edge[0] in gifs and edge[1] in gifs + ] + return interactive_subgraph(edges, list(gifs), nodes, height=height) diff --git a/src/faebryk/library/ANY.py b/src/faebryk/library/ANY.py deleted file mode 100644 index fede5fb4..00000000 --- a/src/faebryk/library/ANY.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from faebryk.core.parameter import Parameter -from faebryk.libs.units import UnitsContainer - - -class ANY(Parameter): - """ - Allow parameter to take any value. - Operations with this parameter automatically resolve to ANY too. - Don't mistake with F.TBD. - """ - - def __eq__(self, __value: object) -> bool: - if isinstance(__value, ANY): - return True - - return False - - def __hash__(self) -> int: - return super().__hash__() - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - return "ANY" if required else "" diff --git a/src/faebryk/library/B0505S_1WR3.py b/src/faebryk/library/B0505S_1WR3.py index 03d54ee7..72cfda5f 100644 --- a/src/faebryk/library/B0505S_1WR3.py +++ b/src/faebryk/library/B0505S_1WR3.py @@ -63,15 +63,19 @@ def __preinit__(self): # ---------------------------------------- # parametrization # ---------------------------------------- - self.power_in.get_trait(F.can_be_decoupled).decouple().capacitance.merge( - F.Range.from_center_rel(4.7 * P.uF, 0.1) + self.power_in.get_trait( + F.can_be_decoupled + ).decouple().capacitance.constrain_subset( + L.Range.from_center_rel(4.7 * P.uF, 0.1) ) - self.power_out.get_trait(F.can_be_decoupled).decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.1) + self.power_out.get_trait( + F.can_be_decoupled + ).decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.1) ) # ---------------------------------------- # connections # ---------------------------------------- - self.power_in.voltage.merge(F.Range(4.3 * P.V, 9 * P.V)) - self.power_out.voltage.merge(F.Range.from_center(5 * P.V, 0.5 * P.V)) + self.power_in.voltage.constrain_subset(L.Range(4.3 * P.V, 9 * P.V)) + self.power_out.voltage.constrain_superset(L.Range.from_center_rel(5 * P.V, 0.1)) diff --git a/src/faebryk/library/BH1750FVI_TR.py b/src/faebryk/library/BH1750FVI_TR.py index ee13ff0e..3a151c04 100644 --- a/src/faebryk/library/BH1750FVI_TR.py +++ b/src/faebryk/library/BH1750FVI_TR.py @@ -13,12 +13,14 @@ class BH1750FVI_TR(Module): class _bh1750_esphome_config(F.has_esphome_config.impl()): - update_interval: F.TBD + update_interval = L.p_field( + units=P.s, + soft_set=L.Range(100 * P.ms, 1 * P.day), + guess=1 * P.s, + tolerance_guess=0, + ) def get_config(self) -> dict: - val = self.update_interval.get_most_narrow() - assert isinstance(val, F.Constant), "No update interval set!" - obj = self.obj assert isinstance(obj, BH1750FVI_TR) @@ -31,7 +33,7 @@ def get_config(self) -> dict: "name": "BH1750 Illuminance", "address": "0x23", "i2c_id": i2c.get_trait(F.is_esphome_bus).get_bus_id(), - "update_interval": f"{val.value.to('s')}", + "update_interval": self.update_interval, } ] } @@ -59,19 +61,25 @@ def set_address(self, addr: int): esphome_config: _bh1750_esphome_config def __preinit__(self): - self.dvi_capacitor.capacitance.merge(1 * P.uF) - self.dvi_resistor.resistance.merge(1 * P.kohm) + self.dvi_capacitor.capacitance.constrain_subset( + L.Range.from_center_rel(1 * P.uF, 0.1) + ) + self.dvi_resistor.resistance.constrain_subset( + L.Range.from_center_rel(1 * P.kohm, 0.1) + ) self.i2c.terminate() - self.i2c.frequency.merge( + self.i2c.frequency.constrain_le( F.I2C.define_max_frequency_capability(F.I2C.SpeedMode.fast_speed) ) # set constraints - self.power.voltage.merge(F.Range(2.4 * P.V, 3.6 * P.V)) + self.power.voltage.constrain_subset(L.Range(2.4 * P.V, 3.6 * P.V)) - self.power.decoupled.decouple().capacitance.merge(100 * P.nF) + self.power.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.1) + ) # TODO: self.dvi.low_pass(self.dvi_capacitor, self.dvi_resistor) self.dvi.signal.connect_via(self.dvi_capacitor, self.power.lv) self.dvi.signal.connect_via(self.dvi_resistor, self.power.hv) diff --git a/src/faebryk/library/BJT.py b/src/faebryk/library/BJT.py index f1935aef..a07d24b5 100644 --- a/src/faebryk/library/BJT.py +++ b/src/faebryk/library/BJT.py @@ -6,7 +6,6 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.core.node import rt_field -from faebryk.core.parameter import Parameter from faebryk.libs.library import L @@ -22,8 +21,8 @@ class OperationRegion(Enum): SATURATION = auto() CUT_OFF = auto() - doping_type: Parameter - operation_region: Parameter + doping_type = L.p_field(domain=L.Domains.ENUM(DopingType)) + operation_region = L.p_field(domain=L.Domains.ENUM(OperationRegion)) emitter: F.Electrical base: F.Electrical diff --git a/src/faebryk/library/Battery.py b/src/faebryk/library/Battery.py index c7157166..519caeb1 100644 --- a/src/faebryk/library/Battery.py +++ b/src/faebryk/library/Battery.py @@ -5,16 +5,25 @@ import faebryk.library._F as F import faebryk.libs.library.L as L from faebryk.core.module import Module +from faebryk.libs.units import P class Battery(Module): - voltage: F.TBD - capacity: F.TBD + voltage = L.p_field( + units=P.V, + soft_set=L.Range(0 * P.V, 100 * P.V), + likely_constrained=True, + ) + capacity = L.p_field( + units=P.Ah, + soft_set=L.Range(100 * P.mAh, 100 * P.Ah), + likely_constrained=True, + ) power: F.ElectricPower def __preinit__(self) -> None: - self.power.voltage.merge(self.voltage) + self.power.voltage.alias_is(self.voltage) @L.rt_field def single_electric_reference(self): diff --git a/src/faebryk/library/Button.py b/src/faebryk/library/Button.py index bd4a48fb..082b89ec 100644 --- a/src/faebryk/library/Button.py +++ b/src/faebryk/library/Button.py @@ -6,13 +6,19 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) class Button(Module): unnamed = L.list_field(2, F.Electrical) - height: F.TBD + height = L.p_field( + units=P.mm, + likely_constrained=False, + soft_set=L.Range(1 * P.mm, 10 * P.mm), + tolerance_guess=10 * P.percent, + ) designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.S diff --git a/src/faebryk/library/ButtonCell.py b/src/faebryk/library/ButtonCell.py index 9700fb3b..d27aa71b 100644 --- a/src/faebryk/library/ButtonCell.py +++ b/src/faebryk/library/ButtonCell.py @@ -5,7 +5,7 @@ from enum import IntEnum, StrEnum import faebryk.library._F as F -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import ParameterOperatable from faebryk.libs.library import L from faebryk.libs.units import P @@ -21,15 +21,15 @@ class Material(StrEnum): NickelMetalHydride = "H" @property - def voltage(self) -> Parameter: + def voltage(self) -> ParameterOperatable.NumberLike: return { - self.Alkaline: F.Constant(1.5 * P.V), - self.SilverOxide: F.Constant(1.55 * P.V), - self.ZincAir: F.Constant(1.65 * P.V), - self.Lithium: F.Constant(3.0 * P.V), - self.Mercury: F.Constant(1.35 * P.V), - self.NickelCadmium: F.Constant(1.2 * P.V), - self.NickelMetalHydride: F.Constant(1.2 * P.V), + self.Alkaline: 1.5 * P.V, + self.SilverOxide: 1.55 * P.V, + self.ZincAir: 1.65 * P.V, + self.Lithium: 3.0 * P.V, + self.Mercury: 1.35 * P.V, + self.NickelCadmium: 1.2 * P.V, + self.NickelMetalHydride: 1.2 * P.V, }[self] class Shape(StrEnum): @@ -53,9 +53,15 @@ class Size(IntEnum): N_2430 = 2430 N_2450 = 2450 - material: F.TBD - shape: F.TBD - size: F.TBD + material = L.p_field( + domain=L.Domains.ENUM(Material), + ) + shape = L.p_field( + domain=L.Domains.ENUM(Shape), + ) + size = L.p_field( + domain=L.Domains.ENUM(Size), + ) designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.B diff --git a/src/faebryk/library/CBM9002A_56ILG_ReferenceDesign.py b/src/faebryk/library/CBM9002A_56ILG_ReferenceDesign.py index 104d4898..0fcf60aa 100644 --- a/src/faebryk/library/CBM9002A_56ILG_ReferenceDesign.py +++ b/src/faebryk/library/CBM9002A_56ILG_ReferenceDesign.py @@ -24,12 +24,16 @@ def __preinit__(self): self.logic.pulled.pull(up=True) self.logic.signal.connect_via(self.cap, self.logic.reference.lv) - self.cap.capacitance.merge(F.Range.from_center_rel(1 * P.uF, 0.05)) + self.cap.capacitance.constrain_subset( + L.Range.from_center_rel(1 * P.uF, 0.05) + ) - self.diode.forward_voltage.merge(F.Range(715 * P.mV, 1.5 * P.V)) - self.diode.reverse_leakage_current.merge(F.Range.upper_bound(1 * P.uA)) - self.diode.current.merge(F.Range.from_center_rel(300 * P.mA, 0.05)) - self.diode.max_current.merge(F.Range.lower_bound(1 * P.A)) + self.diode.forward_voltage.constrain_subset(L.Range(715 * P.mV, 1.5 * P.V)) + self.diode.reverse_leakage_current.constrain_le(1 * P.uA) + self.diode.current.constrain_subset( + L.Range.from_center_rel(300 * P.mA, 0.05) + ) + self.diode.current.constrain_ge(1 * P.A) # ---------------------------------------- # modules, interfaces, parameters @@ -81,9 +85,9 @@ def __preinit__(self): # Parameters # ---------------------------------------- - self.oscillator.crystal.frequency.merge( - F.Range.from_center_rel(24 * P.Mhertz, 0.05) + self.oscillator.crystal.frequency.constrain_subset( + L.Range.from_center_rel(24 * P.Mhertz, 0.05) ) - self.oscillator.crystal.frequency_tolerance.merge( - F.Range(0 * P.ppm, 20 * P.ppm) + self.oscillator.crystal.frequency_tolerance.constrain_subset( + L.Range(0 * P.ppm, 20 * P.ppm) ) diff --git a/src/faebryk/library/CD4011.py b/src/faebryk/library/CD4011.py index cc2b3353..35506068 100644 --- a/src/faebryk/library/CD4011.py +++ b/src/faebryk/library/CD4011.py @@ -8,10 +8,7 @@ class CD4011(F.Logic74xx): def __init__(self): super().__init__( - [ - lambda: F.ElectricLogicGates.NAND(input_cnt=F.Constant(2)) - for _ in range(4) - ] + [lambda: F.ElectricLogicGates.NAND(input_cnt=2) for _ in range(4)] ) simple_value_representation = L.f_field(F.has_simple_value_representation_defined)( diff --git a/src/faebryk/library/CH340x.py b/src/faebryk/library/CH340x.py index 2d375d73..8b638dab 100644 --- a/src/faebryk/library/CH340x.py +++ b/src/faebryk/library/CH340x.py @@ -27,9 +27,9 @@ class CH340x(Module): def __preinit__(self): self.gpio_power.lv.connect(self.usb.usb_if.buspower.lv) - self.gpio_power.voltage.merge(F.Range(0 * P.V, 5.3 * P.V)) + self.gpio_power.voltage.constrain_subset(L.Range(0 * P.V, 5.3 * P.V)) self.gpio_power.decoupled.decouple() - self.usb.usb_if.buspower.voltage.merge(F.Range(4 * P.V, 5.3 * P.V)) + self.usb.usb_if.buspower.voltage.constrain_subset(L.Range(4 * P.V, 5.3 * P.V)) self.usb.usb_if.buspower.decoupled.decouple() diff --git a/src/faebryk/library/CH342.py b/src/faebryk/library/CH342.py index 810643b7..91fec50d 100644 --- a/src/faebryk/library/CH342.py +++ b/src/faebryk/library/CH342.py @@ -25,8 +25,10 @@ class IntegratedLDO(Module): def __preinit__(self): F.ElectricLogic.connect_all_module_references(self, gnd_only=True) - self.power_out.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) - self.power_in.voltage.merge(F.Range(4 * P.V, 5.5 * P.V)) + self.power_out.voltage.constrain_superset( + L.Range.from_center_rel(3.3 * P.V, 0.1) + ) + self.power_in.voltage.constrain_subset(L.Range(4 * P.V, 5.5 * P.V)) @L.rt_field def bridge(self): @@ -124,8 +126,8 @@ def __preinit__(self): # ---------------------------------------- # parametrization # ---------------------------------------- - self.power_3v.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) - self.power_io.voltage.merge(F.Range(1.7 * P.V, 5.5 * P.V)) + self.power_3v.voltage.constrain_subset(L.Range.from_center_rel(3.3 * P.V, 0.1)) + self.power_io.voltage.constrain_subset(L.Range(1.7 * P.V, 5.5 * P.V)) # ---------------------------------------- # connections diff --git a/src/faebryk/library/CH344.py b/src/faebryk/library/CH344.py index e5d16b3c..bd8f6984 100644 --- a/src/faebryk/library/CH344.py +++ b/src/faebryk/library/CH344.py @@ -75,4 +75,4 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.power.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) + self.power.voltage.constrain_subset(L.Range.from_center_rel(3.3 * P.V, 0.1)) diff --git a/src/faebryk/library/CH344Q.py b/src/faebryk/library/CH344Q.py index 1b488dd7..26181376 100644 --- a/src/faebryk/library/CH344Q.py +++ b/src/faebryk/library/CH344Q.py @@ -72,7 +72,7 @@ def enable_hardware_flow_conrol(self): def descriptive_properties(self): return F.has_descriptive_properties_defined( { - DescriptiveProperties.manufacturer.value: "WCH", + DescriptiveProperties.manufacturer: "WCH", DescriptiveProperties.partno: "CH344Q", }, ) diff --git a/src/faebryk/library/CH344Q_ReferenceDesign.py b/src/faebryk/library/CH344Q_ReferenceDesign.py index 8a2d37c4..18f4b43f 100644 --- a/src/faebryk/library/CH344Q_ReferenceDesign.py +++ b/src/faebryk/library/CH344Q_ReferenceDesign.py @@ -87,7 +87,7 @@ def __preinit__(self): # ------------------------------------ self.usb_uart_converter.power.decoupled.decouple().specialize( F.MultiCapacitor(4) - ).set_equal_capacitance_each(F.Range.from_center_rel(100 * P.nF, 0.05)) + ).set_equal_capacitance_each(L.Range.from_center_rel(100 * P.nF, 0.05)) self.usb.usb_if.buspower.connect_via(self.ldo, pwr_3v3) self.usb.usb_if.d.connect(self.usb_uart_converter.usb) @@ -116,14 +116,12 @@ def __preinit__(self): # ------------------------------------ self.usb_uart_converter.enable_status_or_modem_signals() - self.oscillator.crystal.frequency.merge( - F.Range.from_center_rel(8 * P.MHz, 0.001) + self.oscillator.crystal.frequency.constrain_subset( + L.Range.from_center_rel(8 * P.MHz, 0.001) ) - self.oscillator.crystal.frequency_tolerance.merge( - F.Range.upper_bound(40 * P.ppm) - ) - self.oscillator.crystal.load_capacitance.merge( - F.Range.from_center(8 * P.pF, 10 * P.pF) + self.oscillator.crystal.frequency_tolerance.constrain_le(40 * P.ppm) + self.oscillator.crystal.load_capacitance.constrain_subset( + L.Range.from_center(8 * P.pF, 10 * P.pF) ) # TODO: should be property of crystal when picked self.oscillator.current_limiting_resistor.resistance.merge( F.Constant(0 * P.ohm) @@ -136,17 +134,19 @@ def __preinit__(self): F.Range.from_center_rel(100 * P.nF, 0.1) ) - self.usb.usb_if.buspower.max_current.merge( - F.Range.from_center_rel(500 * P.mA, 0.1) - ) + # self.usb.usb_if.buspower.max_current.constrain_subset( + # L.Range.from_center_rel(500 * P.mA, 0.1) + # ) - self.ldo.output_current.merge(F.Range.lower_bound(500 * P.mA)) - self.ldo.output_voltage.merge(F.Range.from_center_rel(3.3 * P.V, 0.05)) + self.ldo.output_current.constrain_ge(500 * P.mA) + self.ldo.output_voltage.constrain_subset( + L.Range.from_center_rel(3.3 * P.V, 0.05) + ) # reset lowpass - self.reset_lowpass.response.merge(F.Filter.Response.LOWPASS) - self.reset_lowpass.cutoff_frequency.merge( - F.Range.from_center_rel(100 * P.Hz, 0.1) + self.reset_lowpass.response.constrain_subset(F.Filter.Response.LOWPASS) + self.reset_lowpass.cutoff_frequency.constrain_subset( + L.Range.from_center_rel(100 * P.Hz, 0.1) ) # Specialize diff --git a/src/faebryk/library/Capacitor.py b/src/faebryk/library/Capacitor.py index c5847b2b..435dee67 100644 --- a/src/faebryk/library/Capacitor.py +++ b/src/faebryk/library/Capacitor.py @@ -7,6 +7,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P from faebryk.libs.util import join_if_non_empty logger = logging.getLogger(__name__) @@ -25,9 +26,21 @@ class TemperatureCoefficient(IntEnum): unnamed = L.list_field(2, F.Electrical) - capacitance: F.TBD - rated_voltage: F.TBD - temperature_coefficient: F.TBD + capacitance = L.p_field( + units=P.F, + likely_constrained=True, + soft_set=L.Range(100 * P.pF, 1 * P.F), + tolerance_guess=10 * P.percent, + ) + # Voltage at which the design may be damaged + max_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(10 * P.V, 100 * P.V), + ) + temperature_coefficient = L.p_field( + domain=L.Domains.ENUM(TemperatureCoefficient), + ) attach_to_footprint: F.can_attach_to_footprint_symmetrically designator_prefix = L.f_field(F.has_designator_prefix_defined)( @@ -43,7 +56,7 @@ def simple_value_representation(self): return F.has_simple_value_representation_based_on_params( ( self.capacitance, - self.rated_voltage, + self.max_voltage, self.temperature_coefficient, ), lambda c, v, t: join_if_non_empty( diff --git a/src/faebryk/library/Common_Mode_Filter.py b/src/faebryk/library/Common_Mode_Filter.py index 151e0b22..ba1ba4d4 100644 --- a/src/faebryk/library/Common_Mode_Filter.py +++ b/src/faebryk/library/Common_Mode_Filter.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -14,10 +15,26 @@ class Common_Mode_Filter(Module): coil_a: F.Inductor coil_b: F.Inductor - inductance: F.TBD - self_resonant_frequency: F.TBD - rated_current: F.TBD - dc_resistance: F.TBD + inductance = L.p_field( + units=P.H, + likely_constrained=True, + soft_set=L.Range(1 * P.µH, 10 * P.mH), + tolerance_guess=10 * P.percent, + ) + self_resonant_frequency = L.p_field( + units=P.Hz, + likely_constrained=True, + soft_set=L.Range(100 * P.Hz, 1 * P.MHz), + tolerance_guess=10 * P.percent, + ) + max_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(1 * P.A, 10 * P.A), + ) + dc_resistance = L.p_field( + units=P.Ω, + ) designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.FL @@ -28,7 +45,7 @@ def __preinit__(self): # parametrization # ---------------------------------------- for coil in [self.coil_a, self.coil_b]: - coil.inductance.merge(self.inductance) - coil.self_resonant_frequency.merge(self.self_resonant_frequency) - coil.rated_current.merge(self.rated_current) - coil.dc_resistance.merge(self.dc_resistance) + coil.inductance.alias_is(self.inductance) + coil.self_resonant_frequency.alias_is(self.self_resonant_frequency) + coil.max_current.alias_is(self.max_current) + coil.dc_resistance.alias_is(self.dc_resistance) diff --git a/src/faebryk/library/Comparator.py b/src/faebryk/library/Comparator.py index 03d6dbef..04ff6d75 100644 --- a/src/faebryk/library/Comparator.py +++ b/src/faebryk/library/Comparator.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P, quantity class Comparator(Module): @@ -14,12 +15,38 @@ class OutputType(Enum): PushPull = auto() OpenDrain = auto() - common_mode_rejection_ratio: F.TBD - input_bias_current: F.TBD - input_hysteresis_voltage: F.TBD - input_offset_voltage: F.TBD - propagation_delay: F.TBD - output_type: F.TBD + common_mode_rejection_ratio = L.p_field( + units=P.dB, + likely_constrained=True, + soft_set=L.Range(quantity(60, P.dB), quantity(120, P.dB)), + tolerance_guess=10 * P.percent, + ) + input_bias_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(1 * P.pA, 1 * P.µA), + tolerance_guess=20 * P.percent, + ) + input_hysteresis_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(1 * P.mV, 100 * P.mV), + tolerance_guess=15 * P.percent, + ) + input_offset_voltage = L.p_field( + units=P.V, + soft_set=L.Range(10 * P.µV, 10 * P.mV), + tolerance_guess=20 * P.percent, + ) + propagation_delay = L.p_field( + units=P.s, + soft_set=L.Range(10 * P.ns, 1 * P.ms), + tolerance_guess=15 * P.percent, + ) + output_type = L.p_field( + domain=L.Domains.ENUM(OutputType), + likely_constrained=True, + ) power: F.ElectricPower inverting_input: F.Electrical diff --git a/src/faebryk/library/Constant.py b/src/faebryk/library/Constant.py deleted file mode 100644 index 14059b90..00000000 --- a/src/faebryk/library/Constant.py +++ /dev/null @@ -1,129 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from enum import Enum -from typing import Self, SupportsAbs - -import numpy as np - -from faebryk.core.parameter import Parameter, _resolved -from faebryk.libs.units import Quantity, UnitsContainer, to_si_str -from faebryk.libs.util import once - - -class Constant(Parameter): - type LIT_OR_PARAM = Parameter.LIT_OR_PARAM - - def __init__(self, value: LIT_OR_PARAM) -> None: - super().__init__() - self.value = value - - def _pretty_val(self): - val = repr(self.value) - # TODO - if isinstance(self.value, Quantity): - val = f"{self.value:.2f#~P}" - return val - - def __str__(self) -> str: - return super().__str__() + f"({self._pretty_val()})" - - def __repr__(self): - return super().__repr__() + f"({self._pretty_val()})" - - @_resolved - def __eq__(self, other) -> bool: - if self is other: - return True - - if not isinstance(other, Constant): - return False - - try: - return np.allclose(self.value, other.value) - except (TypeError, np.exceptions.DTypePromotionError): - ... - - return self.value == other.value - - @once - def _hash_val(self): - # assert not isinstance(self.value, Parameter) - return hash(self.value) - - def __hash__(self) -> int: - if isinstance(self.value, Parameter): - return hash(self.value) - return self._hash_val() - - # comparison operators - @_resolved - def __le__(self, other) -> bool: - if isinstance(other, Constant): - if self == other: - return True - return self.value <= other.value - return other >= self.value - - @_resolved - def __lt__(self, other) -> bool: - if isinstance(other, Constant): - if self == other: - return False - return self.value < other.value - return other > self.value - - @_resolved - def __ge__(self, other) -> bool: - if isinstance(other, Constant): - if self == other: - return True - return self.value >= other.value - return other <= self.value - - @_resolved - def __gt__(self, other) -> bool: - if isinstance(other, Constant): - if self == other: - return False - return self.value > other.value - return other < self.value - - def __abs__(self): - assert isinstance(self.value, SupportsAbs) - return Constant(abs(self.value)) - - def __format__(self, format_spec): - return f"{super().__str__()}({format(self.value, format_spec)})" - - def copy(self) -> Self: - return type(self)(self.value) - - def unpack(self): - if isinstance(self.value, Constant): - return self.value.unpack() - - return self.value - - def __int__(self): - return int(self.value) - - @_resolved - def __contains__(self, other: Parameter) -> bool: - if not isinstance(other, Constant): - return False - return other.value == self.value - - def try_compress(self) -> Parameter: - if isinstance(self.value, Parameter): - return self.value - return super().try_compress() - - def _max(self): - return self.value - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - return to_si_str(self.value, unit) - - def _enum_parameter_representation(self, required: bool) -> str: - return self.value.name if isinstance(self.value, Enum) else str(self.value) diff --git a/src/faebryk/library/Crystal.py b/src/faebryk/library/Crystal.py index 378fc234..d1d03902 100644 --- a/src/faebryk/library/Crystal.py +++ b/src/faebryk/library/Crystal.py @@ -4,6 +4,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class Crystal(Module): @@ -16,13 +17,54 @@ class Crystal(Module): # ---------------------------------------- # parameters # ---------------------------------------- - frequency: F.TBD - frequency_tolerance: F.TBD - frequency_temperature_tolerance: F.TBD - frequency_ageing: F.TBD - equivalent_series_resistance: F.TBD - shunt_capacitance: F.TBD - load_capacitance: F.TBD + frequency = L.p_field( + units=P.Hz, + likely_constrained=True, + soft_set=L.Range(32.768 * P.kHz, 100 * P.MHz), + tolerance_guess=50 * P.ppm, + ) + + frequency_tolerance = L.p_field( + units=P.ppm, + likely_constrained=True, + soft_set=L.Range(10 * P.ppm, 100 * P.ppm), + tolerance_guess=10 * P.percent, + ) + + frequency_temperature_tolerance = L.p_field( + units=P.ppm, + likely_constrained=True, + soft_set=L.Range(1 * P.ppm, 50 * P.ppm), + tolerance_guess=10 * P.percent, + ) + + frequency_ageing = L.p_field( + units=P.ppm, + likely_constrained=True, + soft_set=L.Range(1 * P.ppm, 10 * P.ppm), + tolerance_guess=20 * P.percent, + ) + + equivalent_series_resistance = L.p_field( + units=P.Ω, + likely_constrained=True, + soft_set=L.Range(10 * P.Ω, 200 * P.Ω), + tolerance_guess=10 * P.percent, + ) + + shunt_capacitance = L.p_field( + units=P.F, + likely_constrained=True, + soft_set=L.Range(1 * P.pF, 10 * P.pF), + tolerance_guess=20 * P.percent, + ) + + load_capacitance = L.p_field( + units=P.F, + likely_constrained=True, + soft_set=L.Range(8 * P.pF, 30 * P.pF), + tolerance_guess=10 * P.percent, + ) # ---------------------------------------- # traits diff --git a/src/faebryk/library/Crystal_Oscillator.py b/src/faebryk/library/Crystal_Oscillator.py index fe6cac08..87ddacff 100644 --- a/src/faebryk/library/Crystal_Oscillator.py +++ b/src/faebryk/library/Crystal_Oscillator.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: MIT -from copy import copy - import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L @@ -25,15 +23,15 @@ class Crystal_Oscillator(Module): # ---------------------------------------- # https://blog.adafruit.com/2012/01/24/choosing-the-right-crystal-and-caps-for-your-design/ # http://www.st.com/internet/com/TECHNICAL_RESOURCES/TECHNICAL_LITERATURE/APPLICATION_NOTE/CD00221665.pdf - _STRAY_CAPACITANCE = F.Range(1 * P.pF, 5 * P.pF) + _STRAY_CAPACITANCE = L.Range(1 * P.pF, 5 * P.pF) @L.rt_field def capacitance(self): - return (self.crystal.load_capacitance - copy(self._STRAY_CAPACITANCE)) * 2 + return (self.crystal.load_capacitance - self._STRAY_CAPACITANCE) * 2 def __preinit__(self): for cap in self.capacitors: - cap.capacitance.merge(self.capacitance) + cap.capacitance.alias_is(self.capacitance) self.current_limiting_resistor.allow_removal_if_zero() diff --git a/src/faebryk/library/DifferentialPair.py b/src/faebryk/library/DifferentialPair.py index c885c27f..d155545a 100644 --- a/src/faebryk/library/DifferentialPair.py +++ b/src/faebryk/library/DifferentialPair.py @@ -5,19 +5,26 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface +from faebryk.libs.library import L +from faebryk.libs.units import P class DifferentialPair(ModuleInterface): p: F.SignalElectrical n: F.SignalElectrical - impedance: F.TBD + impedance = L.p_field( + units=P.Ω, + likely_constrained=True, + soft_set=L.Range(10 * P.Ω, 100 * P.Ω), + tolerance_guess=10 * P.percent, + ) def terminated(self) -> Self: terminated_bus = type(self)() rs = terminated_bus.add_to_container(2, F.Resistor) for r in rs: - r.resistance.merge(self.impedance) + r.resistance.alias_is(self.impedance) terminated_bus.p.signal.connect_via(rs[0], self.p.signal) terminated_bus.n.signal.connect_via(rs[1], self.n.signal) diff --git a/src/faebryk/library/Diode.py b/src/faebryk/library/Diode.py index 89c9db3e..2b9d0ef4 100644 --- a/src/faebryk/library/Diode.py +++ b/src/faebryk/library/Diode.py @@ -3,16 +3,44 @@ import faebryk.library._F as F from faebryk.core.module import Module -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import ParameterOperatable from faebryk.libs.library import L +from faebryk.libs.units import P class Diode(Module): - forward_voltage: F.TBD - max_current: F.TBD - current: F.TBD - reverse_working_voltage: F.TBD - reverse_leakage_current: F.TBD + forward_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(0.1 * P.V, 1 * P.V), + tolerance_guess=10 * P.percent, + ) + # Current at which the design is functional + current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(0.1 * P.mA, 10 * P.A), + tolerance_guess=10 * P.percent, + ) + reverse_working_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(10 * P.V, 100 * P.V), + tolerance_guess=10 * P.percent, + ) + reverse_leakage_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(0.1 * P.nA, 1 * P.µA), + tolerance_guess=10 * P.percent, + ) + # Current at which the design may be damaged + # In some cases, this is useful to know, e.g. to calculate the brightness of an LED + max_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(0.1 * P.mA, 10 * P.A), + ) anode: F.Electrical cathode: F.Electrical @@ -43,7 +71,10 @@ def pin_association_heuristic(self): case_sensitive=False, ) + def __preinit__(self): + self.current.constrain_le(self.max_current) + def get_needed_series_resistance_for_current_limit( - self, input_voltage_V: Parameter - ) -> Parameter: + self, input_voltage_V: ParameterOperatable + ): return (input_voltage_V - self.forward_voltage) / self.current diff --git a/src/faebryk/library/Diodes_Incorporated_AP2552W6_7.py b/src/faebryk/library/Diodes_Incorporated_AP2552W6_7.py index 37ccdf1d..c177e90f 100644 --- a/src/faebryk/library/Diodes_Incorporated_AP2552W6_7.py +++ b/src/faebryk/library/Diodes_Incorporated_AP2552W6_7.py @@ -4,8 +4,8 @@ import logging import faebryk.library._F as F # noqa: F401 -from faebryk.core.module import Module, ModuleException -from faebryk.core.parameter import Parameter +from faebryk.core.module import Module +from faebryk.core.parameter import ParameterOperatable from faebryk.exporters.pcb.layout.extrude import LayoutExtrude from faebryk.exporters.pcb.layout.heuristic_decoupling import Params from faebryk.exporters.pcb.layout.next_to import LayoutNextTo @@ -26,8 +26,8 @@ class Diodes_Incorporated_AP2552W6_7(Module): """ @assert_once - def set_current_limit(self, current: Parameter) -> F.Resistor: - self.current_limit.merge(current) + def set_current_limit(self, current: ParameterOperatable.NumberLike) -> F.Resistor: + self.current_limit.alias_is(current) current_limit_setting_resistor = self.ilim.add(F.Resistor()) @@ -42,16 +42,14 @@ def set_current_limit(self, current: Parameter) -> F.Resistor: # Rlim_max = (20.08 / (self.current_limit * P.mA)) ^ (1 / 0.904) * P.kohm # Rlim = Range(Rlim_min, Rlim_max) - Rlim = F.Range.from_center_rel( - 51 * P.kohm, 0.01 - ) # TODO: remove: ~0.52A typical current limit - if not Rlim.is_subset_of(F.Range(10 * P.kohm, 210 * P.kohm)): - raise ModuleException( - self, - f"Rlim must be in the range 10kOhm to 210kOhm but is {Rlim.get_most_narrow()}", # noqa: E501 - ) + # Rlim = F.Constant(51 * P.kohm) # TODO: remove: ~0.52A typical current limit + # if not Rlim.is_subset_of(L.Range(10 * P.kohm, 210 * P.kohm)): + # raise ModuleException( + # self, + # f"Rlim must be in the range 10kOhm to 210kOhm but is {Rlim.get_most_narrow()}", # noqa: E501 + # ) - current_limit_setting_resistor.resistance.merge(Rlim) + # current_limit_setting_resistor.resistance.constrain_subset(Rlim) return current_limit_setting_resistor @@ -64,7 +62,12 @@ def set_current_limit(self, current: Parameter) -> F.Resistor: fault: F.ElectricLogic ilim: F.SignalElectrical - current_limit: F.TBD + current_limit = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(100 * P.mA, 2.1 * P.A), + tolerance_guess=10 * P.percent, + ) # ---------------------------------------- # traits # ---------------------------------------- diff --git a/src/faebryk/library/EEPROM.py b/src/faebryk/library/EEPROM.py index f7ecbf51..156e957d 100644 --- a/src/faebryk/library/EEPROM.py +++ b/src/faebryk/library/EEPROM.py @@ -4,6 +4,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class EEPROM(Module): @@ -24,7 +25,12 @@ def set_address(self, addr: int): # modules, interfaces, parameters # ---------------------------------------- - memory_size: F.TBD + memory_size = L.p_field( + units=P.bit, + likely_constrained=True, + domain=L.Domains.Numbers.NATURAL(), + soft_set=L.Range(128 * P.bit, 1024 * P.kbit), + ) power: F.ElectricPower i2c: F.I2C diff --git a/src/faebryk/library/ESP32_C3.py b/src/faebryk/library/ESP32_C3.py index 4355b52d..8078c7cf 100644 --- a/src/faebryk/library/ESP32_C3.py +++ b/src/faebryk/library/ESP32_C3.py @@ -42,7 +42,7 @@ def __preinit__(self): # https://www.espressif.com/sites/default/files/documentation/esp32-c3_technical_reference_manual_en.pdf#uart for ser in x.uart: - ser.baud.merge(F.Range(0 * P.baud, 5000000 * P.baud)) + ser.baud.constrain_le(5 * P.mbaud) # connect all logic references # TODO: set correctly for each power domain @@ -51,12 +51,14 @@ def __preinit__(self): # set power domain constraints to recommended operating conditions for power_domain in [self.vdd3p3_rtc, self.vdd3p3, self.vdda]: - power_domain.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) - self.vdd3p3_cpu.voltage.merge( - F.Range(3.0 * P.V, 3.6 * P.V) + power_domain.voltage.constrain_subset( + L.Range.from_center(3.3 * P.V, 0.3 * P.V) + ) + self.vdd3p3_cpu.voltage.constrain_subset( + L.Range(3.0 * P.V, 3.6 * P.V) ) # TODO: max 3.3V when writing eFuses - self.vdd_spi.voltage.merge( - F.Range.from_center(3.3 * P.V, 0.3 * P.V) + self.vdd_spi.voltage.constrain_subset( + L.Range.from_center(3.3 * P.V, 0.3 * P.V) ) # TODO: when configured as input # connect all grounds to eachother and power @@ -172,7 +174,7 @@ def __preinit__(self): # ] # ] # + [ - # F.Range(10 * P.khertz, 800 * P.khertz) + # L.Range(10 * P.khertz, 800 * P.khertz) # ], # TODO: should be range 200k-800k, but breaks parameter merge # ) # ) @@ -211,8 +213,14 @@ def set_default_boot_mode(self, default_boot_to_spi_flash: bool = True): # set default boot mode to "SPI Boot mode" # https://www.espressif.com/sites/default/files/documentation/esp32-c3_datasheet_en.pdf page 26 # noqa E501 # TODO: make configurable - self.gpio[8].pulled.pull(up=True).resistance.merge(10 * P.kohm) - self.gpio[2].pulled.pull(up=True).resistance.merge(10 * P.kohm) + self.gpio[8].pulled.pull(up=True).resistance.constrain_subset( + L.Range.from_center_rel(10 * P.kohm, 0.1) + ) + self.gpio[2].pulled.pull(up=True).resistance.constrain_subset( + L.Range.from_center_rel(10 * P.kohm, 0.1) + ) # gpio[9] has an internal pull-up at boot = SPI-Boot if not default_boot_to_spi_flash: - self.gpio[9].pulled.pull(up=False).resistance.merge(10 * P.kohm) + self.gpio[9].pulled.pull(up=False).resistance.constrain_subset( + L.Range.from_center_rel(10 * P.kohm, 0.1) + ) diff --git a/src/faebryk/library/ESP32_C3_MINI_1.py b/src/faebryk/library/ESP32_C3_MINI_1.py index 0b4000bb..f19b096d 100644 --- a/src/faebryk/library/ESP32_C3_MINI_1.py +++ b/src/faebryk/library/ESP32_C3_MINI_1.py @@ -36,8 +36,8 @@ def single_electric_reference(self): def __preinit__(self): # connect power decoupling caps - self.vdd3v3.decoupled.decouple().capacitance.merge( - F.Range(100 * P.nF, 10 * P.uF) + self.vdd3v3.decoupled.decouple().capacitance.constrain_subset( + L.Range(100 * P.nF, 10 * P.uF) ) e = self.esp32_c3 diff --git a/src/faebryk/library/ESP32_C3_MINI_1_ReferenceDesign.py b/src/faebryk/library/ESP32_C3_MINI_1_ReferenceDesign.py index fcc1ff14..646d7af8 100644 --- a/src/faebryk/library/ESP32_C3_MINI_1_ReferenceDesign.py +++ b/src/faebryk/library/ESP32_C3_MINI_1_ReferenceDesign.py @@ -5,6 +5,7 @@ import faebryk.library._F as F from faebryk.core.module import Module +from faebryk.libs.library import L from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -23,8 +24,10 @@ def __preinit__(self): self.lp_filter.in_.signal.connect_via( self.button, self.logic_out.reference.lv ) - self.lp_filter.cutoff_frequency.merge(F.Range(100 * P.Hz, 200 * P.Hz)) - self.lp_filter.response.merge(F.Filter.Response.LOWPASS) + self.lp_filter.cutoff_frequency.constrain_subset( + L.Range(100 * P.Hz, 200 * P.Hz) + ) + self.lp_filter.response.constrain_subset(F.Filter.Response.LOWPASS) esp32_c3_mini_1: F.ESP32_C3_MINI_1 boot_switch: DebouncedButton @@ -88,9 +91,9 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.low_speed_crystal_clock.crystal.frequency.merge( - F.Range.from_center_rel(32.768 * P.kHz, 0.001) + self.low_speed_crystal_clock.crystal.frequency.constrain_subset( + L.Range.from_center_rel(32.768 * P.kHz, 0.001) ) - self.low_speed_crystal_clock.crystal.frequency_tolerance.merge( - F.Range(0 * P.ppm, 20 * P.ppm) + self.low_speed_crystal_clock.crystal.frequency_tolerance.constrain_subset( + L.Range(0 * P.ppm, 20 * P.ppm) ) diff --git a/src/faebryk/library/ElectricLogic.py b/src/faebryk/library/ElectricLogic.py index fc2b6aae..1a1cb673 100644 --- a/src/faebryk/library/ElectricLogic.py +++ b/src/faebryk/library/ElectricLogic.py @@ -89,7 +89,9 @@ class PushPull(Enum): # ---------------------------------------- # modules, interfaces, parameters # ---------------------------------------- - push_pull: F.TBD + push_pull = L.p_field( + domain=L.Domains.ENUM(PushPull), + ) # ---------------------------------------- # traits diff --git a/src/faebryk/library/ElectricLogicGates.py b/src/faebryk/library/ElectricLogicGates.py index 79a866ae..d4117605 100644 --- a/src/faebryk/library/ElectricLogicGates.py +++ b/src/faebryk/library/ElectricLogicGates.py @@ -7,19 +7,17 @@ class ElectricLogicGates(Namespace): class OR(F.ElectricLogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_or_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_or_gate()) class NOR(F.ElectricLogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_nor_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_nor_gate()) class NAND(F.ElectricLogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__( - input_cnt, F.Constant(1), F.LogicGate.can_logic_nand_gate() - ) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_nand_gate()) class XOR(F.ElectricLogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_xor_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_xor_gate()) diff --git a/src/faebryk/library/ElectricPower.py b/src/faebryk/library/ElectricPower.py index 6a8cebc8..e4a412aa 100644 --- a/src/faebryk/library/ElectricPower.py +++ b/src/faebryk/library/ElectricPower.py @@ -2,8 +2,6 @@ # SPDX-License-Identifier: MIT -import math - import faebryk.library._F as F from faebryk.core.node import Node from faebryk.libs.library import L @@ -21,9 +19,7 @@ def on_obj_set(self): def decouple(self): obj = self.get_obj(ElectricPower) return F.can_be_decoupled_defined.decouple(self).builder( - lambda c: c.rated_voltage.merge( - F.Range(obj.voltage * 2.0, math.inf * P.V) - ) + lambda c: c.max_voltage.constrain_ge(obj.voltage * 2.0) ) class can_be_surge_protected_power(F.can_be_surge_protected.impl()): @@ -35,15 +31,21 @@ def on_obj_set(self): def protect(self): obj = self.get_obj(ElectricPower) return [ - tvs.builder(lambda t: t.reverse_working_voltage.merge(obj.voltage)) + tvs.builder(lambda t: t.reverse_working_voltage.alias_is(obj.voltage)) for tvs in F.can_be_surge_protected_defined.protect(self) ] hv: F.Electrical lv: F.Electrical - voltage: F.TBD - max_current: F.TBD + voltage = L.p_field( + units=P.V, + likely_constrained=True, + domain=L.Domains.Numbers.REAL(), + soft_set=L.Range(0 * P.V, 1000 * P.V), + tolerance_guess=5 * P.percent, + ) + max_current = L.p_field(units=P.A) """ Only for this particular power interface Does not propagate to connections @@ -65,8 +67,10 @@ def fused(self, attach_to: Node | None = None): self.connect_shallow(fused_power) - fuse.trip_current.merge(F.Constant(self.max_current)) - # fused_power.max_current.merge(F.Range(0 * P.A, fuse.trip_current)) + fuse.trip_current.constrain_subset( + self.max_current * L.Range.from_center_rel(1.0, 0.1) + ) + fused_power.max_current.constrain_le(fuse.trip_current) if attach_to is not None: attach_to.add(fused_power) @@ -74,7 +78,8 @@ def fused(self, attach_to: Node | None = None): return fused_power def __preinit__(self) -> None: - # self.voltage.merge( + ... + # self.voltage.alias_is( # self.hv.potential - self.lv.potential # ) self.voltage.add( diff --git a/src/faebryk/library/Electrical.py b/src/faebryk/library/Electrical.py index 10d96819..57904184 100644 --- a/src/faebryk/library/Electrical.py +++ b/src/faebryk/library/Electrical.py @@ -1,12 +1,11 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface class Electrical(ModuleInterface): - potential: F.TBD + # potential= L.p_field(units=P.dimensionless) def get_net(self): from faebryk.library.Net import Net diff --git a/src/faebryk/library/Filter.py b/src/faebryk/library/Filter.py index 6fc33304..79324ff1 100644 --- a/src/faebryk/library/Filter.py +++ b/src/faebryk/library/Filter.py @@ -5,6 +5,8 @@ import faebryk.library._F as F from faebryk.core.module import Module +from faebryk.libs.library import L +from faebryk.libs.units import P class Filter(Module): @@ -15,9 +17,20 @@ class Response(Enum): BANDSTOP = auto() OTHER = auto() - cutoff_frequency: F.TBD - order: F.TBD - response: F.TBD + cutoff_frequency = L.p_field( + units=P.Hz, + likely_constrained=True, + domain=L.Domains.Numbers.REAL(), + soft_set=L.Range(0 * P.Hz, 1000 * P.Hz), + ) + order = L.p_field( + domain=L.Domains.Numbers.NATURAL(), + soft_set=L.Range(2, 10), + guess=2, + ) + response = L.p_field( + domain=L.Domains.ENUM(Response), + ) in_: F.Signal out: F.Signal diff --git a/src/faebryk/library/FilterElectricalLC.py b/src/faebryk/library/FilterElectricalLC.py index 322c275f..7eea7ff0 100644 --- a/src/faebryk/library/FilterElectricalLC.py +++ b/src/faebryk/library/FilterElectricalLC.py @@ -3,6 +3,8 @@ import math +from more_itertools import raise_ + import faebryk.library._F as F from faebryk.libs.library import L from faebryk.libs.units import P @@ -14,44 +16,35 @@ class FilterElectricalLC(F.Filter): capacitor: F.Capacitor inductor: F.Inductor - def __preinit__(self) -> None: ... - - @L.rt_field - def construction_dependency(self): - class _(F.has_construction_dependency.impl()): - def _construct(_self): - if F.Constant(F.Filter.Response.LOWPASS).is_subset_of(self.response): - self.response.merge(F.Filter.Response.LOWPASS) - - # TODO other orders - self.order.merge(2) - - L = self.inductor.inductance - C = self.capacitor.capacitance - fc = self.cutoff_frequency - - # TODO requires parameter constraint solving implemented - # fc.merge(1 / (2 * math.pi * math.sqrt(C * L))) + z0 = L.p_field(units=P.ohm) - # instead assume fc being the driving param - realistic_C = F.Range(1 * P.pF, 1 * P.mF) - L.merge(1 / ((2 * math.pi * fc) ** 2 * realistic_C)) - C.merge(1 / ((2 * math.pi * fc) ** 2 * L)) + def __preinit__(self) -> None: + Li = self.inductor.inductance + C = self.capacitor.capacitance + fc = self.cutoff_frequency - # TODO consider splitting C / L in a typical way + def build_lowpass(): + # TODO other orders & types + self.order.constrain_subset(2) + self.response.constrain_subset(F.Filter.Response.LOWPASS) - # low pass - self.in_.signal.connect_via( - (self.inductor, self.capacitor), - self.in_.reference.lv, - ) + fc.alias_is(1 / (2 * math.pi * (C * Li).operation_sqrt())) - self.in_.signal.connect_via(self.inductor, self.out.signal) - return + # low pass + self.in_.signal.connect_via( + (self.inductor, self.capacitor), + self.in_.reference.lv, + ) - if isinstance(self.response, F.Constant): - raise F.has_construction_dependency.NotConstructableEver() + self.in_.signal.connect_via(self.inductor, self.out.signal) + return - raise F.has_construction_dependency.NotConstructableYet() + ( + self.response.operation_is_subset(F.Filter.Response.LOWPASS) + & self.order.operation_is_subset(2) + ).if_then_else( + build_lowpass, + lambda: raise_(NotImplementedError()), + ) - return _() + # TODO add construction dependency trait diff --git a/src/faebryk/library/FilterElectricalRC.py b/src/faebryk/library/FilterElectricalRC.py index eab17126..ed45250f 100644 --- a/src/faebryk/library/FilterElectricalRC.py +++ b/src/faebryk/library/FilterElectricalRC.py @@ -4,6 +4,8 @@ import logging import math +from more_itertools import raise_ + import faebryk.library._F as F # noqa: F401 from faebryk.libs.library import L # noqa: F401 from faebryk.libs.units import P # noqa: F401 @@ -21,44 +23,34 @@ class FilterElectricalRC(F.Filter): capacitor: F.Capacitor resistor: F.Resistor - def __preinit__(self): ... - - @L.rt_field - def construction_dependency(self): - class _(F.has_construction_dependency.impl()): - def _construct(_self): - if F.Constant(F.Filter.Response.LOWPASS).is_subset_of(self.response): - self.response.merge(F.Filter.Response.LOWPASS) - - # TODO other orders - self.order.merge(1) - - R = self.resistor.resistance - C = self.capacitor.capacitance - fc = self.cutoff_frequency - - # TODO requires parameter constraint solving implemented - # fc.merge(1 / (2 * math.pi * R * C)) + z0 = L.p_field(units=P.ohm) - # instead assume fc being the driving param - realistic_C = F.Range(1 * P.pF, 1 * P.mF) - R.merge(1 / (2 * math.pi * realistic_C * fc)) - C.merge(1 / (2 * math.pi * R * fc)) + def __preinit__(self): + R = self.resistor.resistance + C = self.capacitor.capacitance + fc = self.cutoff_frequency - # TODO consider splitting C / L in a typical way + def build_lowpass(): + # TODO other orders, types + self.order.constrain_subset(1) + self.response.constrain_subset(F.Filter.Response.LOWPASS) - # low pass - self.in_.signal.connect_via( - (self.resistor, self.capacitor), - self.in_.reference.lv, - ) + fc.alias_is(1 / (2 * math.pi * R * C)) - self.in_.signal.connect_via(self.resistor, self.out.signal) - return + # low pass + self.in_.signal.connect_via( + (self.resistor, self.capacitor), + self.in_.reference.lv, + ) - if isinstance(self.response, F.Constant): - raise F.has_construction_dependency.NotConstructableEver() + self.in_.signal.connect_via(self.resistor, self.out.signal) - raise F.has_construction_dependency.NotConstructableYet() + ( + self.response.operation_is_subset(F.Filter.Response.LOWPASS) + & self.order.operation_is_subset(1) + ).if_then_else( + build_lowpass, + lambda: raise_(NotImplementedError()), + ) - return _() + # TODO add construction dependency trait diff --git a/src/faebryk/library/Fuse.py b/src/faebryk/library/Fuse.py index 2a5876e7..d8939f8e 100644 --- a/src/faebryk/library/Fuse.py +++ b/src/faebryk/library/Fuse.py @@ -7,6 +7,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -21,9 +22,18 @@ class ResponseType(Enum): FAST = auto() unnamed = L.list_field(2, F.Electrical) - fuse_type: F.TBD - response_type: F.TBD - trip_current: F.TBD + fuse_type = L.p_field( + domain=L.Domains.ENUM(FuseType), + ) + response_type = L.p_field( + domain=L.Domains.ENUM(ResponseType), + ) + trip_current = L.p_field( + units=P.A, + likely_constrained=True, + domain=L.Domains.Numbers.REAL(), + soft_set=L.Range(100 * P.mA, 100 * P.A), + ) attach_to_footprint: F.can_attach_to_footprint_symmetrically diff --git a/src/faebryk/library/GDT.py b/src/faebryk/library/GDT.py index 41b1766c..07b135ca 100644 --- a/src/faebryk/library/GDT.py +++ b/src/faebryk/library/GDT.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -15,8 +16,16 @@ class GDT(Module): tube_1: F.Electrical tube_2: F.Electrical - dc_breakdown_voltage: F.TBD - impulse_discharge_current: F.TBD + dc_breakdown_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(100 * P.V, 1000 * P.V), + ) + impulse_discharge_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(100 * P.mA, 100 * P.A), + ) @L.rt_field def can_bridge(self): diff --git a/src/faebryk/library/GenericBusProtection.py b/src/faebryk/library/GenericBusProtection.py index 00165803..dd4d98ed 100644 --- a/src/faebryk/library/GenericBusProtection.py +++ b/src/faebryk/library/GenericBusProtection.py @@ -63,7 +63,7 @@ def get_mifs[U: ModuleInterface]( for (power_unprotected, power_protected), fuse in zip(power, fuse): power_unprotected.hv.connect_via(fuse, power_protected.hv) # TODO maybe shallow connect? - power_protected.voltage.merge(power_unprotected.voltage) + power_protected.voltage.alias_is(power_unprotected.voltage) # TVS if self.bus_protected.has_trait(F.can_be_surge_protected): diff --git a/src/faebryk/library/HLK_LD2410B_P.py b/src/faebryk/library/HLK_LD2410B_P.py index 7ddea11e..fda83978 100644 --- a/src/faebryk/library/HLK_LD2410B_P.py +++ b/src/faebryk/library/HLK_LD2410B_P.py @@ -9,14 +9,15 @@ class HLK_LD2410B_P(Module): class _ld2410b_esphome_config(F.has_esphome_config.impl()): - throttle: F.TBD + throttle = L.p_field( + units=P.ms, + soft_set=L.Range(10 * P.ms, 1000 * P.ms), + tolerance_guess=0, + ) def get_config(self) -> dict: - val = self.throttle.get_most_narrow() - assert isinstance(val, F.Constant), "No update interval set!" - obj = self.obj - assert isinstance(obj, HLK_LD2410B_P), "This is not an HLK_LD2410B_P!" + assert isinstance(obj, HLK_LD2410B_P) uart_candidates = { mif @@ -34,7 +35,7 @@ def get_config(self) -> dict: return { "ld2410": { - "throttle": f"{val.value.to('ms')}", + "throttle": self.throttle, "uart_id": uart_cfg["id"], }, "binary_sensor": [ @@ -77,7 +78,7 @@ def attach_to_footprint(self): ) def __preinit__(self): - self.uart.baud.merge(F.Constant(256 * P.kbaud)) + self.uart.baud.constrain_le(256 * P.kbaud) # connect all logic references @L.rt_field diff --git a/src/faebryk/library/Header.py b/src/faebryk/library/Header.py index 642c26dc..3fc54512 100644 --- a/src/faebryk/library/Header.py +++ b/src/faebryk/library/Header.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P from faebryk.libs.util import times @@ -32,19 +33,32 @@ def __init__( self._vertical_pin_count = vertical_pin_count def __preinit__(self): - self.pin_count_horizonal.merge(self._horizontal_pin_count) - self.pin_count_vertical.merge(self._vertical_pin_count) - - pin_pitch: F.TBD - mating_pin_lenght: F.TBD - conection_pin_lenght: F.TBD - spacer_height: F.TBD - pin_type: F.TBD - pad_type: F.TBD - angle: F.TBD - - pin_count_horizonal: F.TBD - pin_count_vertical: F.TBD + self.pin_count_horizonal.alias_is(self._horizontal_pin_count) + self.pin_count_vertical.alias_is(self._vertical_pin_count) + + pin_pitch = L.p_field( + units=P.mm, + likely_constrained=True, + domain=L.Domains.Numbers.REAL(), + soft_set=L.Range(1 * P.mm, 10 * P.mm), + ) + pin_type = L.p_field( + domain=L.Domains.ENUM(PinType), + ) + pad_type = L.p_field( + domain=L.Domains.ENUM(PadType), + ) + angle = L.p_field( + domain=L.Domains.ENUM(Angle), + ) + pin_count_horizonal = L.p_field( + domain=L.Domains.Numbers.NATURAL(), + soft_set=L.Range(2, 100), + ) + pin_count_vertical = L.p_field( + domain=L.Domains.Numbers.NATURAL(), + soft_set=L.Range(2, 100), + ) @L.rt_field def contact(self): diff --git a/src/faebryk/library/I2C.py b/src/faebryk/library/I2C.py index b01ee3e7..6599e5fb 100644 --- a/src/faebryk/library/I2C.py +++ b/src/faebryk/library/I2C.py @@ -16,7 +16,11 @@ class I2C(ModuleInterface): scl: F.ElectricLogic sda: F.ElectricLogic - frequency: F.TBD + frequency = L.p_field( + units=P.Hz, + likely_constrained=True, + soft_set=L.Range(10 * P.kHz, 3.4 * P.MHz), + ) @L.rt_field def single_electric_reference(self): @@ -38,7 +42,7 @@ class SpeedMode(Enum): @staticmethod def define_max_frequency_capability(mode: SpeedMode): - return F.Range(I2C.SpeedMode.low_speed, mode) + return L.Range(I2C.SpeedMode.low_speed, mode) def __preinit__(self) -> None: self.frequency.add( diff --git a/src/faebryk/library/INA228.py b/src/faebryk/library/INA228.py index cfb19ac1..0cdb6367 100644 --- a/src/faebryk/library/INA228.py +++ b/src/faebryk/library/INA228.py @@ -110,4 +110,4 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.power.voltage.merge(F.Range(2.7 * P.V, 5.5 * P.V)) + self.power.voltage.constrain_subset(L.Range(2.7 * P.V, 5.5 * P.V)) diff --git a/src/faebryk/library/INA228_ReferenceDesign.py b/src/faebryk/library/INA228_ReferenceDesign.py index 6ec4e732..398067fd 100644 --- a/src/faebryk/library/INA228_ReferenceDesign.py +++ b/src/faebryk/library/INA228_ReferenceDesign.py @@ -33,10 +33,9 @@ def __init__(self, lowside: bool = False, filtered: bool = False): self._filtered = filtered def __preinit__(self): - self.power_in.voltage.merge( - self.power_out.voltage - ) # TODO: minus voltagedrop over shunt self.shunt_sense.p.signal.connect_via(self.shunt, self.shunt_sense.n.signal) + # TODO: minus voltagedrop over shunt + self.power_in.voltage.alias_is(self.power_out.voltage) if self._lowside: self.power_in.lv.connect_via(self.shunt, self.power_out.lv) self.power_in.hv.connect(self.power_out.hv) @@ -50,8 +49,8 @@ def __preinit__(self): # filter_cap = self.add(F.Capacitor()) # filter_resistors = L.list_field(2, F.Resistor) # - # filter_cap.capacitance.merge(F.Range.from_center_rel(0.1 * P.uF, 0.01)) - # filter_cap.rated_voltage.merge(F.Range.from_center_rel(170 * P.V, 0.01) + # filter_cap.capacitance.merge(L.Range.from_center_rel(0.1 * P.uF, 0.01)) + # filter_cap.max_voltage.merge(L.Range.from_center_rel(170 * P.V, 0.01) # for res in filter_resistors: # res.resistance.merge(10 * P.kohm) # TODO: auto calculate, see: https://www.ti.com/lit/ug/tidu473/tidu473.pdf @@ -83,8 +82,12 @@ def __preinit__(self): shunted_power = self.add( self.ShuntedElectricPower(lowside=self._lowside, filtered=self._filtered) ) - shunted_power.shunt.resistance.merge(F.Range.from_center_rel(15 * P.mohm, 0.01)) - shunted_power.shunt.rated_power.merge(F.Range.from_center_rel(2 * P.W, 0.01)) + shunted_power.shunt.resistance.constrain_subset( + L.Range.from_center_rel(15 * P.mohm, 0.01) + ) + shunted_power.shunt.max_power.constrain_subset( + L.Range.from_center_rel(2 * P.W, 0.01) + ) # TODO: calculate according to datasheet p36 # ---------------------------------------- @@ -98,6 +101,8 @@ def __preinit__(self): self.ina288.shunt_input.connect(shunted_power.shunt_sense) # decouple power rail - self.ina288.power.get_trait(F.can_be_decoupled).decouple().capacitance.merge( - F.Range.from_center_rel(0.1 * P.uF, 0.01) + self.ina288.power.get_trait( + F.can_be_decoupled + ).decouple().capacitance.constrain_subset( + L.Range.from_center_rel(0.1 * P.uF, 0.01) ) diff --git a/src/faebryk/library/ISO1540.py b/src/faebryk/library/ISO1540.py index 0541c6ce..97e16cdf 100644 --- a/src/faebryk/library/ISO1540.py +++ b/src/faebryk/library/ISO1540.py @@ -94,5 +94,5 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.non_iso.power.voltage.merge(F.Range(3.0 * P.V, 5.5 * P.V)) - self.iso.power.voltage.merge(F.Range(3.0 * P.V, 5.5 * P.V)) + self.non_iso.power.voltage.constrain_subset(L.Range(3.0 * P.V, 5.5 * P.V)) + self.iso.power.voltage.constrain_subset(L.Range(3.0 * P.V, 5.5 * P.V)) diff --git a/src/faebryk/library/ISO1540_ReferenceDesign.py b/src/faebryk/library/ISO1540_ReferenceDesign.py index 9033c095..574ca139 100644 --- a/src/faebryk/library/ISO1540_ReferenceDesign.py +++ b/src/faebryk/library/ISO1540_ReferenceDesign.py @@ -22,9 +22,9 @@ class ISO1540_ReferenceDesign(Module): # ---------------------------------------- def __preinit__(self): - self.isolator.non_iso.power.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.01) + self.isolator.non_iso.power.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.01) ) - self.isolator.iso.power.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.01) + self.isolator.iso.power.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.01) ) diff --git a/src/faebryk/library/Inductor.py b/src/faebryk/library/Inductor.py index 6b0b28cf..153bf834 100644 --- a/src/faebryk/library/Inductor.py +++ b/src/faebryk/library/Inductor.py @@ -5,16 +5,35 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P from faebryk.libs.util import join_if_non_empty class Inductor(Module): unnamed = L.list_field(2, F.Electrical) - inductance: F.TBD - self_resonant_frequency: F.TBD - rated_current: F.TBD - dc_resistance: F.TBD + inductance = L.p_field( + units=P.H, + likely_constrained=True, + soft_set=L.Range(100 * P.nH, 1 * P.H), + tolerance_guess=10 * P.percent, + ) + self_resonant_frequency = L.p_field( + units=P.Hz, + likely_constrained=True, + soft_set=L.Range(100 * P.kHz, 1 * P.GHz), + tolerance_guess=10 * P.percent, + ) + max_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(1 * P.mA, 100 * P.A), + ) + dc_resistance = L.p_field( + units=P.Ω, + soft_set=L.Range(10 * P.mΩ, 100 * P.Ω), + tolerance_guess=10 * P.percent, + ) @L.rt_field def can_bridge(self): @@ -28,17 +47,17 @@ def simple_value_representation(self): ( self.inductance, self.self_resonant_frequency, - self.rated_current, + self.max_current, self.dc_resistance, ), lambda inductance, self_resonant_frequency, - rated_current, + max_current, dc_resistance: join_if_non_empty( " ", inductance.as_unit_with_tolerance("H"), self_resonant_frequency.as_unit("Hz"), - rated_current.as_unit("A"), + max_current.as_unit("A"), dc_resistance.as_unit("Ω"), ), ) diff --git a/src/faebryk/library/LDO.py b/src/faebryk/library/LDO.py index e397c857..7963650c 100644 --- a/src/faebryk/library/LDO.py +++ b/src/faebryk/library/LDO.py @@ -1,13 +1,12 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import math from enum import Enum, auto import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L -from faebryk.libs.units import P +from faebryk.libs.units import P, quantity from faebryk.libs.util import assert_once, join_if_non_empty @@ -24,22 +23,49 @@ class OutputPolarity(Enum): POSITIVE = auto() NEGATIVE = auto() - max_input_voltage: F.TBD - output_voltage: F.TBD - output_polarity: F.TBD - output_type: F.TBD - output_current: F.TBD - psrr: F.TBD - dropout_voltage: F.TBD - quiescent_current: F.TBD - + max_input_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(1 * P.V, 100 * P.V), + ) + output_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(1 * P.V, 100 * P.V), + ) + quiescent_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(1 * P.mA, 100 * P.mA), + ) + dropout_voltage = L.p_field( + units=P.V, + likely_constrained=True, + soft_set=L.Range(1 * P.mV, 100 * P.mV), + ) + psrr = L.p_field( + units=P.dB, + likely_constrained=True, + soft_set=L.Range(quantity(1, P.dB), quantity(100, P.dB)), + ) + output_polarity = L.p_field( + domain=L.Domains.ENUM(OutputPolarity), + ) + output_type = L.p_field( + domain=L.Domains.ENUM(OutputType), + ) + output_current = L.p_field( + units=P.A, + likely_constrained=True, + soft_set=L.Range(1 * P.mA, 100 * P.mA), + ) enable: F.ElectricLogic power_in: F.ElectricPower power_out = L.d_field(lambda: F.ElectricPower().make_source()) def __preinit__(self): - self.max_input_voltage.merge(F.Range(self.power_in.voltage, math.inf * P.V)) - self.power_out.voltage.merge(self.output_voltage) + self.max_input_voltage.constrain_ge(self.power_in.voltage) + self.power_out.voltage.alias_is(self.output_voltage) self.enable.reference.connect(self.power_in) # TODO: should be implemented differently (see below) diff --git a/src/faebryk/library/LED.py b/src/faebryk/library/LED.py index ab13dcc2..b7dee584 100644 --- a/src/faebryk/library/LED.py +++ b/src/faebryk/library/LED.py @@ -5,7 +5,9 @@ from enum import Enum, auto import faebryk.library._F as F -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import ParameterOperatable +from faebryk.libs.library import L +from faebryk.libs.units import P class LED(F.Diode): @@ -39,23 +41,20 @@ class Color(Enum): ULTRA_VIOLET = auto() INFRA_RED = auto() - brightness: F.TBD - max_brightness: F.TBD - color: F.TBD + brightness = L.p_field(units=P.candela) + max_brightness = L.p_field(units=P.candela) + color = L.p_field(domain=L.Domains.ENUM(Color)) def __preinit__(self): - self.current.merge(self.brightness / self.max_brightness * self.max_current) + self.current.alias_is(self.brightness / self.max_brightness * self.max_current) + self.brightness.constrain_le(self.max_brightness) - # self.brightness.merge( - # F.Range(0 * P.millicandela, self.max_brightness) - # ) - - def set_intensity(self, intensity: Parameter) -> None: - self.brightness.merge(intensity * self.max_brightness) + def set_intensity(self, intensity: ParameterOperatable.NumberLike) -> None: + self.brightness.alias_is(intensity * self.max_brightness) def connect_via_current_limiting_resistor( self, - input_voltage: Parameter, + input_voltage: ParameterOperatable.NumberLike, resistor: F.Resistor, target: F.Electrical, low_side: bool, @@ -65,7 +64,7 @@ def connect_via_current_limiting_resistor( else: self.anode.connect_via(resistor, target) - resistor.resistance.merge( + resistor.resistance.alias_is( self.get_needed_series_resistance_for_current_limit(input_voltage), ) resistor.allow_removal_if_zero() diff --git a/src/faebryk/library/Logic.py b/src/faebryk/library/Logic.py index f15595b1..03ec2da5 100644 --- a/src/faebryk/library/Logic.py +++ b/src/faebryk/library/Logic.py @@ -10,4 +10,8 @@ class Logic(F.Signal): Acts as protocol, because multi inheritance is not supported """ - def set(self, on: bool): ... + # state = L.p_field(domain=L.Domains.BOOL()) + + def set(self, on: bool): + # self.state.constrain_subset(on) + ... diff --git a/src/faebryk/library/Logic74xx.py b/src/faebryk/library/Logic74xx.py index da09e054..c3af9a3c 100644 --- a/src/faebryk/library/Logic74xx.py +++ b/src/faebryk/library/Logic74xx.py @@ -50,7 +50,7 @@ class Family(Enum): CD4000 = auto() power: F.ElectricPower - logic_family: F.TBD + logic_family = L.p_field(domain=L.Domains.ENUM(Family)) designator = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.U diff --git a/src/faebryk/library/LogicGate.py b/src/faebryk/library/LogicGate.py index 11f01157..5d7c4432 100644 --- a/src/faebryk/library/LogicGate.py +++ b/src/faebryk/library/LogicGate.py @@ -49,8 +49,8 @@ def xor(self, *ins: F.Logic): def __init__( self, - input_cnt: F.Constant, - output_cnt: F.Constant, + input_cnt: int, + output_cnt: int, *functions: TraitImpl, ) -> None: super().__init__() diff --git a/src/faebryk/library/LogicGates.py b/src/faebryk/library/LogicGates.py index 53e18797..cbcfb7f1 100644 --- a/src/faebryk/library/LogicGates.py +++ b/src/faebryk/library/LogicGates.py @@ -7,19 +7,17 @@ class LogicGates(Namespace): class OR(F.LogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_or_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_or_gate()) class NOR(F.LogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_nor_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_nor_gate()) class NAND(F.LogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__( - input_cnt, F.Constant(1), F.LogicGate.can_logic_nand_gate() - ) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_nand_gate()) class XOR(F.LogicGate): - def __init__(self, input_cnt: F.Constant): - super().__init__(input_cnt, F.Constant(1), F.LogicGate.can_logic_xor_gate()) + def __init__(self, input_cnt: int): + super().__init__(input_cnt, 1, F.LogicGate.can_logic_xor_gate()) diff --git a/src/faebryk/library/M24C08_FMN6TP.py b/src/faebryk/library/M24C08_FMN6TP.py index 800439b6..dce1ff6e 100644 --- a/src/faebryk/library/M24C08_FMN6TP.py +++ b/src/faebryk/library/M24C08_FMN6TP.py @@ -43,10 +43,12 @@ def __preinit__(self): ) self.data.terminate() - self.power.decoupled.decouple().capacitance.merge( - F.Range(10 * P.nF, 100 * P.nF) + self.power.decoupled.decouple().capacitance.constrain_subset( + L.Range(10 * P.nF, 100 * P.nF) ) + self.power.voltage.constrain_subset(L.Range(1.7 * P.V, 5.5 * P.V)) + self.add( F.has_descriptive_properties_defined( { diff --git a/src/faebryk/library/ME6211C33M5G_N.py b/src/faebryk/library/ME6211C33M5G_N.py index 59d4bf5b..c7bdb0da 100644 --- a/src/faebryk/library/ME6211C33M5G_N.py +++ b/src/faebryk/library/ME6211C33M5G_N.py @@ -19,7 +19,7 @@ def __init__(self, default_enabled: bool = True) -> None: def __preinit__(self): # set constraints - self.output_voltage.merge(F.Range(3.3 * 0.98 * P.V, 3.3 * 1.02 * P.V)) + self.output_voltage.constrain_superset(L.Range.from_center_rel(3.3 * P.V, 0.02)) if self._default_enabled: self.enable.set(True) diff --git a/src/faebryk/library/MOSFET.py b/src/faebryk/library/MOSFET.py index d430b7e3..50575724 100644 --- a/src/faebryk/library/MOSFET.py +++ b/src/faebryk/library/MOSFET.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class MOSFET(Module): @@ -17,12 +18,12 @@ class SaturationType(Enum): ENHANCEMENT = auto() DEPLETION = auto() - channel_type: F.TBD - saturation_type: F.TBD - gate_source_threshold_voltage: F.TBD - max_drain_source_voltage: F.TBD - max_continuous_drain_current: F.TBD - on_resistance: F.TBD + channel_type = L.p_field(domain=L.Domains.ENUM(ChannelType)) + saturation_type = L.p_field(domain=L.Domains.ENUM(SaturationType)) + gate_source_threshold_voltage = L.p_field(units=P.V) + max_drain_source_voltage = L.p_field(units=P.V) + max_continuous_drain_current = L.p_field(units=P.A) + on_resistance = L.p_field(units=P.ohm) source: F.Electrical gate: F.Electrical diff --git a/src/faebryk/library/MultiCapacitor.py b/src/faebryk/library/MultiCapacitor.py index e2aaba17..d3aa31bc 100644 --- a/src/faebryk/library/MultiCapacitor.py +++ b/src/faebryk/library/MultiCapacitor.py @@ -4,7 +4,7 @@ import logging import faebryk.library._F as F # noqa: F401 -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import ParameterOperatable from faebryk.libs.library import L # noqa: F401 from faebryk.libs.util import times # noqa: F401 @@ -43,13 +43,13 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.capacitance.merge(sum(c.capacitance for c in self.capacitors)) + self.capacitance.alias_is(sum(c.capacitance for c in self.capacitors)) - def set_equal_capacitance(self, capacitance: Parameter): + def set_equal_capacitance(self, capacitance: ParameterOperatable): op = capacitance / self._count self.set_equal_capacitance_each(op) - def set_equal_capacitance_each(self, capacitance: Parameter): + def set_equal_capacitance_each(self, capacitance: ParameterOperatable.NumberLike): for c in self.capacitors: - c.capacitance.merge(capacitance) + c.capacitance.constrain_subset(capacitance) diff --git a/src/faebryk/library/OLED_Module.py b/src/faebryk/library/OLED_Module.py index f460b6b9..6e3d2147 100644 --- a/src/faebryk/library/OLED_Module.py +++ b/src/faebryk/library/OLED_Module.py @@ -38,14 +38,14 @@ class DisplayController(Enum): power: F.ElectricPower i2c: F.I2C - display_resolution: F.TBD - display_controller: F.TBD - display_size: F.TBD + display_resolution = L.p_field(domain=L.Domains.ENUM(DisplayResolution)) + display_controller = L.p_field(domain=L.Domains.ENUM(DisplayController)) + display_size = L.p_field(domain=L.Domains.ENUM(DisplaySize)) def __preinit__(self): - self.power.voltage.merge(F.Range(3.0 * P.V, 5 * P.V)) - self.power.decoupled.decouple().capacitance.merge( - F.Range(100 * P.uF, 220 * P.uF) + self.power.voltage.constrain_subset(L.Range(3.0 * P.V, 5 * P.V)) + self.power.decoupled.decouple().capacitance.constrain_subset( + L.Range(100 * P.uF, 220 * P.uF) ) designator_prefix = L.f_field(F.has_designator_prefix_defined)( diff --git a/src/faebryk/library/OpAmp.py b/src/faebryk/library/OpAmp.py index e05e1530..c3402c81 100644 --- a/src/faebryk/library/OpAmp.py +++ b/src/faebryk/library/OpAmp.py @@ -4,16 +4,17 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class OpAmp(Module): - bandwidth: F.TBD - common_mode_rejection_ratio: F.TBD - input_bias_current: F.TBD - input_offset_voltage: F.TBD - gain_bandwidth_product: F.TBD - output_current: F.TBD - slew_rate: F.TBD + bandwidth = L.p_field(units=P.Hz) + common_mode_rejection_ratio = L.p_field(units=P.dimensionless) + input_bias_current = L.p_field(units=P.A) + input_offset_voltage = L.p_field(units=P.V) + gain_bandwidth_product = L.p_field(units=P.Hz) + output_current = L.p_field(units=P.A) + slew_rate = L.p_field(units=P.V / P.s) power: F.ElectricPower inverting_input: F.Electrical diff --git a/src/faebryk/library/Operation.py b/src/faebryk/library/Operation.py deleted file mode 100644 index 5cbc8194..00000000 --- a/src/faebryk/library/Operation.py +++ /dev/null @@ -1,72 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import logging -import typing -from textwrap import indent - -from faebryk.core.parameter import Parameter -from faebryk.libs.util import TwistArgs, find, try_avoid_endless_recursion - -logger = logging.getLogger(__name__) - - -class Operation(Parameter): - class OperationNotExecutable(Exception): ... - - type LIT_OR_PARAM = Parameter.LIT_OR_PARAM - - def __init__( - self, - operands: typing.Iterable[LIT_OR_PARAM], - operation: typing.Callable[..., Parameter], - ) -> None: - super().__init__() - self.operands = tuple(self.from_literal(o) for o in operands) - self.operation = operation - - @try_avoid_endless_recursion - def __repr__(self): - opsnames = { - "Parameter.__truediv__": "/", - "Parameter.__add__": "+", - "Parameter.__sub__": "-", - "Parameter.__mul__": "*", - } - - op = self.operation - operands = self.operands - - # little hack to make it look better - if isinstance(op, TwistArgs): - op = op.op - operands = list(reversed(operands)) - - fname = op.__qualname__ - - try: - fname = find(opsnames.items(), lambda x: fname.startswith(x[0]))[1] - except KeyError: - ... - - n = self.get_most_narrow() - rep = repr(n) if n is not self else super().__repr__() - return ( - rep - + f"[{fname}]" - + f"(\n{'\n'.join(indent(repr(o), ' ') for o in operands)}\n)" - ) - - def _execute(self): - operands = [o.get_most_narrow() for o in self.operands] - out = self.operation(*operands) - if isinstance(out, Operation): - raise Operation.OperationNotExecutable() - logger.debug(f"{operands=} resolved to {out}") - return out - - def try_compress(self) -> Parameter: - try: - return self._execute() - except Operation.OperationNotExecutable: - return self diff --git a/src/faebryk/library/PM1006.py b/src/faebryk/library/PM1006.py index df12656c..056882b2 100644 --- a/src/faebryk/library/PM1006.py +++ b/src/faebryk/library/PM1006.py @@ -26,12 +26,9 @@ class PM1006(Module): """ class _pm1006_esphome_config(F.has_esphome_config.impl()): - update_interval: F.TBD + update_interval = L.p_field(units=P.s, tolerance_guess=0) def get_config(self) -> dict: - val = self.update_interval.get_most_narrow() - assert isinstance(val, F.Constant), "No update interval set!" - obj = self.obj assert isinstance(obj, PM1006), "This is not an PM1006!" @@ -41,7 +38,7 @@ def get_config(self) -> dict: "sensor": [ { "platform": "pm1006", - "update_interval": f"{val.value.to('s')}", + "update_interval": self.update_interval, "uart_id": uart.get_trait(F.is_esphome_bus).get_bus_id(), } ] @@ -61,5 +58,5 @@ def get_config(self) -> dict: # --------------------------------------------------------------------- def __preinit__(self): - self.power.voltage.merge(F.Range.from_center(5, 0.2)) - self.data.baud.merge(F.Constant(9600 * P.baud)) + self.power.voltage.constrain_subset(L.Range.from_center(5 * P.V, 0.2 * P.V)) + self.data.baud.constrain_subset(9600 * P.baud) diff --git a/src/faebryk/library/Potentiometer.py b/src/faebryk/library/Potentiometer.py index 606ae66b..fc017c4d 100644 --- a/src/faebryk/library/Potentiometer.py +++ b/src/faebryk/library/Potentiometer.py @@ -1,23 +1,26 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT + import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class Potentiometer(Module): resistors_ifs = L.list_field(2, F.Electrical) wiper: F.Electrical - total_resistance: F.TBD + total_resistance = L.p_field(units=P.ohm) resistors = L.list_field(2, F.Resistor) def __preinit__(self): for i, resistor in enumerate(self.resistors): self.resistors_ifs[i].connect_via(resistor, self.wiper) - # TODO use range(0, total_resistance) - resistor.resistance.merge(self.total_resistance) + self.total_resistance.alias_is( + self.resistors[0].resistance + self.resistors[1].resistance + ) def connect_as_voltage_divider( self, high: F.Electrical, low: F.Electrical, out: F.Electrical diff --git a/src/faebryk/library/PowerSwitch.py b/src/faebryk/library/PowerSwitch.py index f264e8b2..af5b773b 100644 --- a/src/faebryk/library/PowerSwitch.py +++ b/src/faebryk/library/PowerSwitch.py @@ -30,4 +30,4 @@ def switch_power(self): ) def __preinit__(self): - self.switched_power_out.voltage.merge(self.power_in.voltage) + self.switched_power_out.voltage.alias_is(self.power_in.voltage) diff --git a/src/faebryk/library/PowerSwitchMOSFET.py b/src/faebryk/library/PowerSwitchMOSFET.py index 2dc452ce..cc25e598 100644 --- a/src/faebryk/library/PowerSwitchMOSFET.py +++ b/src/faebryk/library/PowerSwitchMOSFET.py @@ -21,15 +21,13 @@ def __init__(self, lowside: bool, normally_closed: bool) -> None: mosfet: F.MOSFET def __preinit__(self): - self.mosfet.channel_type.merge( - F.Constant( - F.MOSFET.ChannelType.N_CHANNEL - if self._lowside - else F.MOSFET.ChannelType.P_CHANNEL - ) + self.mosfet.channel_type.constrain_subset( + F.MOSFET.ChannelType.N_CHANNEL + if self._lowside + else F.MOSFET.ChannelType.P_CHANNEL ) - self.mosfet.saturation_type.merge( - F.Constant(F.MOSFET.SaturationType.ENHANCEMENT) + self.mosfet.saturation_type.constrain_subset( + F.MOSFET.SaturationType.ENHANCEMENT ) # pull gate diff --git a/src/faebryk/library/QWIIC_Connector.py b/src/faebryk/library/QWIIC_Connector.py index ea62562e..f6721958 100644 --- a/src/faebryk/library/QWIIC_Connector.py +++ b/src/faebryk/library/QWIIC_Connector.py @@ -60,5 +60,5 @@ def can_attach_to_footprint(self): ) def __preinit__(self): - self.power.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) - self.power.max_current.merge(F.Range.from_center_rel(226 * P.mA, 0.05)) + self.power.voltage.constrain_subset(L.Range.from_center(3.3 * P.V, 0.3 * P.V)) + # self.power.max_current.merge(L.Range.from_center_rel(226 * P.mA, 0.05)) diff --git a/src/faebryk/library/RJ45_Receptacle.py b/src/faebryk/library/RJ45_Receptacle.py index 6d4ea630..04dafa91 100644 --- a/src/faebryk/library/RJ45_Receptacle.py +++ b/src/faebryk/library/RJ45_Receptacle.py @@ -21,4 +21,4 @@ class Mounting(Enum): designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.J ) - mounting: F.TBD + mounting = L.p_field(domain=L.Domains.ENUM(Mounting)) diff --git a/src/faebryk/library/RP2040.py b/src/faebryk/library/RP2040.py index 03c21b0b..451e3b6d 100644 --- a/src/faebryk/library/RP2040.py +++ b/src/faebryk/library/RP2040.py @@ -43,8 +43,10 @@ def __preinit__(self): F.ElectricLogic.connect_all_module_references(self, gnd_only=True) # TODO get tolerance - self.power_out.voltage.merge(F.Range.from_center_rel(1.1 * P.V, 0.05)) - self.power_in.voltage.merge(F.Range(1.8 * P.V, 3.3 * P.V)) + self.power_out.voltage.constrain_subset( + L.Range.from_center_rel(1.1 * P.V, 0.05) + ) + self.power_in.voltage.constrain_subset(L.Range(1.8 * P.V, 3.3 * P.V)) @L.rt_field def bridge(self): @@ -108,10 +110,16 @@ def single_reference(self): def __preinit__(self): # TODO get tolerance - self.power_adc.voltage.merge(F.Range.from_center_rel(3.3 * P.V, 0.05)) - self.power_usb_phy.voltage.merge(F.Range.from_center_rel(3.3 * P.V, 0.05)) - self.power_core.voltage.merge(F.Range.from_center_rel(1.1 * P.V, 0.05)) - self.power_io.voltage.merge(F.Range(1.8 * P.V, 3.3 * P.V)) + self.power_adc.voltage.constrain_subset( + L.Range.from_center_rel(3.3 * P.V, 0.05) + ) + self.power_usb_phy.voltage.constrain_subset( + L.Range.from_center_rel(3.3 * P.V, 0.05) + ) + self.power_core.voltage.constrain_subset( + L.Range.from_center_rel(1.1 * P.V, 0.05) + ) + self.power_io.voltage.constrain_subset(L.Range(1.8 * P.V, 3.3 * P.V)) F.ElectricLogic.connect_all_module_references(self, gnd_only=True) F.ElectricLogic.connect_all_node_references( diff --git a/src/faebryk/library/RP2040_ReferenceDesign.py b/src/faebryk/library/RP2040_ReferenceDesign.py index 1abb106e..9f169083 100644 --- a/src/faebryk/library/RP2040_ReferenceDesign.py +++ b/src/faebryk/library/RP2040_ReferenceDesign.py @@ -30,8 +30,10 @@ class Jumper(Module): switch = L.f_field(F.Switch(F.Electrical))() def __preinit__(self): - self.resistor.resistance.merge(F.Range.from_center_rel(1 * P.kohm, 0.05)) - self.logic_out.set_weak(True).resistance.merge(self.resistor.resistance) + self.resistor.resistance.constrain_subset( + L.Range.from_center_rel(1 * P.kohm, 0.05) + ) + self.logic_out.set_weak(True).resistance.alias_is(self.resistor.resistance) self.logic_out.signal.connect_via( [self.resistor, self.switch], self.logic_out.reference.lv ) @@ -82,26 +84,28 @@ def __preinit__(self): # parametrization # ---------------------------------------- # LDO - self.ldo.output_current.merge(F.Range.from_center_rel(600 * P.mA, 0.05)) - self.ldo.power_in.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.05) + self.ldo.output_current.constrain_subset( + L.Range.from_center_rel(600 * P.mA, 0.05) + ) + self.ldo.power_in.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.05) ) - self.ldo.power_out.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.05) + self.ldo.power_out.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.05) ) # XTAL - self.clock_source.crystal.load_capacitance.merge( - F.Range.from_center_rel(10 * P.pF, 0.05) + self.clock_source.crystal.load_capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.pF, 0.05) ) - self.clock_source.current_limiting_resistor.resistance.merge( - F.Range.from_center_rel(1 * P.kohm, 0.05) + self.clock_source.current_limiting_resistor.resistance.constrain_subset( + L.Range.from_center_rel(1 * P.kohm, 0.05) ) self.clock_source.crystal.add( F.has_descriptive_properties_defined( { - DescriptiveProperties.manufacturer.value: "Abracon LLC", + DescriptiveProperties.manufacturer: "Abracon LLC", DescriptiveProperties.partno: "ABM8-272-T3", } ) @@ -111,35 +115,37 @@ def __preinit__(self): terminated_usb_data = self.add( self.usb.usb_if.d.terminated(), "_terminated_usb_data" ) - terminated_usb_data.impedance.merge(F.Range.from_center_rel(27.4 * P.ohm, 0.05)) + terminated_usb_data.impedance.constrain_subset( + L.Range.from_center_rel(27.4 * P.ohm, 0.05) + ) # Flash - self.flash.memory_size.merge(16 * P.Mbit) - self.flash.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.05) + self.flash.memory_size.constrain_subset(16 * P.Mbit) + self.flash.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.05) ) # Power rails self.rp2040.power_io.decoupled.decouple().specialize( F.MultiCapacitor(6) - ).set_equal_capacitance_each(F.Range.from_center_rel(100 * P.nF, 0.05)) - self.rp2040.core_regulator.power_in.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(1 * P.uF, 0.05) + ).set_equal_capacitance_each(L.Range.from_center_rel(100 * P.nF, 0.05)) + self.rp2040.core_regulator.power_in.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(1 * P.uF, 0.05) ) - self.rp2040.power_adc.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.05) + self.rp2040.power_adc.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.05) ) - self.rp2040.power_usb_phy.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.05) + self.rp2040.power_usb_phy.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.05) ) - power_3v3.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(10 * P.uF, 0.05) + power_3v3.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(10 * P.uF, 0.05) ) self.rp2040.power_core.decoupled.decouple().specialize( F.MultiCapacitor(2) - ).set_equal_capacitance_each(F.Range.from_center_rel(100 * P.nF, 0.05)) - self.rp2040.core_regulator.power_out.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(1 * P.uF, 0.05) + ).set_equal_capacitance_each(L.Range.from_center_rel(100 * P.nF, 0.05)) + self.rp2040.core_regulator.power_out.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(1 * P.uF, 0.05) ) # ---------------------------------------- diff --git a/src/faebryk/library/RS485_Bus_Protection.py b/src/faebryk/library/RS485_Bus_Protection.py index 621a05cc..fcce96f1 100644 --- a/src/faebryk/library/RS485_Bus_Protection.py +++ b/src/faebryk/library/RS485_Bus_Protection.py @@ -102,8 +102,8 @@ def can_bridge(self): def __preinit__(self): if self._termination: termination_resistor = self.add(F.Resistor(), name="termination_resistor") - termination_resistor.resistance.merge( - F.Range.from_center_rel(120 * P.ohm, 0.05) + termination_resistor.resistance.constrain_subset( + L.Range.from_center_rel(120 * P.ohm, 0.05) ) self.rs485_ufp.diff_pair.p.signal.connect_via( termination_resistor, self.rs485_ufp.diff_pair.n.signal @@ -111,11 +111,11 @@ def __preinit__(self): if self._polarization: polarization_resistors = self.add_to_container(2, F.Resistor) - polarization_resistors[0].resistance.merge( - F.Range(380 * P.ohm, 420 * P.ohm) + polarization_resistors[0].resistance.constrain_subset( + L.Range(380 * P.ohm, 420 * P.ohm) ) - polarization_resistors[1].resistance.merge( - F.Range(380 * P.ohm, 420 * P.ohm) + polarization_resistors[1].resistance.constrain_subset( + L.Range(380 * P.ohm, 420 * P.ohm) ) self.rs485_dfp.diff_pair.p.signal.connect_via( polarization_resistors[0], self.power.hv @@ -127,26 +127,22 @@ def __preinit__(self): # ---------------------------------------- # parametrization # ---------------------------------------- - self.current_limmiter_resistors[0].resistance.merge( - F.Range.from_center_rel(2.7 * P.ohm, 0.05) + self.current_limmiter_resistors[0].resistance.constrain_subset( + L.Range.from_center_rel(2.7 * P.ohm, 0.05) ) - self.current_limmiter_resistors[0].rated_power.merge( - F.Range.lower_bound(500 * P.mW) - ) - self.current_limmiter_resistors[1].resistance.merge( - F.Range.from_center_rel(2.7 * P.ohm, 0.05) - ) - self.current_limmiter_resistors[1].rated_power.merge( - F.Range.lower_bound(500 * P.mW) + self.current_limmiter_resistors[0].max_power.constrain_ge(500 * P.mW) + self.current_limmiter_resistors[1].resistance.constrain_subset( + L.Range.from_center_rel(2.7 * P.ohm, 0.05) ) + self.current_limmiter_resistors[1].max_power.constrain_ge(500 * P.mW) - self.gnd_couple_resistor.resistance.merge( - F.Range.from_center_rel(1 * P.Mohm, 0.05) + self.gnd_couple_resistor.resistance.constrain_subset( + L.Range.from_center_rel(1 * P.Mohm, 0.05) ) - self.gnd_couple_capacitor.capacitance.merge( - F.Range.from_center_rel(1 * P.uF, 0.05) + self.gnd_couple_capacitor.capacitance.constrain_subset( + L.Range.from_center_rel(1 * P.uF, 0.05) ) - self.gnd_couple_capacitor.rated_voltage.merge(F.Range.lower_bound(2 * P.kV)) + self.gnd_couple_capacitor.max_voltage.constrain_ge(2 * P.kV) # ---------------------------------------- # Connections diff --git a/src/faebryk/library/Range.py b/src/faebryk/library/Range.py deleted file mode 100644 index a7939911..00000000 --- a/src/faebryk/library/Range.py +++ /dev/null @@ -1,159 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from math import inf -from typing import Any, Protocol, Self - -from faebryk.core.parameter import Parameter -from faebryk.libs.units import UnitsContainer, to_si_str - - -class _SupportsRangeOps(Protocol): - def __add__(self, __value) -> "_SupportsRangeOps": ... - def __sub__(self, __value) -> "_SupportsRangeOps": ... - def __mul__(self, __value) -> "_SupportsRangeOps": ... - - def __le__(self, __value) -> bool: ... - def __lt__(self, __value) -> bool: ... - def __ge__(self, __value) -> bool: ... - - -class Range(Parameter): - type PV = Parameter.PV - type LIT_OR_PARAM = Parameter.LIT_OR_PARAM - - class MinMaxError(Exception): ... - - def __init__(self, *bounds: PV | Parameter) -> None: - super().__init__() - - self._bounds: list[Parameter] = [Parameter.from_literal(b) for b in bounds] - - def _get_narrowed_bounds(self) -> list[Parameter]: - return list({b.get_most_narrow() for b in self._bounds}) - - @property - def min(self) -> Parameter: - try: - return min(self._get_narrowed_bounds()) - except (TypeError, ValueError): - raise self.MinMaxError() - - @property - def max(self) -> Parameter: - try: - return max(self._get_narrowed_bounds()) - except (TypeError, ValueError): - raise self.MinMaxError() - - @property - def bounds(self) -> list[Parameter]: - try: - return [self.min, self.max] - except self.MinMaxError: - return self._get_narrowed_bounds() - - def as_tuple(self) -> tuple[Parameter, Parameter]: - return (self.min, self.max) - - def as_center_tuple(self, relative=False) -> tuple[Parameter, Parameter]: - center = (self.min + self.max) / 2 - delta = (self.max - self.min) / 2 - if relative: - delta /= center - return center, delta - - @classmethod - def from_center(cls, center: LIT_OR_PARAM, delta: LIT_OR_PARAM) -> "Range": - return cls(center - delta, center + delta) - - @classmethod - def from_center_rel(cls, center: PV, factor: PV) -> "Range": - return cls.from_center(center, center * factor) - - @classmethod - def _with_bound(cls, bound: LIT_OR_PARAM, other: float) -> "Range": - try: - other_with_unit = Parameter.with_same_unit(bound, other) - except NotImplementedError: - raise NotImplementedError("Specify zero/inf manually in params") - - return cls(bound, other_with_unit) - - @classmethod - def lower_bound(cls, lower: LIT_OR_PARAM) -> "Range": - return cls._with_bound(lower, inf) - - @classmethod - def upper_bound(cls, upper: LIT_OR_PARAM) -> "Range": - return cls._with_bound(upper, 0) - - def __str__(self) -> str: - bounds = map(str, self.bounds) - return super().__str__() + f"({', '.join(bounds)})" - - def __repr__(self): - bounds = map(repr, self.bounds) - return super().__repr__() + f"({', '.join(bounds)})" - - def __eq__(self, other: Any) -> bool: - if not isinstance(other, Range): - return False - return self.bounds == other.bounds - - def __hash__(self) -> int: - return sum(hash(b) for b in self._bounds) - - # comparison operators - def __le__(self, other) -> bool: - return self.max <= other - - def __lt__(self, other) -> bool: - return self.max < other - - def __ge__(self, other) -> bool: - return self.min >= other - - def __gt__(self, other) -> bool: - return self.min > other - - def __format__(self, format_spec): - bounds = [format(b, format_spec) for b in self._get_narrowed_bounds()] - return f"{super().__str__()}({', '.join(bounds)})" - - def __copy__(self) -> Self: - return type(self)(*self._bounds) - - def try_compress(self) -> Parameter: - # compress into constant if possible - if len(set(map(id, self._get_narrowed_bounds()))) == 1: - return Parameter.from_literal(self.bounds[0]) - return super().try_compress() - - def __contains__(self, other: LIT_OR_PARAM) -> bool: - return self.min <= other and self.max >= other - - def _max(self): - return max(p.get_max() for p in self._get_narrowed_bounds()) - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - return ( - self.min.as_unit(unit, base=base) - + " - " - + self.max.as_unit(unit, base=base, required=True) - ) - - def _as_unit_with_tolerance( - self, unit: UnitsContainer, base: int, required: bool - ) -> str: - center, delta = self.as_center_tuple(relative=True) - delta_percent_str = f"±{to_si_str(delta.value, "%", 0)}" - return ( - f"{center.as_unit(unit, base=base, required=required)} {delta_percent_str}" - ) - - def _enum_parameter_representation(self, required: bool) -> str: - return ( - f"{self.min.enum_parameter_representation(required)} - " - f"{self.max.enum_parameter_representation(required)}" - ) diff --git a/src/faebryk/library/Relay.py b/src/faebryk/library/Relay.py index 6a02c9f7..37b468b7 100644 --- a/src/faebryk/library/Relay.py +++ b/src/faebryk/library/Relay.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -20,12 +21,12 @@ class Relay(Module): switch_b_nc: F.Electrical coil_power: F.ElectricPower - coil_rated_voltage: F.TBD - coil_rated_current: F.TBD - coil_resistance: F.TBD - contact_max_switching_voltage: F.TBD - contact_rated_switching_current: F.TBD - contact_max_switchng_current: F.TBD + coil_max_voltage = L.p_field(units=P.V) + coil_max_current = L.p_field(units=P.A) + coil_resistance = L.p_field(units=P.ohm) + contact_max_switching_voltage = L.p_field(units=P.V) + contact_max_switching_current = L.p_field(units=P.A) + contact_max_current = L.p_field(units=P.A) designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.K diff --git a/src/faebryk/library/Resistor.py b/src/faebryk/library/Resistor.py index 1a8515fb..d079252b 100644 --- a/src/faebryk/library/Resistor.py +++ b/src/faebryk/library/Resistor.py @@ -1,11 +1,10 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -from math import sqrt +from more_itertools import raise_ import faebryk.library._F as F from faebryk.core.module import Module -from faebryk.core.parameter import Parameter from faebryk.libs.library import L from faebryk.libs.picker.picker import PickError, has_part_picked_remove from faebryk.libs.units import P @@ -15,9 +14,9 @@ class Resistor(Module): unnamed = L.list_field(2, F.Electrical) - resistance: F.TBD - rated_power: F.TBD - rated_voltage: F.TBD + resistance = L.p_field(units=P.ohm) + max_power = L.p_field(units=P.W) + max_voltage = L.p_field(units=P.V) attach_to_footprint: F.can_attach_to_footprint_symmetrically designator_prefix = L.f_field(F.has_designator_prefix_defined)( @@ -33,12 +32,12 @@ def simple_value_representation(self): return F.has_simple_value_representation_based_on_params( ( self.resistance, - self.rated_power, + self.max_power, ), - lambda resistance, rated_power: join_if_non_empty( + lambda resistance, max_power: join_if_non_empty( " ", resistance.as_unit_with_tolerance("Ω"), - rated_power.as_unit("W"), + max_power.as_unit("W"), ), ) @@ -48,84 +47,17 @@ def allow_removal_if_zero(self): def replace_zero(m: Module): assert m is self - r = self.resistance.get_most_narrow() - if not F.Constant(0.0 * P.ohm).is_subset_of(r): - raise PickError("", self) + def do_replace(): + self.resistance.constrain_subset(0.0 * P.ohm) + self.unnamed[0].connect(self.unnamed[1]) + self.add(has_part_picked_remove()) - self.resistance.override(F.Constant(0.0 * P.ohm)) - self.unnamed[0].connect(self.unnamed[1]) - self.add(has_part_picked_remove()) + self.resistance.operation_is_superset(0.0 * P.ohm).if_then_else( + lambda: do_replace(), + lambda: raise_(PickError("", self)), + preference=True, + ) self.add( F.has_multi_picker(-100, F.has_multi_picker.FunctionPicker(replace_zero)) ) - - def get_voltage_drop_by_current_resistance(self, current_A: Parameter) -> Parameter: - return current_A * self.resistance - - def get_voltage_drop_by_power_resistance(self, power_W: Parameter) -> Parameter: - return sqrt(power_W * self.resistance) - - @staticmethod - def set_voltage_drop_by_power_current( - power_W: Parameter, current_A: Parameter - ) -> Parameter: - return power_W / current_A - - def get_current_flow_by_voltage_resistance( - self, voltage_drop_V: Parameter - ) -> Parameter: - return voltage_drop_V / self.resistance - - def get_current_flow_by_power_resistance(self, power_W: Parameter) -> Parameter: - return sqrt(power_W / self.resistance) - - @staticmethod - def get_current_flow_by_voltage_power( - voltage_drop_V: Parameter, power_W: Parameter - ) -> Parameter: - return power_W / voltage_drop_V - - def set_resistance_by_voltage_current( - self, voltage_drop_V: Parameter, current_A: Parameter - ) -> Parameter: - self.resistance.merge(voltage_drop_V / current_A) - return self.resistance.get_most_narrow() - - def set_resistance_by_voltage_power( - self, voltage_drop_V: Parameter, power_W: Parameter - ) -> Parameter: - self.resistance.merge(pow(voltage_drop_V, 2) / power_W) - return self.resistance.get_most_narrow() - - def set_resistance_by_power_current( - self, current_A: Parameter, power_W: Parameter - ) -> Parameter: - self.resistance.merge(power_W / pow(current_A, 2)) - return self.resistance.get_most_narrow() - - def get_power_dissipation_by_voltage_resistance( - self, voltage_drop_V: Parameter - ) -> Parameter: - return pow(voltage_drop_V, 2) / self.resistance - - def get_power_dissipation_by_current_resistance( - self, current_A: Parameter - ) -> Parameter: - return pow(current_A, 2) * self.resistance - - @staticmethod - def get_power_dissipation_by_voltage_current( - voltage_drop_V: Parameter, current_A - ) -> Parameter: - return voltage_drop_V * current_A - - def set_rated_power_by_voltage_resistance(self, voltage_drop_V: Parameter): - self.rated_power.merge( - self.get_power_dissipation_by_voltage_resistance(voltage_drop_V) - ) - - def set_rated_power_by_current_resistance(self, current_A: Parameter): - self.rated_power.merge( - self.get_power_dissipation_by_current_resistance(current_A) - ) diff --git a/src/faebryk/library/ResistorVoltageDivider.py b/src/faebryk/library/ResistorVoltageDivider.py index 01f2febc..a12a562a 100644 --- a/src/faebryk/library/ResistorVoltageDivider.py +++ b/src/faebryk/library/ResistorVoltageDivider.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -15,8 +16,8 @@ class ResistorVoltageDivider(Module): node = L.list_field(3, F.Electrical) - ratio: F.TBD - max_current: F.TBD + ratio = L.p_field(units=P.dimensionless) + max_current = L.p_field(units=P.A) @L.rt_field def can_bridge(self): diff --git a/src/faebryk/library/Resistor_Voltage_Divider.py b/src/faebryk/library/Resistor_Voltage_Divider.py index 5b286465..405526bb 100644 --- a/src/faebryk/library/Resistor_Voltage_Divider.py +++ b/src/faebryk/library/Resistor_Voltage_Divider.py @@ -6,6 +6,7 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -14,8 +15,8 @@ class Resistor_Voltage_Divider(Module): resistor = L.list_field(2, F.Resistor) node = L.list_field(3, F.Electrical) - ratio: F.TBD - max_current: F.TBD + ratio = L.p_field(units=P.dimensionless) + max_current = L.p_field(units=P.A) def __preinit__(self): self.node[0].connect_via(self.resistor[0], self.node[1]) diff --git a/src/faebryk/library/SCD40.py b/src/faebryk/library/SCD40.py index 7a8f6944..e3b4591a 100644 --- a/src/faebryk/library/SCD40.py +++ b/src/faebryk/library/SCD40.py @@ -14,12 +14,9 @@ class SCD40(Module): """ class _scd4x_esphome_config(F.has_esphome_config.impl()): - update_interval: F.TBD + update_interval = L.p_field(units=P.s, tolerance_guess=0) def get_config(self) -> dict: - val = self.update_interval.get_most_narrow() - assert isinstance(val, F.Constant) - obj = self.get_obj(SCD40) i2c = F.is_esphome_bus.find_connected_bus(obj.i2c) @@ -39,17 +36,11 @@ def get_config(self) -> dict: }, "address": 0x62, "i2c_id": i2c.get_trait(F.is_esphome_bus).get_bus_id(), - "update_interval": f"{val.value.to('s')}", + "update_interval": self.update_interval, } ] } - def is_implemented(self): - return ( - isinstance(self.update_interval.get_most_narrow(), F.Constant) - and super().is_implemented() - ) - esphome_config: _scd4x_esphome_config # interfaces @@ -71,10 +62,10 @@ def attach_to_footprint(self): ) def __preinit__(self): - self.power.voltage.merge(F.Range.from_center_rel(3.3 * P.V, 0.05)) + self.power.voltage.constrain_subset(L.Range.from_center_rel(3.3 * P.V, 0.05)) self.i2c.terminate() self.power.decoupled.decouple() - self.i2c.frequency.merge( + self.i2c.frequency.constrain_le( F.I2C.define_max_frequency_capability(F.I2C.SpeedMode.fast_speed) ) diff --git a/src/faebryk/library/SK9822_EC20.py b/src/faebryk/library/SK9822_EC20.py index 3ef39d61..430dd836 100644 --- a/src/faebryk/library/SK9822_EC20.py +++ b/src/faebryk/library/SK9822_EC20.py @@ -12,7 +12,7 @@ class SK9822_EC20(Module): (RGB) driving intelligent control circuit and the light emitting circuit in one of the LED light source control. Products containing a signal - decoding module, data buffer, a built-in F.Constant + decoding module, data buffer, a built-in Constant current circuit and RC oscillator; CMOS, low voltage, low power consumption; 256 level grayscale PWM adjustment and 32 brightness adjustment; diff --git a/src/faebryk/library/SNx4LVC541A.py b/src/faebryk/library/SNx4LVC541A.py index 31c4b921..bb635017 100644 --- a/src/faebryk/library/SNx4LVC541A.py +++ b/src/faebryk/library/SNx4LVC541A.py @@ -38,7 +38,7 @@ def __preinit__(self): # ---------------------------------------- # parameters # ---------------------------------------- - self.power.voltage.merge(F.Range.upper_bound(3.6 * P.V)) + self.power.voltage.constrain_le(3.6 * P.V) # ---------------------------------------- # aliases diff --git a/src/faebryk/library/SP3243E.py b/src/faebryk/library/SP3243E.py index df4f8c60..6ff81f9b 100644 --- a/src/faebryk/library/SP3243E.py +++ b/src/faebryk/library/SP3243E.py @@ -60,7 +60,7 @@ def enable_auto_online(self): def descriptive_properties(self): return F.has_descriptive_properties_defined( { - DescriptiveProperties.manufacturer.value: "MaxLinear", + DescriptiveProperties.manufacturer: "MaxLinear", DescriptiveProperties.partno: "SP3243EBEA-L/TR", }, ) @@ -106,14 +106,14 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.power.voltage.merge(F.Range(3.0 * P.V, 5.5 * P.V)) + self.power.voltage.constrain_subset(L.Range(3.0 * P.V, 5.5 * P.V)) - self.uart.base_uart.baud.merge(F.Range.upper_bound(250 * P.kbaud)) + self.uart.base_uart.baud.constrain_le(250 * P.kbaud) self.rs232.get_trait( F.has_single_electric_reference - ).get_reference().voltage.merge( - F.Range.from_center(3 * P.V, 15 * P.V) + ).get_reference().voltage.constrain_subset( + L.Range.from_center(3 * P.V, 15 * P.V) ) # TODO: Support negative numbers (-15 * P.V, 15 * P.V)) # ------------------------------------ @@ -124,4 +124,4 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.power.voltage.merge(F.Range(3.0 * P.V, 5.5 * P.V)) + self.power.voltage.constrain_subset(L.Range(3.0 * P.V, 5.5 * P.V)) diff --git a/src/faebryk/library/SP3243E_ReferenceDesign.py b/src/faebryk/library/SP3243E_ReferenceDesign.py index 7aafd05f..2b0342e3 100644 --- a/src/faebryk/library/SP3243E_ReferenceDesign.py +++ b/src/faebryk/library/SP3243E_ReferenceDesign.py @@ -42,11 +42,13 @@ def __preinit__(self): # 4.5V to 5.5V > C1 = 0.047µF, C2,Cvp, Cvn = 0.33µF # 3.0V to 5.5V > C_all = 0.22μF # - cap.capacitance.merge(F.Range.from_center(0.22 * P.uF, 0.22 * 0.05 * P.uF)) - - if isinstance(pwr.voltage.get_most_narrow(), F.TBD): - pwr.voltage.merge( - F.Constant(8 * P.V) - # F.Range.lower_bound(16 * P.V) - ) # TODO: fix merge - # TODO: merge conflict + cap.capacitance.constrain_subset( + L.Range.from_center(0.22 * P.uF, 0.22 * 0.05 * P.uF) + ) + + # if isinstance(pwr.voltage.get_most_narrow(), F.TBD): + # pwr.voltage.merge( + # L.Single(8 * P.V) + # # L.Range.lower_bound(16 * P.V) + # ) # TODO: fix merge + # # TODO: merge conflict diff --git a/src/faebryk/library/SPIFlash.py b/src/faebryk/library/SPIFlash.py index bd04106c..6870e408 100644 --- a/src/faebryk/library/SPIFlash.py +++ b/src/faebryk/library/SPIFlash.py @@ -4,13 +4,17 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class SPIFlash(Module): power: F.ElectricPower qspi = L.f_field(F.MultiSPI)(4) - memory_size: F.TBD + memory_size = L.p_field( + units=P.byte, + domain=L.Domains.Numbers.NATURAL(), + ) designator_prefix = L.f_field(F.has_designator_prefix_defined)( F.has_designator_prefix.Prefix.U ) diff --git a/src/faebryk/library/Set.py b/src/faebryk/library/Set.py deleted file mode 100644 index 97e6d698..00000000 --- a/src/faebryk/library/Set.py +++ /dev/null @@ -1,112 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from typing import Iterable, Self - -import faebryk.library._F as F -from faebryk.core.parameter import Parameter, _resolved -from faebryk.libs.units import UnitsContainer - - -class Set(Parameter): - type LIT_OR_PARAM = Parameter.LIT_OR_PARAM - - def __init__(self, params: Iterable[Parameter]) -> None: - super().__init__() - - # make primitves to constants - self._params = set( - p if isinstance(p, Parameter) else F.Constant(p) for p in params - ) - - @staticmethod - def _flatten(params: set[Parameter]) -> set[Parameter]: - param_set = set( - p for p in params if not isinstance(p, Set) and isinstance(p, Parameter) - ) - set_set = set(x for p in params if isinstance(p, Set) for x in p.params) - - return param_set | set_set - - def flat(self) -> set[Parameter]: - return Set._flatten(self._params) - - @property - def params(self) -> set[Parameter]: - return self.flat() - - def __str__(self) -> str: - return super().__str__() + f"({self.params})" - - def __repr__(self): - return super().__repr__() + f"({self.params!r})" - - def __eq__(self, other) -> bool: - if not isinstance(other, Set): - return False - - return self.params == other.params - - def __hash__(self) -> int: - return sum(hash(p) for p in self.params) - - # comparison operators - def __le__(self, other) -> bool: - return all(p <= other for p in self.params) - - def __lt__(self, other) -> bool: - return all(p < other for p in self.params) - - def __ge__(self, other) -> bool: - return all(p >= other for p in self.params) - - def __gt__(self, other) -> bool: - return all(p > other for p in self.params) - - def copy(self) -> Self: - return type(self)(self.params) - - @_resolved - def __contains__(self, other: Parameter) -> bool: - def nested_in(p): - if other == p: - return True - if isinstance(p, F.Range): - return other in p - return False - - return any(nested_in(p) for p in self.params) - - def try_compress(self) -> Parameter: - # compress into constant if possible - if len(set(map(id, self.params))) == 1: - return Parameter.from_literal(next(iter(self.params))) - return super().try_compress() - - def _max(self): - return max(p.get_max() for p in self.params) - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - return ( - "Set(" - + ", ".join(x.as_unit(unit, required=True) for x in self.params) - + ")" - ) - - def _as_unit_with_tolerance( - self, unit: UnitsContainer, base: int, required: bool - ) -> str: - return ( - "Set(" - + ", ".join( - x.as_unit_with_tolerance(unit, base, required) for x in self.params - ) - + ")" - ) - - def _enum_parameter_representation(self, required: bool) -> str: - return ( - "Set(" - + ", ".join(p.enum_parameter_representation(required) for p in self.params) - + ")" - ) diff --git a/src/faebryk/library/SignalElectrical.py b/src/faebryk/library/SignalElectrical.py index 07d242c7..5f25f429 100644 --- a/src/faebryk/library/SignalElectrical.py +++ b/src/faebryk/library/SignalElectrical.py @@ -88,7 +88,7 @@ class _can_be_surge_protected_defined(F.can_be_surge_protected_defined): def protect(_self): return [ tvs.builder( - lambda t: t.reverse_working_voltage.merge( + lambda t: t.reverse_working_voltage.alias_is( self.reference.voltage ) ) diff --git a/src/faebryk/library/TBD.py b/src/faebryk/library/TBD.py deleted file mode 100644 index 33fbdff4..00000000 --- a/src/faebryk/library/TBD.py +++ /dev/null @@ -1,36 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from textwrap import indent - -from faebryk.core.parameter import Parameter, _resolved -from faebryk.libs.units import UnitsContainer - - -class TBD(Parameter): - @_resolved - def __eq__(self, __value: object) -> bool: - if isinstance(__value, TBD): - return True - - return False - - def __hash__(self) -> int: - return super().__hash__() - - def __repr__(self) -> str: - o = self.get_most_narrow() - if o is self: - return super().__repr__() - else: - out = f"{super().__repr__():<80} ===> " - or_ = repr(o) - if "\n" in or_: - out += indent(or_, len(out) * " ") - else: - out += or_ - - return out - - def _as_unit(self, unit: UnitsContainer, base: int, required: bool) -> str: - return "TBD" if required else "" diff --git a/src/faebryk/library/TD541S485H.py b/src/faebryk/library/TD541S485H.py index 2c18adec..712ac208 100644 --- a/src/faebryk/library/TD541S485H.py +++ b/src/faebryk/library/TD541S485H.py @@ -56,7 +56,8 @@ def __preinit__(self): self.power_iso_out.decoupled.decouple() self.power_iso_in.lv.connect(self.power_iso_out.lv) - self.power_iso_out.voltage.merge(5 * P.V) + # TODO tolerance + self.power_iso_out.voltage.constrain_superset(5 * P.V) F.ElectricLogic.connect_all_module_references( self, diff --git a/src/faebryk/library/TI_CD4011BE.py b/src/faebryk/library/TI_CD4011BE.py index 587a436c..f9f10823 100644 --- a/src/faebryk/library/TI_CD4011BE.py +++ b/src/faebryk/library/TI_CD4011BE.py @@ -36,7 +36,7 @@ def __preinit__(self): self.add( F.has_descriptive_properties_defined( { - DescriptiveProperties.manufacturer.value: "Texas Instruments", + DescriptiveProperties.manufacturer: "Texas Instruments", DescriptiveProperties.partno: "CD4011BE", }, ) diff --git a/src/faebryk/library/TPS2116.py b/src/faebryk/library/TPS2116.py index 393df2a7..7aef408e 100644 --- a/src/faebryk/library/TPS2116.py +++ b/src/faebryk/library/TPS2116.py @@ -134,4 +134,4 @@ def __preinit__(self): # parametrization # ------------------------------------ for power in [self.power_in[0], self.power_in[1], self.power_out]: - power.voltage.merge(F.Range(1.6 * P.V, 5.5 * P.V)) + power.voltage.constrain_subset(L.Range(1.6 * P.V, 5.5 * P.V)) diff --git a/src/faebryk/library/TVS.py b/src/faebryk/library/TVS.py index 987e4548..c0a28e65 100644 --- a/src/faebryk/library/TVS.py +++ b/src/faebryk/library/TVS.py @@ -4,9 +4,11 @@ import logging import faebryk.library._F as F +from faebryk.libs.library import L +from faebryk.libs.units import P logger = logging.getLogger(__name__) class TVS(F.Diode): - reverse_breakdown_voltage: F.TBD + reverse_breakdown_voltage = L.p_field(units=P.V) diff --git a/src/faebryk/library/UART_Base.py b/src/faebryk/library/UART_Base.py index 92c16f25..8450cbcd 100644 --- a/src/faebryk/library/UART_Base.py +++ b/src/faebryk/library/UART_Base.py @@ -4,6 +4,7 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface from faebryk.libs.library import L +from faebryk.libs.units import P from faebryk.libs.util import cast_assert @@ -11,7 +12,7 @@ class UART_Base(ModuleInterface): rx: F.ElectricLogic tx: F.ElectricLogic - baud: F.TBD + baud = L.p_field(units=P.baud) @L.rt_field def single_electric_reference(self): diff --git a/src/faebryk/library/UART_RS485.py b/src/faebryk/library/UART_RS485.py index aa6b19ba..facdd2a4 100644 --- a/src/faebryk/library/UART_RS485.py +++ b/src/faebryk/library/UART_RS485.py @@ -18,12 +18,12 @@ class UART_RS485(Module): read_enable: F.ElectricLogic write_enable: F.ElectricLogic - max_data_rate: F.TBD - gpio_voltage: F.TBD + max_data_rate = L.p_field(units=P.baud) + gpio_voltage = L.p_field(units=P.V) def __preinit__(self): - self.max_data_rate.merge(self.uart.baud) - self.power.voltage.merge(F.Range(3.3 * P.V, 5.0 * P.V)) + self.max_data_rate.alias_is(self.uart.baud) + self.power.voltage.constrain_subset(L.Range(3.3 * P.V, 5.0 * P.V)) self.power.decoupled.decouple() designator_prefix = L.f_field(F.has_designator_prefix_defined)( diff --git a/src/faebryk/library/USB2514B.py b/src/faebryk/library/USB2514B.py index 71113c54..a7b233b5 100644 --- a/src/faebryk/library/USB2514B.py +++ b/src/faebryk/library/USB2514B.py @@ -343,14 +343,18 @@ def __preinit__(self): # ---------------------------------------- # parametrization # ---------------------------------------- - self.power_pll.voltage.merge( - F.Range.from_center_rel(1.8 * P.V, 0.05) + self.power_pll.voltage.constrain_subset( + L.Range.from_center_rel(1.8 * P.V, 0.05) ) # datasheet does not specify a voltage range - self.power_core.voltage.merge( - F.Range.from_center_rel(1.8 * P.V, 0.05) + self.power_core.voltage.constrain_subset( + L.Range.from_center_rel(1.8 * P.V, 0.05) ) # datasheet does not specify a voltage range - self.power_3v3_regulator.voltage.merge( - F.Range.from_center(3.3 * P.V, 0.3 * P.V) + self.power_3v3_regulator.voltage.constrain_subset( + L.Range.from_center(3.3 * P.V, 0.3 * P.V) + ) + self.power_3v3_analog.voltage.constrain_subset( + L.Range.from_center(3.3 * P.V, 0.3 * P.V) + ) + self.power_io.voltage.constrain_subset( + L.Range.from_center(3.3 * P.V, 0.3 * P.V) ) - self.power_3v3_analog.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) - self.power_io.voltage.merge(F.Range.from_center(3.3 * P.V, 0.3 * P.V)) diff --git a/src/faebryk/library/USB2514B_ReferenceDesign.py b/src/faebryk/library/USB2514B_ReferenceDesign.py index f917a354..7fecd084 100644 --- a/src/faebryk/library/USB2514B_ReferenceDesign.py +++ b/src/faebryk/library/USB2514B_ReferenceDesign.py @@ -200,34 +200,36 @@ def __preinit__(self): ) # TODO: load_capacitance is a property of the crystal. remove this - self.crystal_oscillator.crystal.load_capacitance.merge( - F.Range(8 * P.pF, 15 * P.pF) + self.crystal_oscillator.crystal.load_capacitance.constrain_subset( + L.Range(8 * P.pF, 15 * P.pF) ) - self.crystal_oscillator.crystal.frequency.merge( - F.Range.from_center_rel(24 * P.MHz, 0.01) - ) - self.crystal_oscillator.crystal.frequency_tolerance.merge( - F.Range.upper_bound(50 * P.ppm) + self.crystal_oscillator.crystal.frequency.constrain_subset( + L.Range.from_center_rel(24 * P.MHz, 0.01) ) + self.crystal_oscillator.crystal.frequency_tolerance.constrain_le(50 * P.ppm) # TODO: ugly - self.crystal_oscillator.current_limiting_resistor.resistance.merge(0 * P.ohm) + self.crystal_oscillator.current_limiting_resistor.resistance.alias_is(0 * P.ohm) # usb transceiver bias resistor - self.bias_resistor.resistance.merge(F.Range.from_center_rel(12 * P.kohm, 0.01)) + self.bias_resistor.resistance.constrain_subset( + L.Range.from_center_rel(12 * P.kohm, 0.01) + ) for led in [self.suspend_indicator.led, self.power_3v3_indicator]: - led.led.color.merge(F.LED.Color.GREEN) - led.led.brightness.merge( - TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value + led.led.color.constrain_subset(F.LED.Color.GREEN) + led.led.brightness.constrain_subset( + TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value ) - self.ldo_3v3.output_voltage.merge(F.Range.from_center_rel(3.3 * P.V, 0.05)) - self.ldo_3v3.power_in.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.1) + self.ldo_3v3.output_voltage.constrain_subset( + L.Range.from_center_rel(3.3 * P.V, 0.05) + ) + self.ldo_3v3.power_in.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.1) ) - self.ldo_3v3.power_out.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.1) + self.ldo_3v3.power_out.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.1) ) # Hub controller power rails decoupling @@ -236,32 +238,32 @@ def __preinit__(self): F.MultiCapacitor(2) ) ) - regulator_decoupling_caps.capacitors[0].capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.05) + regulator_decoupling_caps.capacitors[0].capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.05) ) - regulator_decoupling_caps.capacitors[1].capacitance.merge( - F.Range.from_center_rel(4.7 * P.uF, 0.05) + regulator_decoupling_caps.capacitors[1].capacitance.constrain_subset( + L.Range.from_center_rel(4.7 * P.uF, 0.05) ) self.hub_controller.power_3v3_analog.decoupled.decouple().specialize( F.MultiCapacitor(4) - ).set_equal_capacitance_each(F.Range.from_center_rel(100 * P.nF, 0.05)) - self.hub_controller.power_pll.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.5) + ).set_equal_capacitance_each(L.Range.from_center_rel(100 * P.nF, 0.05)) + self.hub_controller.power_pll.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.5) ) - self.hub_controller.power_core.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.5) + self.hub_controller.power_core.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.5) ) - self.hub_controller.power_io.decoupled.decouple().capacitance.merge( - F.Range.from_center_rel(100 * P.nF, 0.5) + self.hub_controller.power_io.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.5) ) # VBUS detect for r in self.vbus_voltage_divider.resistor: - r.resistance.merge(F.Range.from_center_rel(100 * P.kohm, 0.01)) + r.resistance.constrain_subset(L.Range.from_center_rel(100 * P.kohm, 0.01)) # reset - self.hub_controller.reset.set_weak(on=True).resistance.merge( - F.Range.from_center_rel(100 * P.kohm, 0.01) + self.hub_controller.reset.set_weak(on=True).resistance.constrain_subset( + L.Range.from_center_rel(100 * P.kohm, 0.01) ) # ---------------------------------------- diff --git a/src/faebryk/library/USB2_0.py b/src/faebryk/library/USB2_0.py index 36048ff2..3b299b8d 100644 --- a/src/faebryk/library/USB2_0.py +++ b/src/faebryk/library/USB2_0.py @@ -3,6 +3,7 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface +from faebryk.libs.library import L from faebryk.libs.units import P @@ -10,4 +11,6 @@ class USB2_0(ModuleInterface): usb_if: F.USB2_0_IF def __preinit__(self): - self.usb_if.buspower.voltage.merge(F.Range.from_center(5 * P.V, 0.25 * P.V)) + self.usb_if.buspower.voltage.constrain_subset( + L.Range.from_center(5 * P.V, 0.25 * P.V) + ) diff --git a/src/faebryk/library/USB2_0_ESD_Protection.py b/src/faebryk/library/USB2_0_ESD_Protection.py index 8ef60fe6..58085b74 100644 --- a/src/faebryk/library/USB2_0_ESD_Protection.py +++ b/src/faebryk/library/USB2_0_ESD_Protection.py @@ -23,8 +23,10 @@ class USB2_0_ESD_Protection(Module): # ---------------------------------------- usb = L.list_field(2, F.USB2_0) - vbus_esd_protection: F.TBD - data_esd_protection: F.TBD + vbus_esd_protection = L.p_field(domain=L.Domains.BOOL()) + data_esd_protection = L.p_field(domain=L.Domains.BOOL()) + + no_pick: has_part_picked_remove # ---------------------------------------- # traits @@ -42,12 +44,15 @@ def __preinit__(self): # connections # ------------------------------------ self.usb[0].connect(self.usb[1]) + self.usb[0].usb_if.buspower.connect(self.usb[1].usb_if.buspower) self.usb[0].usb_if.buspower.decoupled.decouple() # ------------------------------------ # parametrization # ------------------------------------ - self.usb[0].usb_if.buspower.voltage.merge(F.Range(4.75 * P.V, 5.25 * P.V)) + self.usb[0].usb_if.buspower.voltage.constrain_subset( + L.Range(4.75 * P.V, 5.25 * P.V) + ) # TODO remove if adding any child modules has_part_picked_remove.mark_no_pick_needed(self) diff --git a/src/faebryk/library/USB3.py b/src/faebryk/library/USB3.py index 09be9d81..1ca27a1f 100644 --- a/src/faebryk/library/USB3.py +++ b/src/faebryk/library/USB3.py @@ -3,6 +3,7 @@ import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface +from faebryk.libs.library import L from faebryk.libs.units import P @@ -11,4 +12,6 @@ class USB3(ModuleInterface): def __preinit__(self): self.usb3_if.gnd_drain.connect(self.usb3_if.usb_if.buspower.lv) - self.usb3_if.usb_if.buspower.voltage.merge(F.Range(4.75 * P.V, 5.5 * P.V)) + self.usb3_if.usb_if.buspower.voltage.constrain_subset( + L.Range(4.75 * P.V, 5.5 * P.V) + ) diff --git a/src/faebryk/library/USB3_connector.py b/src/faebryk/library/USB3_connector.py index 9803c1be..e0a730d9 100644 --- a/src/faebryk/library/USB3_connector.py +++ b/src/faebryk/library/USB3_connector.py @@ -6,7 +6,6 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L -from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -16,8 +15,6 @@ class USB3_connector(Module): shield: F.Electrical def __preinit__(self): - self.usb3.usb3_if.usb_if.buspower.voltage.merge(F.Range(4.75 * P.V, 5.25 * P.V)) - self.usb3.usb3_if.usb_if.buspower.lv.connect(self.usb3.usb3_if.gnd_drain) designator_prefix = L.f_field(F.has_designator_prefix_defined)( diff --git a/src/faebryk/library/USB_C_5V_PSU.py b/src/faebryk/library/USB_C_5V_PSU.py index 7340601a..7bf6b1e3 100644 --- a/src/faebryk/library/USB_C_5V_PSU.py +++ b/src/faebryk/library/USB_C_5V_PSU.py @@ -16,7 +16,9 @@ class USB_C_5V_PSU(Module): configuration_resistors = L.list_field( 2, lambda: F.Resistor().builder( - lambda r: r.resistance.merge(F.Range.from_center_rel(5.1 * P.kohm, 0.05)) + lambda r: r.resistance.constrain_subset( + L.Range.from_center_rel(5.1 * P.kohm, 0.05) + ) ), ) diff --git a/src/faebryk/library/USB_C_PSU_Vertical.py b/src/faebryk/library/USB_C_PSU_Vertical.py index bba595e9..1e75f53b 100644 --- a/src/faebryk/library/USB_C_PSU_Vertical.py +++ b/src/faebryk/library/USB_C_PSU_Vertical.py @@ -23,20 +23,26 @@ class USB_C_PSU_Vertical(Module): fuse: F.Fuse def __preinit__(self): - self.gnd_capacitor.capacitance.merge(F.Range.from_center_rel(100 * P.nF, 0.05)) - self.gnd_capacitor.rated_voltage.merge(F.Range.from_center_rel(16 * P.V, 0.05)) - self.gnd_resistor.resistance.merge(F.Range.from_center_rel(1 * P.Mohm, 0.05)) + self.gnd_capacitor.capacitance.constrain_subset( + L.Range.from_center_rel(100 * P.nF, 0.05) + ) + self.gnd_capacitor.max_voltage.constrain_subset( + L.Range.from_center_rel(16 * P.V, 0.05) + ) + self.gnd_resistor.resistance.constrain_subset( + L.Range.from_center_rel(1 * P.Mohm, 0.05) + ) for res in self.configuration_resistors: - res.resistance.merge(F.Range.from_center_rel(5.1 * P.kohm, 0.05)) - self.fuse.fuse_type.merge(F.Fuse.FuseType.RESETTABLE) - self.fuse.trip_current.merge(F.Range.from_center_rel(1 * P.A, 0.05)) + res.resistance.constrain_subset(L.Range.from_center_rel(5.1 * P.kohm, 0.05)) + self.fuse.fuse_type.constrain_subset(F.Fuse.FuseType.RESETTABLE) + self.fuse.trip_current.constrain_subset(L.Range.from_center_rel(1 * P.A, 0.05)) # alliases vcon = self.usb_connector.vbus vusb = self.usb.usb_if.buspower v5 = self.power_out gnd = v5.lv - v5.voltage.merge(F.Range.from_center_rel(5 * P.V, 0.05)) + v5.voltage.constrain_superset(L.Range.from_center_rel(5 * P.V, 0.05)) vcon.hv.connect_via(self.fuse, v5.hv) vcon.lv.connect(gnd) diff --git a/src/faebryk/library/USB_RS485.py b/src/faebryk/library/USB_RS485.py index bb585106..6dceb68a 100644 --- a/src/faebryk/library/USB_RS485.py +++ b/src/faebryk/library/USB_RS485.py @@ -45,10 +45,12 @@ def __preinit__(self): self.uart_rs485.power.lv, ) - self.termination.resistance.merge(F.Range.from_center(150 * P.ohm, 1.5 * P.ohm)) - self.polarization[0].resistance.merge( - F.Range.from_center(680 * P.ohm, 6.8 * P.ohm) + self.termination.resistance.constrain_subset( + L.Range.from_center(150 * P.ohm, 1.5 * P.ohm) ) - self.polarization[1].resistance.merge( - F.Range.from_center(680 * P.ohm, 6.8 * P.ohm) + self.polarization[0].resistance.constrain_subset( + L.Range.from_center(680 * P.ohm, 6.8 * P.ohm) + ) + self.polarization[1].resistance.constrain_subset( + L.Range.from_center(680 * P.ohm, 6.8 * P.ohm) ) diff --git a/src/faebryk/library/USB_Type_C_Receptacle_14_pin_Vertical.py b/src/faebryk/library/USB_Type_C_Receptacle_14_pin_Vertical.py index 99a5e135..adf0849f 100644 --- a/src/faebryk/library/USB_Type_C_Receptacle_14_pin_Vertical.py +++ b/src/faebryk/library/USB_Type_C_Receptacle_14_pin_Vertical.py @@ -26,7 +26,7 @@ class USB_Type_C_Receptacle_14_pin_Vertical(Module): descriptive_properties = L.f_field(F.has_descriptive_properties_defined)( { - DescriptiveProperties.manufacturer.value: "Jing Extension of the Electronic Co.", # noqa: E501 + DescriptiveProperties.manufacturer: "Jing Extension of the Electronic Co.", # noqa: E501 DescriptiveProperties.partno: "918-418K2022Y40000", } ) diff --git a/src/faebryk/library/Wuxi_I_core_Elec_AiP74LVC1T45GB236_TR.py b/src/faebryk/library/Wuxi_I_core_Elec_AiP74LVC1T45GB236_TR.py index 4969d1a7..978b51d8 100644 --- a/src/faebryk/library/Wuxi_I_core_Elec_AiP74LVC1T45GB236_TR.py +++ b/src/faebryk/library/Wuxi_I_core_Elec_AiP74LVC1T45GB236_TR.py @@ -82,12 +82,12 @@ def __preinit__(self): # ------------------------------------ # parametrization # ------------------------------------ - self.power_a.voltage.merge(F.Range(1.2 * P.V, 5.5 * P.V)) - self.power_b.voltage.merge(F.Range(1.2 * P.V, 5.5 * P.V)) + self.power_a.voltage.constrain_subset(L.Range(1.2 * P.V, 5.5 * P.V)) + self.power_b.voltage.constrain_subset(L.Range(1.2 * P.V, 5.5 * P.V)) - self.power_a.decoupled.decouple().capacitance.merge( - F.Range.from_center(100 * P.nF, 10 * P.nF) + self.power_a.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center(100 * P.nF, 10 * P.nF) ) - self.power_b.decoupled.decouple().capacitance.merge( - F.Range.from_center(100 * P.nF, 10 * P.nF) + self.power_b.decoupled.decouple().capacitance.constrain_subset( + L.Range.from_center(100 * P.nF, 10 * P.nF) ) diff --git a/src/faebryk/library/XL_3528RGBW_WS2812B.py b/src/faebryk/library/XL_3528RGBW_WS2812B.py index bb45f3de..c90c6379 100644 --- a/src/faebryk/library/XL_3528RGBW_WS2812B.py +++ b/src/faebryk/library/XL_3528RGBW_WS2812B.py @@ -4,15 +4,14 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.libs.library import L +from faebryk.libs.units import P class XL_3528RGBW_WS2812B(Module): class _ws2812b_esphome_config(F.has_esphome_config.impl()): - update_interval: F.TBD + update_interval = L.p_field(units=P.s, tolerance_guess=0) def get_config(self) -> dict: - assert isinstance(self.update_interval, F.Constant) - obj = self.get_obj(XL_3528RGBW_WS2812B) data_pin = F.is_esphome_bus.find_connected_bus(obj.di.signal) @@ -21,7 +20,7 @@ def get_config(self) -> dict: "light": [ { "platform": "esp32_rmt_led_strip", - "update_interval": f"{self.update_interval.value.to('s')}", + "update_interval": self.update_interval, "num_leds": 1, # TODO: make dynamic "rmt_channel": 0, # TODO: make dynamic "chipset": "WS2812", @@ -32,12 +31,6 @@ def get_config(self) -> dict: ] } - def is_implemented(self): - return ( - isinstance(self.update_interval.get_most_narrow(), F.Constant) - and super().is_implemented() - ) - # interfaces power: F.ElectricPower diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 880cef3c..4f96dba8 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -15,23 +15,19 @@ # flake8: noqa: I001 # flake8: noqa: E501 -from faebryk.library.TBD import TBD -from faebryk.library.Range import Range from faebryk.library.has_designator_prefix import has_designator_prefix from faebryk.library.has_pcb_position import has_pcb_position -from faebryk.library.Constant import Constant +from faebryk.library.Electrical import Electrical from faebryk.library.has_esphome_config import has_esphome_config from faebryk.library.is_esphome_bus import is_esphome_bus from faebryk.library.has_construction_dependency import has_construction_dependency from faebryk.library.has_single_electric_reference import has_single_electric_reference from faebryk.library.can_specialize_defined import can_specialize_defined from faebryk.library.Power import Power -from faebryk.library.is_dynamic_by_connections import is_dynamic_by_connections from faebryk.library.Signal import Signal from faebryk.library.has_footprint import has_footprint from faebryk.library.Mechanical import Mechanical from faebryk.library.has_overriden_name import has_overriden_name -from faebryk.library.Operation import Operation from faebryk.library.has_linked_pad import has_linked_pad from faebryk.library.has_reference import has_reference from faebryk.library.can_bridge import can_bridge @@ -48,14 +44,14 @@ from faebryk.library.has_pcb_routing_strategy import has_pcb_routing_strategy from faebryk.library.has_resistance import has_resistance from faebryk.library.has_single_connection import has_single_connection +from faebryk.library.is_dynamic import is_dynamic from faebryk.library.is_representable_by_single_value import is_representable_by_single_value -from faebryk.library.ANY import ANY -from faebryk.library.Electrical import Electrical from faebryk.library.has_designator_prefix_defined import has_designator_prefix_defined from faebryk.library.has_pcb_position_defined import has_pcb_position_defined from faebryk.library.has_pcb_position_defined_relative import has_pcb_position_defined_relative from faebryk.library.has_pcb_position_defined_relative_to_parent import has_pcb_position_defined_relative_to_parent -from faebryk.library.Set import Set +from faebryk.library.XtalIF import XtalIF +from faebryk.library.has_pin_association_heuristic import has_pin_association_heuristic from faebryk.library.has_esphome_config_defined import has_esphome_config_defined from faebryk.library.is_esphome_bus_defined import is_esphome_bus_defined from faebryk.library.has_single_electric_reference_defined import has_single_electric_reference_defined @@ -64,6 +60,7 @@ from faebryk.library.Footprint import Footprint from faebryk.library.has_overriden_name_defined import has_overriden_name_defined from faebryk.library.has_linked_pad_defined import has_linked_pad_defined +from faebryk.library.Symbol import Symbol from faebryk.library.can_bridge_defined import can_bridge_defined from faebryk.library.has_descriptive_properties_defined import has_descriptive_properties_defined from faebryk.library.has_datasheet_defined import has_datasheet_defined @@ -74,22 +71,23 @@ from faebryk.library.has_multi_picker import has_multi_picker from faebryk.library.has_pcb_layout_defined import has_pcb_layout_defined from faebryk.library.has_single_connection_impl import has_single_connection_impl +from faebryk.library.is_dynamic_by_connections import is_dynamic_by_connections from faebryk.library.is_representable_by_single_value_defined import is_representable_by_single_value_defined -from faebryk.library.Symbol import Symbol -from faebryk.library.XtalIF import XtalIF -from faebryk.library.has_pin_association_heuristic import has_pin_association_heuristic from faebryk.library.PJ398SM import PJ398SM from faebryk.library.RJ45_Receptacle import RJ45_Receptacle +from faebryk.library.has_pin_association_heuristic_lookup_table import has_pin_association_heuristic_lookup_table from faebryk.library.LogicOps import LogicOps from faebryk.library.can_attach_to_footprint import can_attach_to_footprint from faebryk.library.can_attach_via_pinmap import can_attach_via_pinmap from faebryk.library.has_footprint_impl import has_footprint_impl from faebryk.library.has_kicad_footprint import has_kicad_footprint from faebryk.library.Pad import Pad +from faebryk.library.has_symbol_layout import has_symbol_layout from faebryk.library.Button import Button from faebryk.library.GDT import GDT -from faebryk.library.has_symbol_layout import has_symbol_layout -from faebryk.library.has_pin_association_heuristic_lookup_table import has_pin_association_heuristic_lookup_table +from faebryk.library.BJT import BJT +from faebryk.library.Diode import Diode +from faebryk.library.MOSFET import MOSFET from faebryk.library.LogicGate import LogicGate from faebryk.library.has_footprint_defined import has_footprint_defined from faebryk.library.Net import Net @@ -98,9 +96,7 @@ from faebryk.library.has_kicad_manual_footprint import has_kicad_manual_footprint from faebryk.library.has_pcb_routing_strategy_greedy_direct_line import has_pcb_routing_strategy_greedy_direct_line from faebryk.library.has_symbol_layout_defined import has_symbol_layout_defined -from faebryk.library.BJT import BJT -from faebryk.library.Diode import Diode -from faebryk.library.MOSFET import MOSFET +from faebryk.library.TVS import TVS from faebryk.library.LogicGates import LogicGates from faebryk.library.can_attach_to_footprint_symmetrically import can_attach_to_footprint_symmetrically from faebryk.library.can_attach_to_footprint_via_pinmap import can_attach_to_footprint_via_pinmap @@ -110,7 +106,8 @@ from faebryk.library.has_equal_pins_in_ifs import has_equal_pins_in_ifs from faebryk.library.has_kicad_footprint_equal_ifs import has_kicad_footprint_equal_ifs from faebryk.library.KicadFootprint import KicadFootprint -from faebryk.library.TVS import TVS +from faebryk.library.can_be_surge_protected import can_be_surge_protected +from faebryk.library.is_surge_protected import is_surge_protected from faebryk.library.Capacitor import Capacitor from faebryk.library.Crystal import Crystal from faebryk.library.Fuse import Fuse @@ -127,8 +124,7 @@ from faebryk.library.SOIC import SOIC from faebryk.library.has_kicad_footprint_equal_ifs_defined import has_kicad_footprint_equal_ifs_defined from faebryk.library.Mounting_Hole import Mounting_Hole -from faebryk.library.can_be_surge_protected import can_be_surge_protected -from faebryk.library.is_surge_protected import is_surge_protected +from faebryk.library.is_surge_protected_defined import is_surge_protected_defined from faebryk.library.MultiCapacitor import MultiCapacitor from faebryk.library.can_be_decoupled import can_be_decoupled from faebryk.library.is_decoupled import is_decoupled @@ -137,9 +133,8 @@ from faebryk.library.Potentiometer import Potentiometer from faebryk.library.ResistorVoltageDivider import ResistorVoltageDivider from faebryk.library.Resistor_Voltage_Divider import Resistor_Voltage_Divider -from faebryk.library.is_surge_protected_defined import is_surge_protected_defined -from faebryk.library.is_decoupled_nodes import is_decoupled_nodes from faebryk.library.can_be_surge_protected_defined import can_be_surge_protected_defined +from faebryk.library.is_decoupled_nodes import is_decoupled_nodes from faebryk.library.can_be_decoupled_defined import can_be_decoupled_defined from faebryk.library.ElectricPower import ElectricPower from faebryk.library.B0505S_1WR3 import B0505S_1WR3 diff --git a/src/faebryk/library/can_switch_power_defined.py b/src/faebryk/library/can_switch_power_defined.py index 5feffed5..5d892a86 100644 --- a/src/faebryk/library/can_switch_power_defined.py +++ b/src/faebryk/library/can_switch_power_defined.py @@ -17,7 +17,7 @@ def __init__( self.out_power = out_power self.in_logic = in_logic - out_power.voltage.merge(in_power.voltage) + out_power.voltage.alias_is(in_power.voltage) def get_logic_in(self) -> F.ElectricLogic: return self.in_logic diff --git a/src/faebryk/library/has_multi_picker.py b/src/faebryk/library/has_multi_picker.py index 49d7db82..4229d200 100644 --- a/src/faebryk/library/has_multi_picker.py +++ b/src/faebryk/library/has_multi_picker.py @@ -4,11 +4,12 @@ import logging from abc import abstractmethod -from typing import Any, Callable, Mapping +from typing import Callable, Mapping import faebryk.library._F as F from faebryk.core.module import Module from faebryk.core.node import Node +from faebryk.core.solver import Solver from faebryk.core.trait import TraitImpl from faebryk.libs.picker.picker import PickError @@ -41,11 +42,12 @@ def __init__(self, prio: int, picker: Picker): def __preinit__(self): ... class FunctionPicker(Picker): - def __init__(self, picker: Callable[[Module], Any]): + def __init__(self, picker: Callable[[Module, Solver], None], solver: Solver): self.picker = picker + self.solver = solver def pick(self, module: Module) -> None: - self.picker(module) + self.picker(module, self.solver) def __repr__(self) -> str: return f"{type(self).__name__}({self.picker.__name__})" diff --git a/src/faebryk/library/has_simple_value_representation_based_on_params.py b/src/faebryk/library/has_simple_value_representation_based_on_params.py index 31a2c255..9c576aef 100644 --- a/src/faebryk/library/has_simple_value_representation_based_on_params.py +++ b/src/faebryk/library/has_simple_value_representation_based_on_params.py @@ -20,6 +20,6 @@ def __init__[*P]( assert all(isinstance(p, Parameter) for p in params) self.params = params + # TODO make this more useful def get_value(self) -> str: - params_const = tuple(param.get_most_narrow() for param in self.params) - return self.transformer(*params_const) + return self.transformer(*self.params) diff --git a/src/faebryk/library/is_dynamic.py b/src/faebryk/library/is_dynamic.py new file mode 100644 index 00000000..4ef6fa59 --- /dev/null +++ b/src/faebryk/library/is_dynamic.py @@ -0,0 +1,14 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +from faebryk.core.parameter import Parameter + + +class is_dynamic(Parameter.TraitT): + """ + Marks a parameter as dynamic, meaning that it needs to be re-evaluatated before use. + Current use only for parameters related through bus connections. + """ + + def exec(self): + pass diff --git a/src/faebryk/library/is_dynamic_by_connections.py b/src/faebryk/library/is_dynamic_by_connections.py index 678aec92..ef15090e 100644 --- a/src/faebryk/library/is_dynamic_by_connections.py +++ b/src/faebryk/library/is_dynamic_by_connections.py @@ -4,6 +4,7 @@ import logging from typing import Callable +import faebryk.library._F as F from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import NodeException from faebryk.core.parameter import Parameter @@ -12,7 +13,7 @@ logger = logging.getLogger(__name__) -class is_dynamic_by_connections(Parameter.is_dynamic.impl()): +class is_dynamic_by_connections(F.is_dynamic.impl()): def __init__(self, key: Callable[[ModuleInterface], Parameter]) -> None: super().__init__() self._key = key @@ -40,9 +41,7 @@ def exec_for_mifs(self, mifs: set[ModuleInterface]): params_with_guard = [ ( param, - cast_assert( - is_dynamic_by_connections, param.get_trait(Parameter.is_dynamic) - ), + cast_assert(is_dynamic_by_connections, param.get_trait(F.is_dynamic)), ) for param in params ] @@ -57,7 +56,7 @@ def exec_for_mifs(self, mifs: set[ModuleInterface]): if id(param) in self._merged: continue self._merged.add(id(param)) - self_param.merge(param) + self_param.alias_is(param) # Enable guards again for _, guard in params_with_guard: diff --git a/src/faebryk/library/is_esphome_bus.py b/src/faebryk/library/is_esphome_bus.py index 48212e80..ed16cd42 100644 --- a/src/faebryk/library/is_esphome_bus.py +++ b/src/faebryk/library/is_esphome_bus.py @@ -4,7 +4,7 @@ from abc import abstractmethod from faebryk.core.moduleinterface import ModuleInterface -from faebryk.libs.util import find +from faebryk.libs.util import cast_assert, find class is_esphome_bus(ModuleInterface.TraitT): @@ -14,9 +14,12 @@ class is_esphome_bus(ModuleInterface.TraitT): def get_bus_id(self) -> str: ... @staticmethod - def find_connected_bus(bus: ModuleInterface): - connected_mifs = list(bus.get_connected()) + def find_connected_bus[T: ModuleInterface](bus: T) -> T: + connected_mifs = bus.get_connected() try: - return find(connected_mifs, lambda mif: mif.has_trait(is_esphome_bus)) + return cast_assert( + type(bus), + find(connected_mifs, lambda mif: mif.has_trait(is_esphome_bus)), + ) except ValueError: raise Exception(f"No esphome bus connected to {bus}: {connected_mifs}") diff --git a/src/faebryk/library/pf_74AHCT2G125.py b/src/faebryk/library/pf_74AHCT2G125.py index 5d426706..881a9590 100644 --- a/src/faebryk/library/pf_74AHCT2G125.py +++ b/src/faebryk/library/pf_74AHCT2G125.py @@ -37,7 +37,7 @@ def attach_to_footprint(self): ) def __preinit__(self): - self.power.voltage.merge(F.Range(4.5 * P.V, 5.5 * P.V)) + self.power.voltage.constrain_subset(L.Range(4.5 * P.V, 5.5 * P.V)) self.power.decoupled.decouple() @L.rt_field diff --git a/src/faebryk/libs/app/erc.py b/src/faebryk/libs/app/erc.py index 8295a9b6..d5b61b8b 100644 --- a/src/faebryk/libs/app/erc.py +++ b/src/faebryk/libs/app/erc.py @@ -9,8 +9,8 @@ from faebryk.core.graph import Graph, GraphFunctions from faebryk.core.module import Module from faebryk.core.moduleinterface import ModuleInterface -from faebryk.library.Operation import Operation from faebryk.libs.picker.picker import has_part_picked +from faebryk.libs.units import P from faebryk.libs.util import groupby logger = logging.getLogger(__name__) @@ -31,12 +31,12 @@ def __init__(self, faulting_ifs: Sequence[ModuleInterface], *args: object) -> No class ERCFaultElectricPowerUndefinedVoltage(ERCFault): - def __init__(self, faulting_EP: list[F.ElectricPower], *args: object) -> None: - faulting_EP = list(sorted(faulting_EP, key=lambda ep: ep.get_name())) - msg = "ElectricPower(s) with undefined or unsolved voltage: " + ",\n ".join( - f"{ep}: {ep.voltage}" for ep in faulting_EP + def __init__(self, faulting_EP: F.ElectricPower, *args: object) -> None: + msg = ( + f"ElectricPower with undefined or unsolved voltage: {faulting_EP}:" + f" {faulting_EP.voltage}" ) - super().__init__(faulting_EP, msg, *args) + super().__init__([faulting_EP], msg, *args) class ERCPowerSourcesShortedError(ERCFault): @@ -45,7 +45,7 @@ class ERCPowerSourcesShortedError(ERCFault): """ -def simple_erc(G: Graph): +def simple_erc(G: Graph, voltage_limit=1e5 * P.V): """Simple ERC check. This function will check for the following ERC violations: @@ -80,14 +80,14 @@ def simple_erc(G: Graph): if other_sources: raise ERCPowerSourcesShortedError([ep] + other_sources) - unresolved_voltage = [ - ep - for ep in electricpower - if isinstance(ep.voltage.get_most_narrow(), (F.TBD, Operation)) - ] + for ep in electricpower: + if ep.voltage.inspect_known_max() > voltage_limit: + + def raise_on_limit(x): + if x.inspect_known_max() > voltage_limit: + raise ERCFaultElectricPowerUndefinedVoltage(ep) - if unresolved_voltage: - raise ERCFaultElectricPowerUndefinedVoltage(unresolved_voltage) + ep.voltage.inspect_add_on_solution(raise_on_limit) # shorted nets nets = GraphFunctions(G).nodes_of_type(F.Net) diff --git a/src/faebryk/libs/app/parameters.py b/src/faebryk/libs/app/parameters.py index 7d8640a2..cbcfde8b 100644 --- a/src/faebryk/libs/app/parameters.py +++ b/src/faebryk/libs/app/parameters.py @@ -9,7 +9,6 @@ import faebryk.library._F as F from faebryk.core.cpp import Graph from faebryk.core.graph import GraphFunctions -from faebryk.core.module import Module from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.parameter import Parameter from faebryk.libs.test.times import Times @@ -18,45 +17,18 @@ logger = logging.getLogger(__name__) -def replace_tbd_with_any(module: Module, recursive: bool, loglvl: int | None = None): - """ - Replace all F.TBD instances with F.ANY instances in the given module. - - :param module: The module to replace F.TBD instances in. - :param recursive: If True, replace F.TBD instances in submodules as well. - """ - lvl = logger.getEffectiveLevel() - if loglvl is not None: - logger.setLevel(loglvl) - - module = module.get_most_special() - - for param in module.get_children(direct_only=True, types=Parameter): - if isinstance(param.get_most_narrow(), F.TBD): - logger.debug(f"Replacing in {module}: {param} with F.ANY") - param.merge(F.ANY()) - - logger.setLevel(lvl) - - if recursive: - for m in module.get_children_modules(types=Module): - replace_tbd_with_any(m, recursive=False, loglvl=loglvl) - - def resolve_dynamic_parameters(graph: Graph): other_dynamic_params, connection_dynamic_params = partition( lambda param_trait: isinstance(param_trait[1], F.is_dynamic_by_connections), [ (param, trait) - for param, trait in GraphFunctions(graph).nodes_with_trait( - Parameter.is_dynamic - ) + for param, trait in GraphFunctions(graph).nodes_with_trait(F.is_dynamic) ], ) # non-connection for _, trait in other_dynamic_params: - trait.execute() + trait.exec() # connection _resolve_dynamic_parameters_connection( diff --git a/src/faebryk/libs/brightness.py b/src/faebryk/libs/brightness.py index 48804ed2..7bc71040 100644 --- a/src/faebryk/libs/brightness.py +++ b/src/faebryk/libs/brightness.py @@ -1,12 +1,10 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -from copy import copy from enum import Enum -import faebryk.library._F as F -from faebryk.core.parameter import Parameter -from faebryk.libs.units import P +from faebryk.libs.library import L +from faebryk.libs.units import P, Quantity """ luminous intensity in candela (candela) @@ -35,43 +33,20 @@ """ -class _Unit: - def __init__(self, value: Parameter): - self._value = value +def luminous_flux_to_intensity(flux: Quantity, solid_angle: Quantity) -> Quantity: + return flux / solid_angle - def __repr__(self): - return f"{self._value!r}" - @property - def value(self): - return copy(self._value) +def luminous_intensity_to_flux(intensity: Quantity, solid_angle: Quantity) -> Quantity: + return intensity * solid_angle -# Temporary unit classes until faebryk supports units -class LuminousIntensity(_Unit): - pass +def luminous_flux_to_illuminance(flux: Quantity, area: Quantity) -> Quantity: + return flux / area -class LuminousFlux(_Unit): - @classmethod - def from_intensity( - cls, - intensity: LuminousIntensity, - solid_angle: Parameter, - ) -> "LuminousFlux": - return LuminousFlux(intensity.value * solid_angle) - - def to_intensity(self, solid_angle: Parameter) -> LuminousIntensity: - return LuminousIntensity(self.value / solid_angle) - - -class Illuminance(_Unit): - @classmethod - def from_flux(cls, flux: LuminousFlux, area: Parameter) -> "Illuminance": - return Illuminance(flux.value / area) - - def to_luminous_flux(self, area: Parameter) -> LuminousFlux: - return LuminousFlux(self.value * area) +def illuminance_to_flux(illuminance: Quantity, area: Quantity) -> Quantity: + return illuminance * area class TypicalLuminousIntensity(Enum): @@ -79,50 +54,36 @@ class TypicalLuminousIntensity(Enum): Well known luminous intensities in candela. """ - CANDLE = LuminousFlux(F.Constant(1 * P.candela)) - - CREE_SMD_LED_EXTREMELY_DIM = LuminousFlux(F.Constant(10 * P.millicandela)) - CREE_SMD_LED_VERY_DIM = LuminousFlux(F.Constant(25 * P.millicandela)) - CREE_SMD_LED_DIM = LuminousFlux(F.Constant(50 * P.millicandela)) - CREE_SMD_LED_NORMAL = LuminousFlux(F.Constant(100 * P.millicandela)) - CREE_SMD_LED_BRIGHT = LuminousFlux(F.Constant(250 * P.millicandela)) - CREE_SMD_LED_VERY_BRIGHT = LuminousFlux(F.Constant(2 * P.candela)) - CREE_SMD_LED_EXTREMELY_BRIGHT = LuminousFlux(F.Constant(14 * P.candela)) - - TYPICAL_SMD_LED_MAX_BRIGHTNESS = LuminousFlux( - F.Range(60 * P.millicandela, 800 * P.mcandela) - ) - - WS2812B_LED_RED = LuminousFlux(F.Constant(420 * P.millicandela)) - WS2812B_LED_GREEN = LuminousFlux(F.Constant(720 * P.millicandela)) - WS2812B_LED_BLUE = LuminousFlux(F.Constant(200 * P.millicandela)) - - APPLICATION_CAR_HEADLIGHTS_HALOGEN_LOW_BEAM_MEDIUM = LuminousFlux( - F.Constant(20 * P.kcandela) - ) - APPLICATION_CAR_HEADLIGHTS_HALOGEN_HIGH_BEAM_MEDIUM = LuminousFlux( - F.Constant(40 * P.kcandela) - ) - APPLICATION_CAR_TURN_INDICATOR_DIM = LuminousFlux(F.Constant(1 * P.kcandela)) - APPLICATION_CAR_TURN_INDICATOR_BRIGHT = LuminousFlux(F.Constant(10 * P.kcandela)) - APPLICATION_CAR_BREAK_LIGHT_DIM = LuminousFlux(F.Constant(5 * P.kcandela)) - APPLICATION_CAR_BREAK_LIGHT_BRIGHT = LuminousFlux(F.Constant(50 * P.kcandela)) + CANDLE = 1 * P.candela + + CREE_SMD_LED_EXTREMELY_DIM = 10 * P.millicandela + CREE_SMD_LED_VERY_DIM = 25 * P.millicandela + CREE_SMD_LED_DIM = 50 * P.millicandela + CREE_SMD_LED_NORMAL = 100 * P.millicandela + CREE_SMD_LED_BRIGHT = 250 * P.millicandela + CREE_SMD_LED_VERY_BRIGHT = 2 * P.candela + CREE_SMD_LED_EXTREMELY_BRIGHT = 14 * P.candela + + TYPICAL_SMD_LED_MAX_BRIGHTNESS = L.Range(60 * P.millicandela, 800 * P.millicandela) + + WS2812B_LED_RED = 420 * P.millicandela + WS2812B_LED_GREEN = 720 * P.millicandela + WS2812B_LED_BLUE = 200 * P.millicandela + + APPLICATION_CAR_HEADLIGHTS_HALOGEN_LOW_BEAM_MEDIUM = 20 * P.kcandela + APPLICATION_CAR_HEADLIGHTS_HALOGEN_HIGH_BEAM_MEDIUM = 40 * P.kcandela + APPLICATION_CAR_TURN_INDICATOR_DIM = 1 * P.kcandela + APPLICATION_CAR_TURN_INDICATOR_BRIGHT = 10 * P.kcandela + APPLICATION_CAR_BREAK_LIGHT_DIM = 5 * P.kcandela + APPLICATION_CAR_BREAK_LIGHT_BRIGHT = 50 * P.kcandela # not sure about these values - APPLICATION_LED_STANDBY = LuminousFlux(F.Range(1 * P.millicandela, 10 * P.mcandela)) - APPLICATION_LED_INDICATOR_INSIDE = LuminousFlux( - F.Range(10 * P.millicandela, 100 * P.mcandela) - ) - APPLICATION_LED_KEYBOARD_BACKLIGHT = LuminousFlux( - F.Range(50 * P.millicandela, 500 * P.mcandela) - ) - APPLICATION_LED_INDICATOR_OUTSIDE = LuminousFlux( - F.Range(100 * P.millicandela, 1 * P.candela) - ) - APPLICATION_LED_DECORATIVE_LIGHTING = LuminousFlux( - F.Range(100 * P.millicandela, 1 * P.candela) - ) - APPLICATION_LED_FLASHLIGHT = LuminousFlux(F.Range(10 * P.candela, 1 * P.kcandela)) + APPLICATION_LED_STANDBY = L.Range(1 * P.millicandela, 10 * P.mcandela) + APPLICATION_LED_INDICATOR_INSIDE = L.Range(10 * P.millicandela, 100 * P.mcandela) + APPLICATION_LED_KEYBOARD_BACKLIGHT = L.Range(50 * P.millicandela, 500 * P.mcandela) + APPLICATION_LED_INDICATOR_OUTSIDE = L.Range(100 * P.millicandela, 1 * P.candela) + APPLICATION_LED_DECORATIVE_LIGHTING = L.Range(100 * P.millicandela, 1 * P.candela) + APPLICATION_LED_FLASHLIGHT = L.Range(10 * P.candela, 1 * P.kcandela) class TypicalLuminousFlux(Enum): @@ -130,21 +91,21 @@ class TypicalLuminousFlux(Enum): Well known luminous flux in lumen. """ - IKEA_E14_BULB_LED_DIM = LuminousFlux(F.Constant(100 * P.lm)) - IKEA_E14_BULB_LED_MEDIUM = LuminousFlux(F.Constant(250 * P.lm)) - IKEA_E14_BULB_LED_BRIGHT = LuminousFlux(F.Constant(470 * P.lm)) - IKEA_GU10_BULB_LED_DIM = LuminousFlux(F.Constant(230 * P.lm)) - IKEA_GU10_BULB_LED_MEDIUM = LuminousFlux(F.Constant(345 * P.lm)) - IKEA_E27_BULB_LED_DIM = LuminousFlux(F.Constant(470 * P.lm)) - IKEA_E27_BULB_LED_MEDIUM = LuminousFlux(F.Constant(806 * P.lm)) - IKEA_E27_BULB_LED_BRIGHT = LuminousFlux(F.Constant(1500 * P.lm)) + IKEA_E14_BULB_LED_DIM = 100 * P.lm + IKEA_E14_BULB_LED_MEDIUM = 250 * P.lm + IKEA_E14_BULB_LED_BRIGHT = 470 * P.lm + IKEA_GU10_BULB_LED_DIM = 230 * P.lm + IKEA_GU10_BULB_LED_MEDIUM = 345 * P.lm + IKEA_E27_BULB_LED_DIM = 470 * P.lm + IKEA_E27_BULB_LED_MEDIUM = 806 * P.lm + IKEA_E27_BULB_LED_BRIGHT = 1500 * P.lm - CREE_SMD_LED_VERY_BRIGHT = LuminousFlux(F.Constant(6000 * P.lm)) + CREE_SMD_LED_VERY_BRIGHT = 6000 * P.lm - LASER_POINTER_GREEN_5MW = LuminousFlux(F.Constant(3.4 * P.lm)) + LASER_POINTER_GREEN_5MW = 3.4 * P.lm - CAR_HEADLIGHTS_HALOGEN_LOW_BEAM_MEDIUM = LuminousFlux(F.Constant(1000 * P.lm)) - CAR_HEADLIGHTS_HALOGEN_HIGH_BEAM_MEDIUM = LuminousFlux(F.Constant(1300 * P.lm)) + CAR_HEADLIGHTS_HALOGEN_LOW_BEAM_MEDIUM = 1000 * P.lm + CAR_HEADLIGHTS_HALOGEN_HIGH_BEAM_MEDIUM = 1300 * P.lm class TypicalIlluminance(Enum): @@ -153,17 +114,17 @@ class TypicalIlluminance(Enum): """ # https://en.wikipedia.org/wiki/Lux - MOONLESS_OVERCAST_NIGHT_SKY_STARLIGHT = Illuminance(F.Constant(0.0001 * P.lx)) - MOONLESS_CLEAR_NIGHT_SKY_WITH_AIRGLOW = Illuminance(F.Constant(0.002 * P.lx)) - FULL_MOON_ON_A_CLEAR_NIGHT = Illuminance(F.Constant(0.05 * P.lx)) - DARK_LIMIT_OF_CIVIL_TWILIGHT_UNDER_A_CLEAR_SKY = Illuminance(F.Constant(3.4 * P.lx)) - PUBLIC_AREAS_WITH_DARK_SURROUNDINGS = Illuminance(F.Constant(20 * P.lx)) - FAMILY_LIVING_ROOM_LIGHTS = Illuminance(F.Constant(50 * P.lx)) - OFFICE_BUILDING_HALLWAY_TOILET_LIGHTING = Illuminance(F.Constant(80 * P.lx)) - VERY_DARK_OVERCAST_DAY = Illuminance(F.Constant(100 * P.lx)) - TRAIN_STATION_PLATFORMS = Illuminance(F.Constant(150 * P.lx)) - OFFICE_LIGHTING = Illuminance(F.Constant(320 * P.lx)) - SUNRISE_OR_SUNSET_ON_A_CLEAR_DAY = Illuminance(F.Constant(400 * P.lx)) - OVERCAST_DAY = Illuminance(F.Constant(1000 * P.lx)) - FULL_DAYLIGHT = Illuminance(F.Constant(25000 * P.lx)) - DIRECT_SUNLIGHT = Illuminance(F.Constant(100000 * P.lx)) + MOONLESS_OVERCAST_NIGHT_SKY_STARLIGHT = 0.0001 * P.lx + MOONLESS_CLEAR_NIGHT_SKY_WITH_AIRGLOW = 0.002 * P.lx + FULL_MOON_ON_A_CLEAR_NIGHT = 0.05 * P.lx + DARK_LIMIT_OF_CIVIL_TWILIGHT_UNDER_A_CLEAR_SKY = 3.4 * P.lx + PUBLIC_AREAS_WITH_DARK_SURROUNDINGS = 20 * P.lx + FAMILY_LIVING_ROOM_LIGHTS = 50 * P.lx + OFFICE_BUILDING_HALLWAY_TOILET_LIGHTING = 80 * P.lx + VERY_DARK_OVERCAST_DAY = 100 * P.lx + TRAIN_STATION_PLATFORMS = 150 * P.lx + OFFICE_LIGHTING = 320 * P.lx + SUNRISE_OR_SUNSET_ON_A_CLEAR_DAY = 400 * P.lx + OVERCAST_DAY = 1000 * P.lx + FULL_DAYLIGHT = 25000 * P.lx + DIRECT_SUNLIGHT = 100000 * P.lx diff --git a/src/faebryk/libs/e_series.py b/src/faebryk/libs/e_series.py index fad7be80..a72afec4 100644 --- a/src/faebryk/libs/e_series.py +++ b/src/faebryk/libs/e_series.py @@ -1,518 +1,510 @@ -import copy import logging -import math +from collections.abc import Sequence from math import ceil, floor, log10 -from typing import Tuple +from typing import Tuple, TypeVar, cast -import faebryk.library._F as F -from faebryk.core.parameter import Parameter -from faebryk.libs.units import Quantity +from faebryk.libs.library import L +from faebryk.libs.sets import Range, Ranges +from faebryk.libs.units import Quantity, Unit, dimensionless +from faebryk.libs.util import once logger = logging.getLogger(__name__) -E_SERIES = set[float] +E_SERIES = frozenset[float] class E_SERIES_VALUES: - E192 = { - 1.00, - 1.01, - 1.02, - 1.04, - 1.05, - 1.06, - 1.07, - 1.09, - 1.10, - 1.11, - 1.13, - 1.14, - 1.15, - 1.17, - 1.18, - 1.20, - 1.21, - 1.23, - 1.24, - 1.26, - 1.27, - 1.29, - 1.30, - 1.32, - 1.33, - 1.35, - 1.37, - 1.38, - 1.40, - 1.42, - 1.43, - 1.45, - 1.47, - 1.49, - 1.50, - 1.52, - 1.54, - 1.56, - 1.58, - 1.60, - 1.62, - 1.64, - 1.65, - 1.67, - 1.69, - 1.72, - 1.74, - 1.76, - 1.78, - 1.80, - 1.82, - 1.84, - 1.87, - 1.89, - 1.91, - 1.93, - 1.96, - 1.98, - 2.00, - 2.03, - 2.05, - 2.08, - 2.10, - 2.13, - 2.15, - 2.18, - 2.21, - 2.23, - 2.26, - 2.29, - 2.32, - 2.34, - 2.37, - 2.40, - 2.43, - 2.46, - 2.49, - 2.52, - 2.55, - 2.58, - 2.61, - 2.64, - 2.67, - 2.71, - 2.74, - 2.77, - 2.80, - 2.84, - 2.87, - 2.91, - 2.94, - 2.98, - 3.01, - 3.05, - 3.09, - 3.12, - 3.16, - 3.20, - 3.24, - 3.28, - 3.32, - 3.36, - 3.40, - 3.44, - 3.48, - 3.52, - 3.57, - 3.61, - 3.65, - 3.70, - 3.74, - 3.79, - 3.83, - 3.88, - 3.92, - 3.97, - 4.02, - 4.07, - 4.12, - 4.17, - 4.22, - 4.27, - 4.32, - 4.37, - 4.42, - 4.48, - 4.53, - 4.59, - 4.64, - 4.70, - 4.75, - 4.81, - 4.87, - 4.93, - 4.99, - 5.05, - 5.11, - 5.17, - 5.23, - 5.30, - 5.36, - 5.42, - 5.49, - 5.56, - 5.62, - 5.69, - 5.76, - 5.83, - 5.90, - 5.97, - 6.04, - 6.12, - 6.19, - 6.26, - 6.34, - 6.42, - 6.49, - 6.57, - 6.65, - 6.73, - 6.81, - 6.90, - 6.98, - 7.06, - 7.15, - 7.23, - 7.32, - 7.41, - 7.50, - 7.59, - 7.68, - 7.77, - 7.87, - 7.96, - 8.06, - 8.16, - 8.25, - 8.35, - 8.45, - 8.56, - 8.66, - 8.76, - 8.87, - 8.98, - 9.09, - 9.20, - 9.31, - 9.42, - 9.53, - 9.65, - 9.76, - 9.88, - } - - E96 = { - 1.00, - 1.02, - 1.05, - 1.07, - 1.10, - 1.13, - 1.15, - 1.18, - 1.21, - 1.24, - 1.27, - 1.30, - 1.33, - 1.37, - 1.40, - 1.43, - 1.47, - 1.50, - 1.54, - 1.58, - 1.62, - 1.65, - 1.69, - 1.74, - 1.78, - 1.82, - 1.87, - 1.91, - 1.96, - 2.00, - 2.05, - 2.10, - 2.15, - 2.21, - 2.26, - 2.32, - 2.37, - 2.43, - 2.49, - 2.55, - 2.61, - 2.67, - 2.74, - 2.80, - 2.87, - 2.94, - 3.01, - 3.09, - 3.16, - 3.24, - 3.32, - 3.40, - 3.48, - 3.57, - 3.65, - 3.74, - 3.83, - 3.92, - 4.02, - 4.12, - 4.22, - 4.32, - 4.42, - 4.53, - 4.64, - 4.75, - 4.87, - 4.99, - 5.11, - 5.23, - 5.36, - 5.49, - 5.62, - 5.76, - 5.90, - 6.04, - 6.19, - 6.34, - 6.49, - 6.65, - 6.81, - 6.98, - 7.15, - 7.32, - 7.50, - 7.68, - 7.87, - 8.06, - 8.25, - 8.45, - 8.66, - 8.87, - 9.09, - 9.31, - 9.53, - 9.76, - } - - E48 = { - 1.00, - 1.05, - 1.10, - 1.15, - 1.21, - 1.27, - 1.33, - 1.40, - 1.47, - 1.54, - 1.62, - 1.69, - 1.78, - 1.87, - 1.96, - 2.05, - 2.15, - 2.26, - 2.37, - 2.49, - 2.61, - 2.74, - 2.87, - 3.01, - 3.16, - 3.32, - 3.48, - 3.65, - 3.83, - 4.02, - 4.22, - 4.42, - 4.64, - 4.87, - 5.11, - 5.36, - 5.62, - 5.90, - 6.19, - 6.49, - 6.81, - 7.15, - 7.50, - 7.87, - 8.25, - 8.66, - 9.09, - 9.53, - } - - E24 = { - 1.0, - 1.1, - 1.2, - 1.3, - 1.5, - 1.6, - 1.8, - 2.0, - 2.2, - 2.4, - 2.7, - 3.0, - 3.3, - 3.6, - 3.9, - 4.3, - 4.7, - 5.1, - 5.6, - 6.2, - 6.8, - 7.5, - 8.2, - 9.1, - } - - E12 = { - 1.0, - 1.2, - 1.5, - 1.8, - 2.2, - 2.7, - 3.3, - 3.9, - 4.7, - 5.6, - 6.8, - 8.2, - } - - E6 = { - 1.0, - 1.5, - 2.2, - 3.3, - 4.7, - 6.8, - } - - E3 = { - 1.0, - 2.2, - 4.7, - } - - E_ALL = set(sorted(E24 | E192)) - - -def repeat_set_over_base( - values: set[float], base: int, exp_range: range, n_decimals: int = 13 -) -> set[float]: - assert all(v >= 1 and v < base for v in values) - return set( - [round(val * base**exp, n_decimals) for val in values for exp in exp_range] + E192 = frozenset( + [ + 1.00, + 1.01, + 1.02, + 1.04, + 1.05, + 1.06, + 1.07, + 1.09, + 1.10, + 1.11, + 1.13, + 1.14, + 1.15, + 1.17, + 1.18, + 1.20, + 1.21, + 1.23, + 1.24, + 1.26, + 1.27, + 1.29, + 1.30, + 1.32, + 1.33, + 1.35, + 1.37, + 1.38, + 1.40, + 1.42, + 1.43, + 1.45, + 1.47, + 1.49, + 1.50, + 1.52, + 1.54, + 1.56, + 1.58, + 1.60, + 1.62, + 1.64, + 1.65, + 1.67, + 1.69, + 1.72, + 1.74, + 1.76, + 1.78, + 1.80, + 1.82, + 1.84, + 1.87, + 1.89, + 1.91, + 1.93, + 1.96, + 1.98, + 2.00, + 2.03, + 2.05, + 2.08, + 2.10, + 2.13, + 2.15, + 2.18, + 2.21, + 2.23, + 2.26, + 2.29, + 2.32, + 2.34, + 2.37, + 2.40, + 2.43, + 2.46, + 2.49, + 2.52, + 2.55, + 2.58, + 2.61, + 2.64, + 2.67, + 2.71, + 2.74, + 2.77, + 2.80, + 2.84, + 2.87, + 2.91, + 2.94, + 2.98, + 3.01, + 3.05, + 3.09, + 3.12, + 3.16, + 3.20, + 3.24, + 3.28, + 3.32, + 3.36, + 3.40, + 3.44, + 3.48, + 3.52, + 3.57, + 3.61, + 3.65, + 3.70, + 3.74, + 3.79, + 3.83, + 3.88, + 3.92, + 3.97, + 4.02, + 4.07, + 4.12, + 4.17, + 4.22, + 4.27, + 4.32, + 4.37, + 4.42, + 4.48, + 4.53, + 4.59, + 4.64, + 4.70, + 4.75, + 4.81, + 4.87, + 4.93, + 4.99, + 5.05, + 5.11, + 5.17, + 5.23, + 5.30, + 5.36, + 5.42, + 5.49, + 5.56, + 5.62, + 5.69, + 5.76, + 5.83, + 5.90, + 5.97, + 6.04, + 6.12, + 6.19, + 6.26, + 6.34, + 6.42, + 6.49, + 6.57, + 6.65, + 6.73, + 6.81, + 6.90, + 6.98, + 7.06, + 7.15, + 7.23, + 7.32, + 7.41, + 7.50, + 7.59, + 7.68, + 7.77, + 7.87, + 7.96, + 8.06, + 8.16, + 8.25, + 8.35, + 8.45, + 8.56, + 8.66, + 8.76, + 8.87, + 8.98, + 9.09, + 9.20, + 9.31, + 9.42, + 9.53, + 9.65, + 9.76, + 9.88, + ] ) + E96 = frozenset( + [ + 1.00, + 1.02, + 1.05, + 1.07, + 1.10, + 1.13, + 1.15, + 1.18, + 1.21, + 1.24, + 1.27, + 1.30, + 1.33, + 1.37, + 1.40, + 1.43, + 1.47, + 1.50, + 1.54, + 1.58, + 1.62, + 1.65, + 1.69, + 1.74, + 1.78, + 1.82, + 1.87, + 1.91, + 1.96, + 2.00, + 2.05, + 2.10, + 2.15, + 2.21, + 2.26, + 2.32, + 2.37, + 2.43, + 2.49, + 2.55, + 2.61, + 2.67, + 2.74, + 2.80, + 2.87, + 2.94, + 3.01, + 3.09, + 3.16, + 3.24, + 3.32, + 3.40, + 3.48, + 3.57, + 3.65, + 3.74, + 3.83, + 3.92, + 4.02, + 4.12, + 4.22, + 4.32, + 4.42, + 4.53, + 4.64, + 4.75, + 4.87, + 4.99, + 5.11, + 5.23, + 5.36, + 5.49, + 5.62, + 5.76, + 5.90, + 6.04, + 6.19, + 6.34, + 6.49, + 6.65, + 6.81, + 6.98, + 7.15, + 7.32, + 7.50, + 7.68, + 7.87, + 8.06, + 8.25, + 8.45, + 8.66, + 8.87, + 9.09, + 9.31, + 9.53, + 9.76, + ] + ) -class ParamNotResolvedError(Exception): ... - - -_e_series_cache: list[tuple[Parameter, int, set]] = [] + E48 = frozenset( + [ + 1.00, + 1.05, + 1.10, + 1.15, + 1.21, + 1.27, + 1.33, + 1.40, + 1.47, + 1.54, + 1.62, + 1.69, + 1.78, + 1.87, + 1.96, + 2.05, + 2.15, + 2.26, + 2.37, + 2.49, + 2.61, + 2.74, + 2.87, + 3.01, + 3.16, + 3.32, + 3.48, + 3.65, + 3.83, + 4.02, + 4.22, + 4.42, + 4.64, + 4.87, + 5.11, + 5.36, + 5.62, + 5.90, + 6.19, + 6.49, + 6.81, + 7.15, + 7.50, + 7.87, + 8.25, + 8.66, + 9.09, + 9.53, + ] + ) + E24 = frozenset( + [ + 1.0, + 1.1, + 1.2, + 1.3, + 1.5, + 1.6, + 1.8, + 2.0, + 2.2, + 2.4, + 2.7, + 3.0, + 3.3, + 3.6, + 3.9, + 4.3, + 4.7, + 5.1, + 5.6, + 6.2, + 6.8, + 7.5, + 8.2, + 9.1, + ] + ) -def e_series_intersect[T: float | Quantity]( - value: Parameter, e_series: E_SERIES = E_SERIES_VALUES.E_ALL -) -> F.Set: - # TODO this got really uglu, need to clean up + E12 = frozenset( + [ + 1.0, + 1.2, + 1.5, + 1.8, + 2.2, + 2.7, + 3.3, + 3.9, + 4.7, + 5.6, + 6.8, + 8.2, + ] + ) - value = value.get_most_narrow() + E6 = frozenset( + [ + 1.0, + 1.5, + 2.2, + 3.3, + 4.7, + 6.8, + ] + ) - for k, i, v in _e_series_cache: - if k == value and i == id(e_series): - return F.Set(v) + E3 = frozenset( + [ + 1.0, + 2.2, + 4.7, + ] + ) + E_ALL = frozenset(sorted(E24 | E192)) - if isinstance(value, F.Constant): - value = F.Range(value) - elif isinstance(value, F.Set): - raise NotImplementedError - elif isinstance(value, (F.Operation, F.TBD)): - raise ParamNotResolvedError() - elif isinstance(value, F.ANY): - # TODO - raise ParamNotResolvedError() - assert isinstance(value, F.Range) +QuantityT = TypeVar("QuantityT", int, float, Quantity) - min_val = value.min - max_val = value.max - unit = 1 - if not isinstance(min_val, F.Constant) or not isinstance(max_val, F.Constant): - # TODO - raise Exception() +def repeat_set_over_base( + values: set[float], + base: int, + exp_range: Sequence[int], + unit: Unit, + n_decimals: int = 13, +) -> L.Singles[QuantityT]: + assert all(v >= 1 and v < base for v in values) + return L.Singles[QuantityT]( + *( + round(val * base**exp, n_decimals) * unit + for val in values + for exp in exp_range + ) + ) - min_val = min_val.value - max_val = max_val.value - if isinstance(min_val, Quantity): - assert isinstance(max_val, Quantity) +@once +def e_series_intersect( + value_set: Range[QuantityT] | Ranges[QuantityT], + e_series: E_SERIES | None = None, +) -> L.Ranges[QuantityT]: + if e_series is None: + e_series = E_SERIES_VALUES.E_ALL - min_val_q = min_val.to_compact() + if isinstance(value_set, Range): + value_set = Ranges(value_set) - unit = min_val_q.units - max_val_q = max_val.to(unit) - assert max_val_q.units == unit + if ( + value_set.is_empty() + or value_set.min_elem() < 0 + or value_set.max_elem() == float("inf") + ): + raise ValueError("Need positive finite set") - min_val: float = min_val_q.magnitude - max_val: float = max_val_q.magnitude + out = L.Empty(value_set.units) - assert isinstance(min_val, (float, int)) and isinstance(max_val, (float, int)) + for sub_range in value_set: + min_val_q = sub_range.min_elem().to_compact() + max_val_q = sub_range.max_elem().to(min_val_q.units) - # TODO ugly - if max_val == math.inf: - max_val = min_val * 10e3 + min_val = min_val_q.magnitude + max_val = max_val_q.magnitude - e_series_values = repeat_set_over_base( - e_series, 10, range(floor(log10(min_val)), ceil(log10(max_val)) + 1) - ) - out = value & {e * unit for e in e_series_values} - _e_series_cache.append((copy.copy(value), id(e_series), out.params)) + e_series_values = repeat_set_over_base( + values=e_series, + base=10, + exp_range=range(floor(log10(min_val)), ceil(log10(max_val)) + 1), + unit=min_val_q.units, + ) + out = out.op_union_ranges(e_series_values.op_intersect_range(sub_range)) return out def e_series_discretize_to_nearest( - value: Parameter, e_series: E_SERIES = E_SERIES_VALUES.E_ALL -) -> F.Constant: - if not isinstance(value, (F.Constant, F.Range)): - raise NotImplementedError - - target = value.value if isinstance(value, F.Constant) else sum(value.as_tuple()) / 2 + value: Range[Quantity], e_series: E_SERIES = E_SERIES_VALUES.E_ALL +) -> Quantity: + target = cast(Quantity, (value.min_elem() + value.max_elem())) / 2 e_series_values = repeat_set_over_base( - e_series, 10, range(floor(log10(target)), ceil(log10(target)) + 1) + e_series, 10, range(floor(log10(target)), ceil(log10(target)) + 1), target.units ) - return F.Constant(min(e_series_values, key=lambda x: abs(x - target))) + return min(e_series_values, key=lambda x: abs(x - target)) def e_series_ratio( - RH: Parameter, - RL: Parameter, - output_input_ratio: Parameter, + RH: Range[float], + RL: Range[float], + output_input_ratio: Range[float], e_values: E_SERIES = E_SERIES_VALUES.E_ALL, ) -> Tuple[float, float]: """ @@ -521,8 +513,6 @@ def e_series_ratio( RH and RL define the contstraints for the components, and output_input_ratio is the output/input voltage ratio as defined below. - RH and output_input_ratio must be constrained to a range or constant, but RL can be - ANY. output_input_ratio = RL/(RH + RL) RL/oir = RH + RL @@ -534,51 +524,41 @@ def e_series_ratio( Can be used for a resistive divider. """ - if ( - not isinstance(RH, (F.Constant, F.Range)) - or not isinstance(RL, (F.Constant, F.Range, F.ANY)) - or not isinstance(output_input_ratio, (F.Constant, F.Range)) - ): - raise NotImplementedError - - if not output_input_ratio.is_subset_of(F.Range(0, 1)): - raise ValueError("Invalid output/input voltage ratio") - - rh = F.Range(RH.value, RH.value) if isinstance(RH, F.Constant) else RH - rl = F.Range(RL.value, RL.value) if isinstance(RL, F.Constant) else RL - oir = ( - F.Range(output_input_ratio.value, output_input_ratio.value) - if isinstance(output_input_ratio, F.Constant) - else output_input_ratio + rh_factor = output_input_ratio.op_invert().op_subtract_ranges( + L.Singles(1.0 * dimensionless) ) - rh_values = e_series_intersect(rh, e_values) - rl_values = e_series_intersect(rl, e_values) if isinstance(rl, F.Range) else None + rh = Ranges(RH).op_intersect_ranges(rh_factor.op_mul_ranges(Ranges(RL))) + rh_e = e_series_intersect(rh, e_values) + rl = Ranges(RL).op_intersect_ranges( + rh_factor.op_invert().op_mul_ranges(Ranges(rh_e)) + ) + rl_e = e_series_intersect(rl, e_values) - target_ratio = oir.as_center_tuple()[0] + target_ratio = ( + cast(Quantity, (output_input_ratio.min_elem() + output_input_ratio.max_elem())) + / 2 + ) solutions = [] - for rh_val in rh_values.params: - rl_ideal = rh_val / (F.Constant(1) / target_ratio - 1) + for rh_range in rh_e: + rh_val = rh_range.min_elem() + rl_ideal = rh_val / (1 / target_ratio - 1) - rl_nearest_e_val = ( - min(rl_values.params, key=lambda x: abs(x - rl_ideal)) - if rl_values - else e_series_discretize_to_nearest(rl_ideal, e_values) - ) + rl_nearest_e_val = rl_e.closest_elem(rl_ideal) real_ratio = rl_nearest_e_val / (rh_val + rl_nearest_e_val) - solutions.append((real_ratio, (rh_val, rl_nearest_e_val))) + solutions.append((real_ratio, (float(rh_val), float(rl_nearest_e_val)))) optimum = min(solutions, key=lambda x: abs(x[0] - target_ratio)) logger.debug( - f"{target_ratio=}, {optimum[0]=}, {oir}, " + f"{target_ratio=}, {optimum[0]=}, {output_input_ratio}, " f"error: {abs(optimum[0]/ target_ratio - 1)*100:.4f}%" ) - if optimum[0] not in oir: + if optimum[0] not in output_input_ratio: raise ArithmeticError( "Calculated optimum RH RL value pair gives output/input voltage ratio " "outside of specified range. Consider relaxing the constraints" diff --git a/src/faebryk/libs/examples/buildutil.py b/src/faebryk/libs/examples/buildutil.py index 362aa9e3..4d71d1fb 100644 --- a/src/faebryk/libs/examples/buildutil.py +++ b/src/faebryk/libs/examples/buildutil.py @@ -7,10 +7,11 @@ from typing import Callable import faebryk.libs.picker.lcsc as lcsc +from faebryk.core.defaultsolver import DefaultSolver from faebryk.core.module import Module from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer from faebryk.libs.app.checks import run_checks -from faebryk.libs.app.parameters import replace_tbd_with_any, resolve_dynamic_parameters +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.app.pcb import apply_design from faebryk.libs.examples.pickers import add_example_pickers from faebryk.libs.picker.api.api import ApiNotConfiguredError @@ -52,13 +53,10 @@ def apply_design_to_pcb( logger.info("Filling unspecified parameters") - replace_tbd_with_any( - m, recursive=True, loglvl=logging.DEBUG if DEV_MODE else logging.INFO - ) - G = m.get_graph() resolve_dynamic_parameters(G) run_checks(m, G) + solver = DefaultSolver() # TODO this can be prettier # picking ---------------------------------------------------------------- @@ -70,7 +68,7 @@ def apply_design_to_pcb( try: JLCPCB_DB() for n in modules: - add_jlcpcb_pickers(n, base_prio=-10) + add_jlcpcb_pickers(n, base_prio=-10, solver=solver) except FileNotFoundError: logger.warning("JLCPCB database not found. Skipping JLCPCB pickers.") case PickerType.API: @@ -81,8 +79,9 @@ def apply_design_to_pcb( logger.warning("API not configured. Skipping API pickers.") for n in modules: - add_example_pickers(n) + add_example_pickers(n, solver) pick_part_recursively(m) + solver.find_and_lock_solution(G) # ------------------------------------------------------------------------- example_prj = Path(__file__).parent / Path("resources/example") diff --git a/src/faebryk/libs/examples/pickers.py b/src/faebryk/libs/examples/pickers.py index cab62c0c..0eee615e 100644 --- a/src/faebryk/libs/examples/pickers.py +++ b/src/faebryk/libs/examples/pickers.py @@ -10,7 +10,8 @@ import faebryk.library._F as F from faebryk.core.module import Module -from faebryk.libs.app.parameters import replace_tbd_with_any +from faebryk.core.solver import Solver +from faebryk.libs.library import L from faebryk.libs.picker.lcsc import LCSC_Part from faebryk.libs.picker.picker import PickerOption, pick_module_by_params from faebryk.libs.units import P @@ -21,31 +22,35 @@ logger = logging.getLogger(__name__) -def pick_fuse(module: F.Fuse): +# TODO replace Single with actual Range.from_center_rel + + +def pick_fuse(module: F.Fuse, solver: Solver): pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C914087"), params={ - "fuse_type": F.Constant(F.Fuse.FuseType.RESETTABLE), - "response_type": F.Constant(F.Fuse.ResponseType.SLOW), - "trip_current": F.Constant(1 * P.A), + "fuse_type": L.PlainSet(F.Fuse.FuseType.RESETTABLE), + "response_type": L.PlainSet(F.Fuse.ResponseType.SLOW), + "trip_current": 1 * P.A, }, ), PickerOption( part=LCSC_Part(partno="C914085"), params={ - "fuse_type": F.Constant(F.Fuse.FuseType.RESETTABLE), - "response_type": F.Constant(F.Fuse.ResponseType.SLOW), - "trip_current": F.Constant(0.5 * P.A), + "fuse_type": L.PlainSet(F.Fuse.FuseType.RESETTABLE), + "response_type": L.PlainSet(F.Fuse.ResponseType.SLOW), + "trip_current": 0.5 * P.A, }, ), ], ) -def pick_mosfet(module: F.MOSFET): +def pick_mosfet(module: F.MOSFET, solver: Solver): standard_pinmap = { "1": module.gate, "2": module.source, @@ -53,18 +58,19 @@ def pick_mosfet(module: F.MOSFET): } pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C20917"), params={ - "channel_type": F.Constant(F.MOSFET.ChannelType.N_CHANNEL), + "channel_type": L.PlainSet(F.MOSFET.ChannelType.N_CHANNEL), }, pinmap=standard_pinmap, ), PickerOption( part=LCSC_Part(partno="C15127"), params={ - "channel_type": F.Constant(F.MOSFET.ChannelType.P_CHANNEL), + "channel_type": L.PlainSet(F.MOSFET.ChannelType.P_CHANNEL), }, pinmap=standard_pinmap, ), @@ -72,7 +78,7 @@ def pick_mosfet(module: F.MOSFET): ) -def pick_capacitor(module: F.Capacitor): +def pick_capacitor(module: F.Capacitor, solver: Solver): """ Link a partnumber/footprint to a Capacitor @@ -81,34 +87,35 @@ def pick_capacitor(module: F.Capacitor): pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C1525"), params={ - "temperature_coefficient": F.Range( + "temperature_coefficient": L.Range( F.Capacitor.TemperatureCoefficient.Y5V, F.Capacitor.TemperatureCoefficient.X7R, ), - "capacitance": F.Constant(100 * P.nF), - "rated_voltage": F.Constant(16 * P.V), + "capacitance": L.Single(100 * P.nF), + "max_voltage": 16 * P.V, }, ), PickerOption( part=LCSC_Part(partno="C19702"), params={ - "temperature_coefficient": F.Range( + "temperature_coefficient": L.Range( F.Capacitor.TemperatureCoefficient.Y5V, F.Capacitor.TemperatureCoefficient.X7R, ), - "capacitance": F.Constant(10 * P.uF), - "rated_voltage": F.Constant(10 * P.V), + "capacitance": L.Single(10 * P.uF), + "max_voltage": 10 * P.V, }, ), ], ) -def pick_resistor(resistor: F.Resistor): +def pick_resistor(resistor: F.Resistor, solver: Solver): """ Link a partnumber/footprint to a Resistor @@ -117,98 +124,100 @@ def pick_resistor(resistor: F.Resistor): pick_module_by_params( resistor, + solver, [ PickerOption( part=LCSC_Part(partno="C25111"), - params={"resistance": F.Constant(40.2 * P.kohm)}, + params={"resistance": L.Single(40.2 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25076"), - params={"resistance": F.Constant(100 * P.kohm)}, + params={"resistance": L.Single(100 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25087"), - params={"resistance": F.Constant(200 * P.kohm)}, + params={"resistance": L.Single(200 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C11702"), - params={"resistance": F.Constant(1 * P.kohm)}, + params={"resistance": L.Single(1 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25879"), - params={"resistance": F.Constant(2.2 * P.kohm)}, + params={"resistance": L.Single(2.2 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25900"), - params={"resistance": F.Constant(4.7 * P.kohm)}, + params={"resistance": L.Single(4.7 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25905"), - params={"resistance": F.Constant(5.1 * P.kohm)}, + params={"resistance": L.Single(5.1 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25917"), - params={"resistance": F.Constant(6.8 * P.kohm)}, + params={"resistance": L.Single(6.8 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25744"), - params={"resistance": F.Constant(10 * P.kohm)}, + params={"resistance": L.Single(10 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25752"), - params={"resistance": F.Constant(12 * P.kohm)}, + params={"resistance": L.Single(12 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25771"), - params={"resistance": F.Constant(27 * P.kohm)}, + params={"resistance": L.Single(27 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25741"), - params={"resistance": F.Constant(100 * P.kohm)}, + params={"resistance": L.Single(100 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25782"), - params={"resistance": F.Constant(390 * P.kohm)}, + params={"resistance": L.Single(390 * P.kohm)}, ), PickerOption( part=LCSC_Part(partno="C25790"), - params={"resistance": F.Constant(470 * P.kohm)}, + params={"resistance": L.Single(470 * P.kohm)}, ), ], ) -def pick_led(module: F.LED): +def pick_led(module: F.LED, solver: Solver): pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C72043"), params={ - "color": F.Constant(F.LED.Color.EMERALD), - "max_brightness": F.Constant(285 * P.mcandela), - "forward_voltage": F.Constant(3.7 * P.volt), - "max_current": F.Constant(100 * P.mA), + "color": L.PlainSet(F.LED.Color.EMERALD), + "max_brightness": 285 * P.mcandela, + "forward_voltage": L.Single(3.7 * P.volt), + "max_current": 100 * P.mA, }, pinmap={"1": module.cathode, "2": module.anode}, ), PickerOption( part=LCSC_Part(partno="C72041"), params={ - "color": F.Constant(F.LED.Color.BLUE), - "max_brightness": F.Constant(28.5 * P.mcandela), - "forward_voltage": F.Constant(3.1 * P.volt), - "max_current": F.Constant(100 * P.mA), + "color": L.PlainSet(F.LED.Color.BLUE), + "max_brightness": 28.5 * P.mcandela, + "forward_voltage": L.Single(3.1 * P.volt), + "max_current": 100 * P.mA, }, pinmap={"1": module.cathode, "2": module.anode}, ), PickerOption( part=LCSC_Part(partno="C72038"), params={ - "color": F.Constant(F.LED.Color.YELLOW), - "max_brightness": F.Constant(180 * P.mcandela), - "forward_voltage": F.Constant(2.3 * P.volt), - "max_current": F.Constant(60 * P.mA), + "color": L.PlainSet(F.LED.Color.YELLOW), + "max_brightness": 180 * P.mcandela, + "forward_voltage": L.Single(2.3 * P.volt), + "max_current": 60 * P.mA, }, pinmap={"1": module.cathode, "2": module.anode}, ), @@ -216,14 +225,15 @@ def pick_led(module: F.LED): ) -def pick_tvs(module: F.TVS): +def pick_tvs(module: F.TVS, solver: Solver): pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C85402"), params={ - "reverse_working_voltage": F.Constant(5 * P.V), + "reverse_working_voltage": L.Single(5 * P.V), }, pinmap={ "1": module.cathode, @@ -234,27 +244,31 @@ def pick_tvs(module: F.TVS): ) -def pick_battery(module: F.Battery): +def pick_battery(module: F.Battery | Module, solver: Solver): + if not isinstance(module, F.Battery): + raise ValueError("Module is not a Battery") if not isinstance(module, F.ButtonCell): bcell = F.ButtonCell() - replace_tbd_with_any(bcell, recursive=False) module.specialize(bcell) bcell.add( - F.has_multi_picker(0, F.has_multi_picker.FunctionPicker(pick_battery)) + F.has_multi_picker( + 0, F.has_multi_picker.FunctionPicker(pick_battery, solver) + ) ) return pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C5239862"), params={ - "voltage": F.Constant(3 * P.V), - "capacity": F.Range.from_center(225 * P.mAh, 50 * P.mAh), - "material": F.Constant(F.ButtonCell.Material.Lithium), - "size": F.Constant(F.ButtonCell.Size.N_2032), - "shape": F.Constant(F.ButtonCell.Shape.Round), + "voltage": L.Single(3 * P.V), + "capacity": L.Range.from_center(225 * P.mAh, 50 * P.mAh), + "material": L.PlainSet(F.ButtonCell.Material.Lithium), + "size": L.Single(F.ButtonCell.Size.N_2032), + "shape": L.PlainSet(F.ButtonCell.Shape.Round), }, pinmap={ "1": module.power.lv, @@ -265,25 +279,26 @@ def pick_battery(module: F.Battery): ) -def pick_switch(module: "_TSwitch"): +def pick_switch(module: "_TSwitch", solver: Solver): module.add(F.can_attach_to_footprint_symmetrically()) pick_module_by_params( module, + solver, [ PickerOption( part=LCSC_Part(partno="C318884"), pinmap={ - "1": module.unnamed[0], - "2": module.unnamed[0], - "3": module.unnamed[1], - "4": module.unnamed[1], + "1": module.unnamed[0], # type: ignore + "2": module.unnamed[0], # type: ignore + "3": module.unnamed[1], # type: ignore + "4": module.unnamed[1], # type: ignore }, ) ], ) -def add_example_pickers(module: Module): +def add_example_pickers(module: Module, solver: Solver): lookup = { F.Resistor: pick_resistor, F.LED: pick_led, @@ -297,5 +312,5 @@ def add_example_pickers(module: Module): F.has_multi_picker.add_pickers_by_type( module, lookup, - F.has_multi_picker.FunctionPicker, + lambda pick_fn: F.has_multi_picker.FunctionPicker(pick_fn, solver), ) diff --git a/src/faebryk/libs/library/L.py b/src/faebryk/libs/library/L.py index 12181080..9b1c7ab9 100644 --- a/src/faebryk/libs/library/L.py +++ b/src/faebryk/libs/library/L.py @@ -13,10 +13,26 @@ list_field, rt_field, ) +from faebryk.core.parameter import R, p_field # noqa: F401 from faebryk.core.reference import reference # noqa: F401 +from faebryk.libs.sets import ( # noqa: F401 + Empty, + P_Set, + P_UnitSet, + PlainSet, + Range, + Ranges, + Single, + Singles, +) class AbstractclassError(Exception): ... logger = logging.getLogger(__name__) + + +Predicates = R.Predicates +Domains = R.Domains +Expressions = R.Expressions diff --git a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py index c4cab9cb..29a2feb5 100644 --- a/src/faebryk/libs/picker/jlcpcb/jlcpcb.py +++ b/src/faebryk/libs/picker/jlcpcb/jlcpcb.py @@ -16,7 +16,6 @@ import patoolib import requests -from pint import DimensionalityError from rich.progress import track from tortoise import Tortoise from tortoise.expressions import Q @@ -25,7 +24,13 @@ import faebryk.library._F as F from faebryk.core.module import Module -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import ParameterOperatable +from faebryk.core.solver import Solver +from faebryk.libs.e_series import ( + E_SERIES, + e_series_intersect, +) +from faebryk.libs.library import L from faebryk.libs.picker.lcsc import ( LCSC_NoDataException, LCSC_Part, @@ -37,8 +42,8 @@ PickError, has_part_picked_defined, ) -from faebryk.libs.units import P, UndefinedUnitError -from faebryk.libs.util import at_exit, once, try_or +from faebryk.libs.units import P, Quantity, UndefinedUnitError, to_si_str +from faebryk.libs.util import at_exit, cast_assert, once logger = logging.getLogger(__name__) @@ -46,34 +51,20 @@ BUILD_FOLDER = Path("./build") CACHE_FOLDER = BUILD_FOLDER / Path("cache") +INSPECT_KNOWN_SUPERSETS_LIMIT = 100 + class JLCPCB_Part(LCSC_Part): def __init__(self, partno: str) -> None: super().__init__(partno=partno) -class TBD_ParseError(F.TBD): - """ - Wrapper for TBD that behaves exactly like TBD for the core and picker - But gives us the possibility to attach parser errors to it for deferred - error logging - """ - - def __init__(self, e: Exception, msg: str): - self.e = e - self.msg = msg - super().__init__() - - def __repr__(self): - return f"{super().__repr__()}({self.msg}: {self.e})" - - -@dataclass +@dataclass(frozen=True) class MappingParameterDB: param_name: str attr_keys: list[str] attr_tolerance_key: str | None = None - transform_fn: Callable[[str], Parameter] | None = None + transform_fn: Callable[[str], L.Range] | None = None ignore_at: bool = True @@ -205,9 +196,9 @@ def extra_(self) -> dict: assert isinstance(self.extra, dict) return self.extra - def attribute_to_parameter( + def attribute_to_range( self, attribute_name: str, use_tolerance: bool = False, ignore_at: bool = True - ) -> Parameter: + ) -> L.Range[Quantity]: """ Convert a component value in the extra['attributes'] dict to a parameter @@ -236,7 +227,7 @@ def attribute_to_parameter( values = value_field.split("~") if len(values) != 2: raise ValueError(f"Invalid range from value '{value_field}'") - return F.Range(*(P.Quantity(v) for v in values)) + return L.Range(*(P.Quantity(v) for v in values)) # unit hacks @@ -246,7 +237,7 @@ def attribute_to_parameter( raise ValueError(f"Could not parse value field '{value_field}'") from e if not use_tolerance: - return F.Constant(value) + return L.Single(value) if "Tolerance" not in self.extra_["attributes"]: raise ValueError(f"No Tolerance field in component (lcsc: {self.lcsc})") @@ -264,9 +255,9 @@ def attribute_to_parameter( f"'{self.extra_['attributes']['Tolerance']}'" ) - return F.Range.from_center_rel(value, tolerance) + return L.Range.from_center_rel(value, tolerance) - def get_parameter(self, m: MappingParameterDB) -> Parameter: + def get_range(self, m: MappingParameterDB) -> L.Range[Quantity]: """ Transform a component attribute to a parameter @@ -318,54 +309,44 @@ def get_parameter(self, m: MappingParameterDB) -> Parameter: if parser is not None: return parser(self.extra_["attributes"][attr_key]) - return self.attribute_to_parameter( + return self.attribute_to_range( attr_key, tolerance_search_key is not None, m.ignore_at ) - def get_params(self, mapping: list[MappingParameterDB]) -> list[Parameter]: - return [ - try_or( - lambda: self.get_parameter(m), - default_f=lambda e: TBD_ParseError( - e, f"Failed to parse {m.param_name}" - ), - catch=(LookupError, ValueError, AssertionError), - ) - for m in mapping - ] + def get_range_for_mappings( + self, mapping: list[MappingParameterDB] + ) -> tuple[ + dict[MappingParameterDB, L.Range[Quantity]], dict[MappingParameterDB, Exception] + ]: + params = {} + exceptions = {} + for m in mapping: + try: + params[m] = self.get_range(m) + except LookupError | ValueError | AssertionError as e: + exceptions[m] = e + return params, exceptions def attach( self, module: Module, mapping: list[MappingParameterDB], qty: int = 1, - allow_TBD: bool = False, + ignore_exceptions: bool = False, ): - params = self.get_params(mapping) + params, exceptions = self.get_range_for_mappings(mapping) - if not allow_TBD and any(isinstance(p, TBD_ParseError) for p in params): + if not ignore_exceptions and exceptions: params_str = indent( - "\n" - + "\n".join(repr(p) for p in params if isinstance(p, TBD_ParseError)), + "\n" + "\n".join(repr(e) for e in exceptions.values()), " " * 4, ) raise Component.ParseError( f"Failed to parse parameters for component {self.partno}: {params_str}" ) - # Override module parameters with picked component parameters - # sort by type to avoid merge conflicts - module_params: list[tuple[Parameter, Parameter]] = [ - (getattr(module, name), value) - for name, value in zip([m.param_name for m in mapping], params) - ] - types_sort = [F.ANY, F.TBD, F.Constant, F.Range, F.Set, F.Operation] - for p, value in sorted( - module_params, key=lambda x: types_sort.index(type(x[0].get_most_narrow())) - ): - p.override(value) - attach(module, self.partno) + module.add( F.has_descriptive_properties_defined( { @@ -382,6 +363,10 @@ def attach( ) module.add(has_part_picked_defined(JLCPCB_Part(self.partno))) + + for name, value in params.items(): + getattr(module, name.param_name).alias_is(value) + if logger.isEnabledFor(logging.DEBUG): logger.debug( f"Attached component {self.partno} to module {module}: \n" @@ -401,7 +386,7 @@ class ComponentQuery: class Error(Exception): ... class ParamError(Error): - def __init__(self, param: Parameter, msg: str): + def __init__(self, param: L.P_UnitSet, msg: str): self.param = param self.msg = msg super().__init__(f"{msg} for parameter {param!r}") @@ -443,39 +428,41 @@ def filter_by_description(self, *keywords: str) -> Self: def filter_by_si_values( self, - value: Parameter, - si_vals: list[str], + value: L.Ranges[Quantity], + si_unit: str, + e_series: E_SERIES | None = None, tolerance_requirement: float | None = None, ) -> Self: assert self.Q - if logger.isEnabledFor(logging.DEBUG): - logger.debug( - f"Filtering by value:\n{indent(value.get_tree_param().pretty(), ' '*4)}" - ) - logger.debug(f"Possible values: {si_vals}") - - if tolerance_requirement is not None: - cmp = value.get_parent_of_type(Module) - assert cmp - value = value.get_most_narrow() - if not isinstance(value, F.Range): - raise PickError(f"Can only pick ranges, not: {value}", cmp) - tol = value.as_center_tuple(relative=True)[1] - if tol < tolerance_requirement: # type: ignore - raise PickError( - f"Tolerance not supported: {value}, " - f"expected at least {tolerance_requirement}, but is {tol}", - cmp, - ) - self.filter_by_tolerance(tolerance_requirement) - - if isinstance(value, F.ANY): + if value.is_unbounded(): return self assert not self.results + intersection = e_series_intersect(value, e_series) + if intersection.is_empty(): + raise ComponentQuery.ParamError(value, "No intersection with E-series") + si_vals = [ + to_si_str(r.min_elem(), si_unit).replace("µ", "u").replace("inf", "∞") + for r in intersection + ] + if tolerance_requirement: + self.filter_by_tolerance(tolerance_requirement) return self.filter_by_description(*si_vals) + def hint_filter_parameter( + self, param: ParameterOperatable, si_unit: str, e_series: E_SERIES | None = None + ) -> Self: + # TODO implement + # param will in the general case consist of multiple ranges + # we have to pick some range or make a new one to pre_filter our candidates + # we can try making a new range with inspect_min and max to filter out + # everything we already know won't fit + # then we can check the cardinality of the remaining candidates to see if we + # need to pick a range contained in the param to filter + raise NotImplementedError() + return self + def filter_by_tolerance(self, tolerance: float) -> Self: assert self.Q @@ -561,6 +548,7 @@ def filter_by_module_params( self, module: Module, mapping: list[MappingParameterDB], + solver: Solver, ) -> Generator[Component, None, None]: """ Filter the results by the parameters of the module @@ -577,27 +565,51 @@ def filter_by_module_params( :return: The first component that matches the parameters """ + # iterate through all candidate components for c in self.get(): - params = c.get_params(mapping) - - if not all( - pm := [ - try_or( - lambda: p.is_subset_of(getattr(module, m.param_name)), - default=False, - catch=DimensionalityError, - ) - for p, m in zip(params, mapping) - ] - ): - if logger.isEnabledFor(logging.DEBUG): - unmatch = [ - f"({m.param_name}: {p} " - f"{getattr(module, m.param_name).get_most_narrow()})" - for p, v, m in zip(params, pm, mapping) - if not v - ] - logger.debug(f"Component {c.lcsc} doesn't match: [{unmatch}]") + range_mapping, exceptions = c.get_range_for_mappings(mapping) + + if exceptions: # TODO + continue + + param_mapping = [ + ( + cast_assert(ParameterOperatable, getattr(module, m.param_name)), + c_range, + ) + for m, c_range in range_mapping.items() + ] + + known_incompatible = False + + # check for any param that has few supersets whether the component's range + # is compatible already instead of waiting for the solver + for m_param, c_range in param_mapping: + if not m_param.inspect_known_supersets_are_few(): + continue + + known_superset = L.Ranges(*m_param.inspect_get_known_superranges()) + if not known_superset.is_superset_of(L.Ranges(c_range)): + known_incompatible = True + break + + # check for every param whether the candidate component's range is + # compatible by querying the solver + if not known_incompatible: + anded = True + for m_param, c_range in param_mapping: + anded &= m_param.operation_is_superset(c_range) + + result = solver.assert_any_predicate([(anded, None)], lock=False) + if not result.true_predicates: + known_incompatible = True + + # debug + if known_incompatible: + logger.debug( + f"Component {c.lcsc} doesn't match: " + f"{[p for p, v in range_mapping.items()]}" + ) continue if logger.isEnabledFor(logging.DEBUG): @@ -611,14 +623,19 @@ def filter_by_module_params( yield c def filter_by_module_params_and_attach( - self, module: Module, mapping: list[MappingParameterDB], qty: int = 1 + self, + module: Module, + mapping: list[MappingParameterDB], + solver: Solver, + qty: int = 1, ): - # TODO if no modules without TBD, rerun with TBD allowed + # TODO remove ignore_exceptions + # was used to handle TBDs failures = [] - for c in self.filter_by_module_params(module, mapping): + for c in self.filter_by_module_params(module, mapping, solver): try: - c.attach(module, mapping, qty, allow_TBD=False) + c.attach(module, mapping, qty, ignore_exceptions=False) return self except (ValueError, Component.ParseError) as e: failures.append((c, e)) diff --git a/src/faebryk/libs/picker/jlcpcb/picker_lib.py b/src/faebryk/libs/picker/jlcpcb/picker_lib.py index 77c9ef47..105ac7f4 100644 --- a/src/faebryk/libs/picker/jlcpcb/picker_lib.py +++ b/src/faebryk/libs/picker/jlcpcb/picker_lib.py @@ -5,7 +5,9 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.core.parameter import Parameter +from faebryk.core.solver import Solver from faebryk.libs.e_series import E_SERIES_VALUES +from faebryk.libs.library import L from faebryk.libs.picker.jlcpcb.jlcpcb import ( Component, ComponentQuery, @@ -15,7 +17,6 @@ DescriptiveProperties, PickError, ) -from faebryk.libs.picker.util import generate_si_values from faebryk.libs.util import KeyErrorAmbiguous, KeyErrorNotFound, cast_assert logger = logging.getLogger(__name__) @@ -31,21 +32,26 @@ # - should be classes instead of functions -def str_to_enum[T: Enum](enum: type[T], x: str) -> F.Constant: +# Generic pickers ---------------------------------------------------------------------- + + +def str_to_enum[T: Enum](enum: type[T], x: str) -> L.PlainSet[T]: name = x.replace(" ", "_").replace("-", "_").upper() if name not in [e.name for e in enum]: raise ValueError(f"Enum translation error: {x}[={name}] not in {enum}") - return F.Constant(enum[name]) + return L.PlainSet(enum[name]) -def str_to_enum_func[T: Enum](enum: type[T]) -> Callable[[str], F.Constant]: - def f(x: str) -> F.Constant: +def str_to_enum_func[T: Enum](enum: type[T]) -> Callable[[str], L.PlainSet[T]]: + def f(x: str) -> L.PlainSet[T]: return str_to_enum(enum, x) return f def enum_to_str(x: Parameter, force: bool = True) -> set[str]: + # FIXME + raise NotImplementedError() val = x.get_most_narrow() if isinstance(val, F.Constant): val = F.Set([val]) @@ -257,7 +263,7 @@ def find_component_by_lcsc_id(lcsc_id: str) -> Component: return next(iter(parts)) -def find_and_attach_by_lcsc_id(module: Module): +def find_and_attach_by_lcsc_id(module: Module, solver: Solver): """ Find a part in the JLCPCB database by its LCSC part number """ @@ -285,14 +291,8 @@ def find_and_attach_by_lcsc_id(module: Module): f"Part with LCSC part number {lcsc_pn} has insufficient stock", module ) - try: - part.attach(module, try_get_param_mapping(module), allow_TBD=True) - except Parameter.MergeException as e: - # TODO might be better to raise an error that makes the picker give up - # but this works for now, just not extremely efficient - raise PickError( - f"Could not attach part with LCSC part number {lcsc_pn}: {e}", module - ) from e + # FIXME: check that params are compatible + part.attach(module, try_get_param_mapping(module)) def find_component_by_mfr(mfr: str, mfr_pn: str) -> Component: @@ -318,7 +318,7 @@ def find_component_by_mfr(mfr: str, mfr_pn: str) -> Component: return next(iter(parts)) -def find_and_attach_by_mfr(module: Module): +def find_and_attach_by_mfr(module: Module, solver: Solver): """ Find a part in the JLCPCB database by its manufacturer part number """ @@ -354,14 +354,12 @@ def find_and_attach_by_mfr(module: Module): for part in parts: try: - part.attach(module, try_get_param_mapping(module), allow_TBD=True) + # FIXME: check that params are compatible + part.attach(module, try_get_param_mapping(module)) return except ValueError as e: logger.warning(f"Failed to attach component: {e}") continue - except Parameter.MergeException: - # TODO not very efficient - pass raise PickError( f"Could not attach any part with manufacturer part number {mfr_pn}", module @@ -371,7 +369,7 @@ def find_and_attach_by_mfr(module: Module): # Type specific pickers ---------------------------------------------------------------- -def find_resistor(cmp: Module): +def find_resistor(cmp: Module, solver: Solver): """ Find a resistor part in the JLCPCB database that matches the parameters of the provided resistor @@ -383,19 +381,15 @@ def find_resistor(cmp: Module): ComponentQuery() .filter_by_category("Resistors", "Chip Resistor - Surface Mount") .filter_by_stock(qty) - .filter_by_si_values( - cmp.resistance, - generate_si_values(cmp.resistance, "Ω", E_SERIES_VALUES.E96), - tolerance_requirement=0.01, - ) + .hint_filter_parameter(cmp.resistance, "Ω", E_SERIES_VALUES.E96) .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_capacitor(cmp: Module): +def find_capacitor(cmp: Module, solver: Solver): """ Find a capacitor part in the JLCPCB database that matches the parameters of the provided capacitor @@ -411,19 +405,15 @@ def find_capacitor(cmp: Module): "Capacitors", "Multilayer Ceramic Capacitors MLCC - SMD/SMT" ) .filter_by_stock(qty) - .filter_by_si_values( - cmp.capacitance, - generate_si_values(cmp.capacitance, "F", E_SERIES_VALUES.E24), - tolerance_requirement=0.05, - ) .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) + .hint_filter_parameter(cmp.capacitance, "F", E_SERIES_VALUES.E24) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_inductor(cmp: Module): +def find_inductor(cmp: Module, solver: Solver): """ Find an inductor part in the JLCPCB database that matches the parameters of the provided inductor. @@ -442,18 +432,14 @@ def find_inductor(cmp: Module): .filter_by_category("Inductors", "Inductors") .filter_by_stock(qty) .filter_by_traits(cmp) - .filter_by_si_values( - cmp.inductance, - generate_si_values(cmp.inductance, "H", E_SERIES_VALUES.E24), - tolerance_requirement=0.05, - ) .filter_by_specified_parameters(mapping) + .hint_filter_parameter(cmp.inductance, "H", E_SERIES_VALUES.E24) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_tvs(cmp: Module): +def find_tvs(cmp: Module, solver: Solver): """ Find a TVS diode part in the JLCPCB database that matches the parameters of the provided diode @@ -473,11 +459,11 @@ def find_tvs(cmp: Module): .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_diode(cmp: Module): +def find_diode(cmp: Module, solver: Solver): """ Find a diode part in the JLCPCB database that matches the parameters of the provided diode @@ -491,22 +477,16 @@ def find_diode(cmp: Module): ComponentQuery() .filter_by_category("Diodes", "") .filter_by_stock(qty) - .filter_by_si_values( - cmp.max_current, - generate_si_values(cmp.max_current, "A", E_SERIES_VALUES.E3), - ) - .filter_by_si_values( - cmp.reverse_working_voltage, - generate_si_values(cmp.reverse_working_voltage, "V", E_SERIES_VALUES.E3), - ) + .hint_filter_parameter(cmp.max_current, "A", E_SERIES_VALUES.E3) + .hint_filter_parameter(cmp.reverse_working_voltage, "V", E_SERIES_VALUES.E3) .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_led(cmp: Module): +def find_led(cmp: Module, solver: Solver): """ Find a LED part in the JLCPCB database that matches the parameters of the provided LED @@ -521,13 +501,14 @@ def find_led(cmp: Module): .filter_by_stock(qty) .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) - .filter_by_attribute_mention(list(enum_to_str(cmp.color, force=False))) + # TODO + # .filter_by_attribute_mention(list(enum_to_str(cmp.color, force=False))) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_mosfet(cmp: Module): +def find_mosfet(cmp: Module, solver: Solver): """ Find a MOSFET part in the JLCPCB database that matches the parameters of the provided MOSFET @@ -543,11 +524,11 @@ def find_mosfet(cmp: Module): .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) -def find_ldo(cmp: Module): +def find_ldo(cmp: Module, solver: Solver): """ Find a LDO part in the JLCPCB database that matches the parameters of the provided LDO @@ -563,7 +544,7 @@ def find_ldo(cmp: Module): .filter_by_traits(cmp) .filter_by_specified_parameters(mapping) .sort_by_price(qty) - .filter_by_module_params_and_attach(cmp, mapping, qty) + .filter_by_module_params_and_attach(cmp, mapping, solver, qty) ) diff --git a/src/faebryk/libs/picker/jlcpcb/pickers.py b/src/faebryk/libs/picker/jlcpcb/pickers.py index 86a5a9c7..3d802754 100644 --- a/src/faebryk/libs/picker/jlcpcb/pickers.py +++ b/src/faebryk/libs/picker/jlcpcb/pickers.py @@ -3,6 +3,7 @@ import faebryk.library._F as F import faebryk.libs.picker.jlcpcb.picker_lib as P from faebryk.core.module import Module +from faebryk.core.solver import Solver from faebryk.libs.picker.common import StaticPartPicker from faebryk.libs.picker.jlcpcb.jlcpcb import JLCPCB_DB, ComponentQuery from faebryk.libs.picker.picker import PickError @@ -39,18 +40,23 @@ def _find_parts(self, module: Module): return q.get() -def add_jlcpcb_pickers(module: Module, base_prio: int = 0) -> None: +def add_jlcpcb_pickers(module: Module, solver: Solver, base_prio: int = 0) -> None: # check if DB ok JLCPCB_DB() # Generic pickers prio = base_prio - module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_lcsc_id))) - module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_mfr))) + module.add( + F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_lcsc_id, solver)) + ) + module.add(F.has_multi_picker(prio, JLCPCBPicker(P.find_and_attach_by_mfr, solver))) # Type specific pickers prio = base_prio + 1 F.has_multi_picker.add_pickers_by_type( - module, P.TYPE_SPECIFIC_LOOKUP, JLCPCBPicker, prio + module, + P.TYPE_SPECIFIC_LOOKUP, + lambda pick_fn: JLCPCBPicker(pick_fn, solver), + prio, ) diff --git a/src/faebryk/libs/picker/picker.py b/src/faebryk/libs/picker/picker.py index 4f1ea8a5..964775de 100644 --- a/src/faebryk/libs/picker/picker.py +++ b/src/faebryk/libs/picker/picker.py @@ -15,7 +15,8 @@ import faebryk.library._F as F from faebryk.core.module import Module from faebryk.core.moduleinterface import ModuleInterface -from faebryk.core.parameter import Parameter +from faebryk.core.parameter import Parameter, ParameterOperatable, Predicate +from faebryk.core.solver import Solver from faebryk.libs.util import flatten, not_none logger = logging.getLogger(__name__) @@ -41,7 +42,12 @@ class DescriptiveProperties(StrEnum): @dataclass class PickerOption: part: Part - params: dict[str, Parameter] | None = None + params: dict[str, ParameterOperatable.NonParamSet] | None = None + """ + Parameters that need to be matched for this option to be valid. + + Assumes specified params are narrowest possible value for this part + """ filter: Callable[[Module], bool] | None = None pinmap: dict[str, F.Electrical] | None = None info: dict[str | DescriptiveProperties, str] | None = None @@ -147,32 +153,50 @@ def mark_no_pick_needed(module: Module): ) -def pick_module_by_params(module: Module, options: Iterable[PickerOption]): +def pick_module_by_params( + module: Module, solver: Solver, options: Iterable[PickerOption] +): if module.has_trait(has_part_picked): logger.debug(f"Ignoring already picked module: {module}") return params = { - not_none(p.get_parent())[1]: p.get_most_narrow() + not_none(p.get_parent())[1]: p for p in module.get_children(direct_only=True, types=Parameter) } - options = list(options) + filtered_options = [o for o in options if not o.filter or o.filter(module)] + predicates: dict[PickerOption, ParameterOperatable.BooleanLike] = {} + for o in filtered_options: + predicate_list: list[Predicate] = [] - try: - option = next( - filter( - lambda o: (not o.filter or o.filter(module)) - and all( - v.is_subset_of(params.get(k, F.ANY())) - for k, v in (o.params or {}).items() - if not k.startswith("_") - ), - options, - ) - ) - except StopIteration: - raise PickErrorParams(module, options) + for k, v in (o.params or {}).items(): + if not k.startswith("_"): + param = params[k] + predicate_list.append(param.operation_is_superset(v)) + + # No predicates, thus always valid option + if len(predicate_list) == 0: + predicates[o] = True + continue + + anded = predicate_list[0] + for p in predicate_list[1:]: + anded = anded.operation_and(p) + + predicates[o] = anded + + if len(predicates) == 0: + raise PickErrorParams(module, list(options)) + + solve_result = solver.assert_any_predicate( + [(p, k) for k, p in predicates.items()], lock=True + ) + + # TODO handle failure parameters + + # pick first valid option + _, option = next(iter(solve_result.true_predicates)) if option.pinmap: module.add(F.can_attach_to_footprint_via_pinmap(option.pinmap)) @@ -180,11 +204,12 @@ def pick_module_by_params(module: Module, options: Iterable[PickerOption]): option.part.supplier.attach(module, option) module.add(has_part_picked_defined(option.part)) - # Merge params from footprint option + # Shrink solution space that we need to search for + # by hinting that option params are biggest possible set we might want to support for k, v in (option.params or {}).items(): if k not in params: continue - params[k].override(v) + params[k].alias_is(v) logger.debug(f"Attached {option.part.partno} to {module}") return option diff --git a/src/faebryk/libs/sets.py b/src/faebryk/libs/sets.py new file mode 100644 index 00000000..88c5c175 --- /dev/null +++ b/src/faebryk/libs/sets.py @@ -0,0 +1,765 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +from bisect import bisect +from collections.abc import Generator, Iterable, Iterator +from typing import Any, Protocol, Type, TypeVar, cast + +from faebryk.libs.units import HasUnit, Quantity, Unit, dimensionless + +# Protocols ---------------------------------------------------------------------------- + + +class P_Set[T](Protocol): + def is_empty(self) -> bool: ... + + def __bool__(self) -> bool: + raise Exception("don't use bool to check for emptiness, use is_empty()") + + def __contains__(self, item: T) -> bool: ... + + +class P_IterableSet[T, IterT](P_Set[T], Iterable[IterT], Protocol): ... + + +class P_UnitSet[T](P_Set[T], Protocol): + units: Unit + + +class P_IterableUnitSet[T, IterT](P_UnitSet[T], Iterable[IterT], Protocol): ... + + +# -------------------------------------------------------------------------------------- + +# Types -------------------------------------------------------------------------------- + +NumericT = TypeVar("NumericT", int, float, contravariant=False, covariant=False) +QuantityT = TypeVar( + "QuantityT", int, float, Quantity, contravariant=False, covariant=False +) + + +# -------------------------------------------------------------------------------------- + +# Helpers ------------------------------------------------------------------------------ + + +def base_units(units: Unit) -> Unit: + return cast(Unit, Quantity(1, units).to_base_units().units) + + +# -------------------------------------------------------------------------------------- +# Generic ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class PlainSet[U](P_IterableSet[U, U]): + def __init__(self, *elements: U): + self.elements = set(elements) + + def is_empty(self) -> bool: + return len(self.elements) == 0 + + def __contains__(self, item: U) -> bool: + return item in self.elements + + def __eq__(self, value: Any) -> bool: + if not isinstance(value, PlainSet): + return False + return self.elements == value.elements + + def __hash__(self) -> int: + return sum(hash(e) for e in self.elements) + + def __repr__(self) -> str: + return f"PlainSet({', '.join(repr(e) for e in self.elements)})" + + def __iter__(self) -> Iterator[U]: + return self.elements.__iter__() + + +# Numeric ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class _N_Range(P_Set[NumericT]): + def __init__(self, min: NumericT, max: NumericT): + if not min <= max: + raise ValueError("min must be less than or equal to max") + if min == float("inf") or max == float("-inf"): + raise ValueError("min or max has bad infinite value") + self._min = min + self._max = max + + def is_empty(self) -> bool: + return False + + def is_unbounded(self) -> bool: + return self._min == float("-inf") and self._max == float("inf") + + def min_elem(self) -> NumericT: + return self._min + + def max_elem(self) -> NumericT: + return self._max + + def op_add_range(self, other: "_N_Range[NumericT]") -> "_N_Range[NumericT]": + return _N_Range(self._min + other._min, self._max + other._max) + + def op_negate(self) -> "_N_Range[NumericT]": + return _N_Range(-self._max, -self._min) + + def op_subtract_range(self, other: "_N_Range[NumericT]") -> "_N_Range[NumericT]": + return self.op_add_range(other.op_negate()) + + def op_mul_range(self, other: "_N_Range[NumericT]") -> "_N_Range[NumericT]": + return _N_Range( + min( + self._min * other._min, + self._min * other._max, + self._max * other._min, + self._max * other._max, + ), + max( + self._min * other._min, + self._min * other._max, + self._max * other._min, + self._max * other._max, + ), + ) + + def op_invert(self) -> "_N_Ranges[float]": + if self._min == 0 == self._max: + return _N_Empty() + if self._min < 0 < self._max: + return _N_Ranges( + _N_Range(float("-inf"), 1 / self._min), + _N_Range(1 / self._max, float("inf")), + ) + elif self._min < 0 == self._max: + return _N_Ranges(_N_Range(float("-inf"), 1 / self._min)) + elif self._min == 0 < self._max: + return _N_Ranges(_N_Range(1 / self._max, float("inf"))) + else: + return _N_Ranges(_N_Range(1 / self._max, 1 / self._min)) + + def op_div_range( + self: "_N_Range[float]", other: "_N_Range[float]" + ) -> "_N_Ranges[float]": + return _N_Ranges(*(self.op_mul_range(o) for o in other.op_invert().ranges)) + + def op_intersect_range(self, other: "_N_Range[NumericT]") -> "_N_Ranges[NumericT]": + min_ = max(self._min, other._min) + max_ = min(self._max, other._max) + if min_ <= max_: + return _N_Ranges(_N_Range(min_, max_)) + return _N_Empty() + + def maybe_merge_range( + self, other: "_N_Range[NumericT]" + ) -> list["_N_Range[NumericT]"]: + is_left = self._min <= other._min + left = self if is_left else other + right = other if is_left else self + if right._min in self: + return [_N_Range(left._min, max(left._max, right._max))] + return [left, right] + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, _N_Range): + return False + return self._min == other._min and self._max == other._max + + def __contains__(self, item: NumericT) -> bool: + return self._min <= item <= self._max + + def __hash__(self) -> int: + return hash((self._min, self._max)) + + def __repr__(self) -> str: + return f"_Range({self._min}, {self._max})" + + +def _N_Single(value: NumericT) -> _N_Range[NumericT]: + return _N_Range(value, value) + + +class _N_NonIterableRanges(P_Set[NumericT]): + def __init__(self, *ranges: _N_Range[NumericT] | "_N_NonIterableRanges[NumericT]"): + def gen_flat_non_empty() -> Generator[_N_Range[NumericT]]: + for r in ranges: + if r.is_empty(): + continue + if isinstance(r, _N_NonIterableRanges): + yield from r.ranges + else: + assert isinstance(r, _N_Range) + yield r + + non_empty_ranges = list(gen_flat_non_empty()) + sorted_ranges = sorted(non_empty_ranges, key=lambda e: e.min_elem()) + + def gen_merge(): + last = None + for range in sorted_ranges: + if last is None: + last = range + else: + *prefix, last = last.maybe_merge_range(range) + yield from prefix + if last is not None: + yield last + + self.ranges = list(gen_merge()) + + def is_empty(self) -> bool: + return len(self.ranges) == 0 + + def min_elem(self) -> NumericT: + if self.is_empty(): + raise ValueError("empty range cannot have min element") + return self.ranges[0].min_elem() + + def max_elem(self) -> NumericT: + if self.is_empty(): + raise ValueError("empty range cannot have max element") + return self.ranges[-1].max_elem() + + def closest_elem(self, target: NumericT) -> NumericT: + if self.is_empty(): + raise ValueError("empty range cannot have closest element") + index = bisect(self.ranges, target, key=lambda r: r.min_elem()) + left = self.ranges[index - 1] if index > 0 else None + if left is not None and target in left: + return target + left_bound = left.max_elem() if left is not None else None + right_bound = ( + self.ranges[index].min_elem() if index < len(self.ranges) else None + ) + try: + [one] = [b for b in [left_bound, right_bound] if b is not None] + return one + except ValueError: + assert left_bound and right_bound + if target - left_bound < right_bound - target: + return left_bound + return right_bound + assert False # unreachable + + def is_superset_of(self, other: "_N_NonIterableRanges[NumericT]") -> bool: + return other == other.op_intersect_ranges(self) + + def is_subset_of(self, other: "_N_NonIterableRanges[NumericT]") -> bool: + return other.is_superset_of(self) + + def op_intersect_range( + self, other: "_N_Range[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + return _N_NonIterableRanges(*(r.op_intersect_range(other) for r in self.ranges)) + + def op_intersect_ranges( + self, other: "_N_NonIterableRanges[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + result = [] + s, o = 0, 0 + while s < len(self.ranges) and o < len(other.ranges): + rs, ro = self.ranges[s], other.ranges[o] + intersect = rs.op_intersect_range(ro) + if not intersect.is_empty(): + result.append(intersect) + + if rs.max_elem() < ro.min_elem(): + # no remaining element in other list can intersect with rs + s += 1 + elif ro.max_elem() < rs.min_elem(): + # no remaining element in self list can intersect with ro + o += 1 + elif rs.max_elem() < ro.max_elem(): + # rs ends before ro, so move to next in self list + s += 1 + elif ro.max_elem() < rs.max_elem(): + # ro ends before rs, so move to next in other list + o += 1 + else: + # rs and ro end on same number, so move to next in both lists + s += 1 + o += 1 + + return _N_NonIterableRanges(*result) + + def op_union_ranges( + self, other: "_N_NonIterableRanges[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + return _N_NonIterableRanges(*self.ranges, *other.ranges) + + def op_add_ranges( + self, other: "_N_NonIterableRanges[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + return _N_NonIterableRanges( + *(r.op_add_range(o) for r in self.ranges for o in other.ranges) + ) + + def op_negate(self) -> "_N_NonIterableRanges[NumericT]": + return _N_NonIterableRanges(*(r.op_negate() for r in self.ranges)) + + def op_subtract_ranges( + self, other: "_N_NonIterableRanges[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + return self.op_add_ranges(other.op_negate()) + + def op_mul_ranges( + self, other: "_N_NonIterableRanges[NumericT]" + ) -> "_N_NonIterableRanges[NumericT]": + return _N_NonIterableRanges( + *(r.op_mul_range(o) for r in self.ranges for o in other.ranges) + ) + + def op_invert(self) -> "_N_NonIterableRanges[float]": + return _N_NonIterableRanges(*(r.op_invert() for r in self.ranges)) + + def op_div_ranges( + self: "_N_NonIterableRanges[float]", other: "_N_NonIterableRanges[float]" + ) -> "_N_NonIterableRanges[float]": + return self.op_mul_ranges(other.op_invert()) + + def __contains__(self, item: NumericT) -> bool: + index = bisect(self.ranges, item, key=lambda r: r.min_elem()) + + if index == 0: + return False + return item in self.ranges[index - 1] + + def __eq__(self, value: Any) -> bool: + if not isinstance(value, _N_NonIterableRanges): + return False + if len(self.ranges) != len(value.ranges): + return False + for r1, r2 in zip(self.ranges, value.ranges): + if r1 != r2: + return False + return True + + def __hash__(self) -> int: + return hash(tuple(hash(r) for r in self.ranges)) + + def __repr__(self) -> str: + return f"_N_Ranges({', '.join(f"[{r._min}, {r._max}]" for r in self.ranges)})" + + +class _N_Ranges(_N_NonIterableRanges[NumericT], Iterable[_N_Range[NumericT]]): + def __iter__(self) -> Generator[_N_Range[NumericT]]: + yield from self.ranges + + +class _N_Singles(_N_NonIterableRanges[NumericT], Iterable[NumericT]): + def __init__(self, *values: NumericT): + super().__init__(*(_N_Single(v) for v in values)) + + def __iter__(self) -> Generator[NumericT]: + for r in self.ranges: + yield r._min + + +def _N_Empty() -> _N_Ranges: + return _N_Ranges() + + +# Units ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +class Range(P_UnitSet[QuantityT]): + def __init__( + self, + min: QuantityT | None = None, + max: QuantityT | None = None, + units: Unit | None = None, + ): + if min is None and max is None: + raise ValueError("must provide at least one of min or max") + + min_unit = ( + None + if min is None + else min.units + if isinstance(min, Quantity) + else dimensionless + ) + max_unit = ( + None + if max is None + else max.units + if isinstance(max, Quantity) + else dimensionless + ) + if units and min_unit and not min_unit.is_compatible_with(units): + raise ValueError("min incompatible with units") + if units and max_unit and not max_unit.is_compatible_with(units): + raise ValueError("max incompatible with units") + if min_unit and max_unit and not min_unit.is_compatible_with(max_unit): + raise ValueError("min and max must be compatible") + self.units = units or min_unit or max_unit + self.range_units = base_units(self.units) + + if isinstance(min, Quantity): + num_min = min.to_base_units().magnitude + if not (isinstance(num_min, float) or isinstance(num_min, int)): + raise ValueError("min must be a float or int quantity") + else: + num_min = min + + if isinstance(max, Quantity): + num_max = max.to_base_units().magnitude + if not (isinstance(num_max, float) or isinstance(num_max, int)): + raise ValueError("max must be a float or int quantity") + else: + num_max = max + + is_float = isinstance(num_min, float) or isinstance(num_max, float) + if is_float: + num_min = float(num_min) if num_min is not None else float("-inf") + num_max = float(num_max) if num_max is not None else float("inf") + else: + assert isinstance(num_min, int) or isinstance(num_max, int) + if num_min is None or num_max is None: + raise ValueError("min and max must be provided for ints") + + self._range = _N_Range(num_min, num_max) + + @staticmethod + def from_center(center: QuantityT, abs_tol: QuantityT) -> "Range[QuantityT]": + left = center - abs_tol + right = center + abs_tol + return Range(left, right) + + @staticmethod + def from_center_rel(center: QuantityT, rel_tol: float) -> "Range[QuantityT]": + return Range(center - center * rel_tol, center + center * rel_tol) + + @staticmethod + def _from_range(range: _N_Range[NumericT], units: Unit) -> "Range[QuantityT]": + return Range( + min=Quantity(range._min, base_units(units)), + max=Quantity(range._max, base_units(units)), + units=units, + ) + + def base_to_units(self, value: NumericT) -> Quantity: + return Quantity(value, self.range_units).to(self.units) + + def min_elem(self) -> Quantity: + return self.base_to_units(self._range.min_elem()) + + def max_elem(self) -> Quantity: + return self.base_to_units(self._range.max_elem()) + + def is_empty(self) -> bool: + return self._range.is_empty() + + def is_unbounded(self) -> bool: + return self._range.is_unbounded() + + def op_intersect_range( + self, other: "Range[QuantityT]" + ) -> "NonIterableRanges[QuantityT]": + if not self.units.is_compatible_with(other.units): + return NonIterableRanges(units=self.units) + _range = self._range.op_intersect_range(other._range) + return NonIterableRanges._from_ranges(_range, self.units) + + def op_add_range(self, other: "Range[QuantityT]") -> "Range[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._range.op_add_range(other._range) + return Range._from_range(_range, self.units) + + def op_negate(self) -> "Range[QuantityT]": + _range = self._range.op_negate() + return Range._from_range(_range, self.units) + + def op_subtract_range(self, other: "Range[QuantityT]") -> "Range[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._range.op_subtract_range(other._range) + return Range._from_range(_range, self.units) + + def op_mul_range(self, other: "Range[QuantityT]") -> "Range[QuantityT]": + _range = self._range.op_mul_range(other._range) + return Range._from_range(_range, self.units * other.units) + + def op_invert(self) -> "Ranges[QuantityT]": + _range = self._range.op_invert() + return Ranges._from_ranges(_range, 1 / self.units) + + def op_div_range(self, other: "Range[QuantityT]") -> "NonIterableRanges[QuantityT]": + _range = self._range.op_div_range(other._range) + return NonIterableRanges._from_ranges(_range, self.units / other.units) + + # def __copy__(self) -> Self: + # r = Range.__new__(Range) + # r.min = self.min + # r.max = self.max + # r.empty = self.empty + # r.units = self.units + # return r + + def __contains__(self, item: Any) -> bool: + if isinstance(item, Quantity): + if not item.units.is_compatible_with(self.units): + return False + item = item.to(self.range_units).magnitude + if not isinstance(item, float) and not isinstance(item, int): + return False + return self._range.__contains__(item) + return False + + # yucky with floats + def __eq__(self, value: Any) -> bool: + if not HasUnit.check(value): + return False + if not self.units.is_compatible_with(value.units): + return False + if isinstance(value, Range): + return self._range == value._range + if isinstance(value, NonIterableRanges) and len(value._ranges.ranges) == 1: + return self._range == value._ranges.ranges[0] + return False + + # TODO, convert to base unit first + def __hash__(self) -> int: + return hash((self._range, self.range_units)) + + def __repr__(self) -> str: + if self.units.is_compatible_with(dimensionless): + return f"Range({self._range._min}, {self._range._max})" + return ( + f"Range({self.base_to_units(self._range._min)}, " + f"{self.base_to_units(self._range._max)} | {self.units})" + ) + + +class Single(Range[QuantityT]): + def __init__(self, value: QuantityT): + super().__init__(value, value) + + def get_value(self) -> QuantityT: + return self.min_elem() + + def __iter__(self) -> Generator[Quantity]: + yield self.min_elem() + + +NonIterableRangesT = TypeVar("NonIterableRangesT", bound="NonIterableRanges") + + +class NonIterableRanges(P_UnitSet[QuantityT]): + def __init__( + self, + *ranges: Range[QuantityT] + | "NonIterableRanges[QuantityT]" + | tuple[QuantityT, QuantityT], + units: Unit | None = None, + ): + proper_ranges = [ + Range(r[0], r[1]) if isinstance(r, tuple) else r for r in ranges + ] + range_units = [HasUnit.get_units_or_dimensionless(r) for r in proper_ranges] + if len(range_units) == 0 and units is None: + raise ValueError("units must be provided for empty union") + self.units = units or range_units[0] + self.range_units = base_units(self.units) + if not all(self.units.is_compatible_with(u) for u in range_units): + raise ValueError("all elements must have compatible units") + + def get_backing(r: Range[QuantityT] | "NonIterableRanges[QuantityT]"): + if isinstance(r, Range): + return r._range + else: + return r._ranges + + self._ranges = _N_Ranges(*(get_backing(r) for r in proper_ranges)) + + @classmethod + def _from_ranges( + cls: Type[NonIterableRangesT], + ranges: "_N_NonIterableRanges[NumericT]", + units: Unit, + ) -> NonIterableRangesT: + r = cls.__new__(cls) + r._ranges = ranges + r.units = units + r.range_units = base_units(units) + return r + + def is_empty(self) -> bool: + return self._ranges.is_empty() + + def base_to_units(self, value: NumericT) -> Quantity: + return Quantity(value, self.range_units).to(self.units) + + def min_elem(self) -> Quantity: + if self.is_empty(): + raise ValueError("empty range cannot have min element") + return self.base_to_units(self._ranges.min_elem()) + + def max_elem(self) -> Quantity: + if self.is_empty(): + raise ValueError("empty range cannot have max element") + return self.base_to_units(self._ranges.max_elem()) + + def closest_elem(self, target: Quantity) -> Quantity: + if not self.units.is_compatible_with(target.units): + raise ValueError("incompatible units") + return self.base_to_units( + self._ranges.closest_elem(target.to(self.range_units).magnitude) + ) + + def is_superset_of(self, other: "NonIterableRanges[QuantityT]") -> bool: + if not self.units.is_compatible_with(other.units): + return False + return self._ranges.is_superset_of(other._ranges) + + def is_subset_of(self, other: "NonIterableRanges[QuantityT]") -> bool: + return other.is_superset_of(self) + + def op_intersect_range(self, other: "Range[QuantityT]") -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_intersect_range(other._range) + return Ranges._from_ranges(_range, self.units) + + def op_intersect_ranges( + self, *other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + # TODO make pretty + def single(left, right): + if not left.units.is_compatible_with(right.units): + raise ValueError("incompatible units") + _range = left._ranges.op_intersect_ranges(right._ranges) + return Ranges._from_ranges(_range, left.units) + + out = Ranges(self) + + for o in other: + out = single(out, o) + + return out + + def op_union_ranges( + self, other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_union_ranges(other._ranges) + return Ranges._from_ranges(_range, self.units) + + def op_add_ranges( + self, other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_add_ranges(other._ranges) + return Ranges._from_ranges(_range, self.units) + + def __add__(self, other) -> "NonIterableRanges[QuantityT]": + if isinstance(other, NonIterableRanges): + return self.op_add_ranges(other) + elif isinstance(other, Range): + return self.op_add_ranges(Ranges(other)) + elif isinstance(other, Quantity): + return self.op_add_ranges(Singles(other)) + elif isinstance(other, int) or isinstance(other, float): + return self.op_add_ranges(Singles(other * dimensionless)) + return NotImplemented + + def __radd__(self, other) -> "NonIterableRanges[QuantityT]": + return self + other + + def op_negate(self) -> "Ranges[QuantityT]": + _range = self._ranges.op_negate() + return Ranges._from_ranges(_range, self.units) + + def op_subtract_ranges( + self, other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_subtract_ranges(other._ranges) + return Ranges._from_ranges(_range, self.units) + + def op_mul_ranges( + self, other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_mul_ranges(other._ranges) + return Ranges._from_ranges(_range, self.units * other.units) + + def op_invert(self) -> "Ranges[QuantityT]": + _range = self._ranges.op_invert() + return Ranges._from_ranges(_range, 1 / self.units) + + def op_div_ranges( + self, other: "NonIterableRanges[QuantityT]" + ) -> "Ranges[QuantityT]": + if not self.units.is_compatible_with(other.units): + raise ValueError("incompatible units") + _range = self._ranges.op_div_ranges(other._ranges) + return Ranges._from_ranges(_range, self.units / other.units) + + def __contains__(self, item: Any) -> bool: + if isinstance(item, Quantity): + if not item.units.is_compatible_with(self.units): + return False + item = item.to(self.range_units).magnitude + if not isinstance(item, float) and not isinstance(item, int): + return False + return self._ranges.__contains__(item) + return False + + def __eq__(self, value: Any) -> bool: + if not HasUnit.check(value): + return False + if not self.units.is_compatible_with(value.units): + return False + if isinstance(value, NonIterableRanges): + return self._ranges == value._ranges + if isinstance(value, Range) and len(self._ranges.ranges) == 1: + return self._ranges.ranges[0] == value._range + return False + + def __hash__(self) -> int: + return hash((self._ranges, self.range_units)) + + def __repr__(self) -> str: + if self.units.is_compatible_with(dimensionless): + inner = ", ".join(f"[{r._min}, {r._max}]" for r in self._ranges.ranges) + return f"Ranges({inner})" + inner = ", ".join( + f"[{self.base_to_units(r._min)}, {self.base_to_units(r._max)}]" + for r in self._ranges.ranges + ) + return f"Ranges({inner} | {self.units})" + + +class Ranges(NonIterableRanges[QuantityT], Iterable[Range[QuantityT]]): + def __iter__(self) -> Generator[Range[QuantityT]]: + for r in self._ranges.ranges: + yield Range._from_range(r, self.units) + + def is_unbounded(self) -> bool: + if self.is_empty(): + return False + return next(iter(self)).is_unbounded() + + +def Empty(units: Unit | None = None) -> Ranges[QuantityT]: + if units is None: + units = dimensionless + return Ranges(units=units) + + +class Singles(NonIterableRanges[QuantityT]): + def __init__(self, *values: QuantityT, units: Unit | None = None): + super().__init__(*(Single(v) for v in values), units=units) + + def __iter__(self) -> Generator[Quantity]: + for r in self._ranges.ranges: + yield self.base_to_units(r._min) diff --git a/src/faebryk/libs/test/solver.py b/src/faebryk/libs/test/solver.py new file mode 100644 index 00000000..56d4dd1e --- /dev/null +++ b/src/faebryk/libs/test/solver.py @@ -0,0 +1,22 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT +from faebryk.core.defaultsolver import DefaultSolver +from faebryk.core.graphinterface import Graph +from faebryk.core.node import Node +from faebryk.core.parameter import ParameterOperatable +from faebryk.libs.sets import PlainSet + + +def solves_to(stmt: ParameterOperatable, result: bool): + stmt.inspect_add_on_solution(lambda x: x.inspect_known_values() == PlainSet(result)) + + +def solve_and_test(G: Graph | Node, *stmts: ParameterOperatable): + if isinstance(G, Node): + G = G.get_graph() + + for stmt in stmts: + solves_to(stmt, True) + + solver = DefaultSolver() + solver.find_and_lock_solution(G) diff --git a/src/faebryk/libs/units.py b/src/faebryk/libs/units.py index e4bf4c51..526e161a 100644 --- a/src/faebryk/libs/units.py +++ b/src/faebryk/libs/units.py @@ -2,14 +2,37 @@ # SPDX-License-Identifier: MIT # re-exporting Quantity in-case we ever want to change it +from typing import Any + from pint import Quantity as _Quantity # noqa: F401 from pint import UndefinedUnitError, Unit, UnitRegistry # noqa: F401 from pint.util import UnitsContainer as _UnitsContainer +from faebryk.libs.util import cast_assert + P = UnitRegistry() UnitsContainer = _UnitsContainer | str Quantity = P.Quantity +dimensionless = cast_assert(Unit, P.dimensionless) + + +def quantity(value: float | int, unit: UnitsContainer | Unit | Quantity) -> Quantity: + return P.Quantity(value, unit) + + +class HasUnit: + units: Unit + + @staticmethod + def check(obj: Any) -> bool: + return hasattr(obj, "units") or isinstance(obj, Unit) + + @staticmethod + def get_units_or_dimensionless(obj: Any) -> Unit: + if isinstance(obj, Unit): + return obj + return obj.units if HasUnit.check(obj) else dimensionless def to_si_str( diff --git a/src/faebryk/libs/util.py b/src/faebryk/libs/util.py index 849645b6..9b31bd09 100644 --- a/src/faebryk/libs/util.py +++ b/src/faebryk/libs/util.py @@ -16,7 +16,8 @@ from contextlib import contextmanager from dataclasses import dataclass, fields from enum import StrEnum -from itertools import chain +from genericpath import commonprefix +from itertools import chain, pairwise from pathlib import Path from textwrap import indent from typing import ( @@ -1155,6 +1156,140 @@ def setdefault(self, key: T, default: U) -> U: return default +def dict_map_values(d: dict, function: Callable[[Any], Any]) -> dict: + """recursively map all values in a dict""" + + result = {} + for key, value in d.items(): + if isinstance(value, dict): + result[key] = dict_map_values(value, function) + elif isinstance(value, list): + result[key] = [dict_map_values(v, function) for v in value] + else: + result[key] = function(value) + return result + + +def merge_dicts(*dicts: dict) -> dict: + """merge a list of dicts into a single dict, + if same key is present and value is list, lists are merged + if same key is dict, dicts are merged recursively + """ + result = {} + for d in dicts: + for k, v in d.items(): + if k in result: + if isinstance(v, list): + assert isinstance( + result[k], list + ), f"Trying to merge list into key '{k}' of type {type(result[k])}" + result[k] += v + elif isinstance(v, dict): + assert isinstance(result[k], dict) + result[k] = merge_dicts(result[k], v) + else: + result[k] = v + else: + result[k] = v + return result + + +def abstract[T: type](cls: T) -> T: + """ + Mark a class as abstract. + """ + + old_new = cls.__new__ + + def _new(cls_, *args, **kwargs): + if cls_ is cls: + raise TypeError(f"{cls.__name__} is abstract and cannot be instantiated") + return old_new(cls_, *args, **kwargs) + + cls.__new__ = _new + return cls + + +def typename(x: object | type) -> str: + if not isinstance(x, type): + x = type(x) + return x.__name__ + + +def dict_value_visitor(d: dict, visitor: Callable[[Any, Any], Any]): + for k, v in list(d.items()): + if isinstance(v, dict): + dict_value_visitor(v, visitor) + else: + d[k] = visitor(k, v) + + +class DefaultFactoryDict[T, U](dict[T, U]): + def __init__(self, factory: Callable[[T], U], *args, **kwargs): + self.factory = factory + super().__init__(*args, **kwargs) + + def __missing__(self, key: T) -> U: + res = self.factory(key) + self[key] = res + return res + + +class EquivalenceClasses[T: Hashable]: + def __init__(self, base: Iterable[T] | None = None): + self.classes: dict[T, set[T]] = DefaultFactoryDict(lambda k: {k}) + for elem in base or []: + self.classes[elem] + + def add_eq(self, *values: T): + if len(values) < 2: + return + val1 = values[0] + for val in values[1:]: + self.classes[val1].update(self.classes[val]) + for v in self.classes[val]: + self.classes[v] = self.classes[val1] + + def get(self) -> list[set[T]]: + sets = {id(s): s for s in self.classes.values()} + return list(sets.values()) + + +def common_prefix_to_tree(iterable: list[str]) -> Iterable[str]: + """ + Turns: + + <760>|RP2040.adc[0]|ADC.reference|ElectricPower.max_current|Parameter + <760>|RP2040.adc[0]|ADC.reference|ElectricPower.voltage|Parameter + <760>|RP2040.adc[1]|ADC.reference|ElectricPower.max_current|Parameter + <760>|RP2040.adc[1]|ADC.reference|ElectricPower.voltage|Parameter + + Into: + + <760>|RP2040.adc[0]|ADC.reference|ElectricPower.max_current|Parameter + -----------------------------------------------.voltage|Parameter + -----------------1]|ADC.reference|ElectricPower.max_current|Parameter + -----------------------------------------------.voltage|Parameter + + Notes: + Recommended to sort the iterable first. + """ + yield iterable[0] + + for s1, s2 in pairwise(iterable): + prefix = commonprefix([s1, s2]) + prefix_length = len(prefix) + yield "-" * prefix_length + s2[prefix_length:] + + +def ind[T: str | list[str]](lines: T) -> T: + prefix = " " + if isinstance(lines, str): + return indent(lines, prefix=prefix) + if isinstance(lines, list): + return [f"{prefix}{line}" for line in lines] # type: ignore + + def run_live( *args, logger: logging.Logger = logger, @@ -1240,24 +1375,7 @@ def global_lock(lock_file_path: Path, timeout_s: float | None = None): lock_file_path.unlink(missing_ok=True) -def typename(x: object | type) -> str: - if not isinstance(x, type): - x = type(x) - return x.__name__ - - def consume(iter: Iterable, n: int) -> list: assert n >= 0 out = list(itertools.islice(iter, n)) return out if len(out) == n else [] - - -class DefaultFactoryDict[T, U](dict[T, U]): - def __init__(self, factory: Callable[[T], U], *args, **kwargs): - self.factory = factory - super().__init__(*args, **kwargs) - - def __missing__(self, key: T) -> U: - res = self.factory(key) - self[key] = res - return res diff --git a/test/core/test_parameters.py b/test/core/test_parameters.py index c6c4f026..8afdff05 100644 --- a/test/core/test_parameters.py +++ b/test/core/test_parameters.py @@ -2,362 +2,199 @@ # SPDX-License-Identifier: MIT import logging -import unittest -from operator import add +from itertools import pairwise + +import pytest import faebryk.library._F as F -from faebryk.core.core import logger as core_logger +from faebryk.core.defaultsolver import DefaultSolver from faebryk.core.module import Module +from faebryk.core.node import Node from faebryk.core.parameter import Parameter -from faebryk.libs.app.parameters import resolve_dynamic_parameters -from faebryk.libs.units import P +from faebryk.libs.library import L +from faebryk.libs.logging import setup_basic_logging +from faebryk.libs.sets import Range, Ranges +from faebryk.libs.units import P, dimensionless +from faebryk.libs.util import times logger = logging.getLogger(__name__) -core_logger.setLevel(logger.getEffectiveLevel()) -class TestParameters(unittest.TestCase): - def test_operations(self): - def assertIsInstance[T: Parameter](obj: Parameter, cls: type[T]) -> T: - obj = obj.get_most_narrow() - self.assertIsInstance(obj, cls) - assert isinstance(obj, cls) - return obj +def test_new_definitions(): + _ = Parameter( + units=P.ohm, + domain=L.Domains.Numbers.REAL(negative=False), + soft_set=Range(1 * P.ohm, 10 * P.Mohm), + likely_constrained=True, + ) - # Constant - ONE = F.Constant(1) - self.assertEqual(ONE.value, 1) - TWO = F.Constant(2) - self.assertEqual(assertIsInstance(ONE + TWO, F.Constant).value, 3) - self.assertEqual(assertIsInstance(ONE - TWO, F.Constant).value, -1) +def test_solve_phase_one(): + solver = DefaultSolver() - self.assertEqual(assertIsInstance((ONE / TWO) / TWO, F.Constant).value, 1 / 4) + def Voltage(): + return L.p_field(units=P.V, within=Range(0 * P.V, 10 * P.kV)) - # Range - R_ONE_TEN = F.Range(1, 10) - self.assertEqual(assertIsInstance(R_ONE_TEN + TWO, F.Range), F.Range(3, 12)) + class App(Module): + voltage1 = Voltage() + voltage2 = Voltage() + voltage3 = Voltage() - R_TWO_THREE = F.Range(2, 3) - self.assertEqual( - assertIsInstance(R_ONE_TEN + R_TWO_THREE, F.Range), F.Range(3, 13) - ) - self.assertEqual( - assertIsInstance(R_ONE_TEN * R_TWO_THREE, F.Range), F.Range(2, 30) - ) - self.assertEqual( - assertIsInstance(R_ONE_TEN - R_TWO_THREE, F.Range), F.Range(-2, 8) - ) - self.assertEqual( - assertIsInstance(R_ONE_TEN / R_TWO_THREE, F.Range), F.Range(1 / 3, 10 / 2) - ) + app = App() + voltage1 = app.voltage1 + voltage2 = app.voltage2 + voltage3 = app.voltage3 - # TBD Range - a = F.TBD() - b = F.TBD() - R_TBD = F.Range(a, b) - add = R_ONE_TEN + R_TBD - mul = R_ONE_TEN * R_TBD - sub = R_ONE_TEN - R_TBD - div = R_ONE_TEN / R_TBD - a.merge(F.Constant(2)) - b.merge(F.Constant(3)) - self.assertEqual(assertIsInstance(add, F.Range), F.Range(3, 13)) - self.assertEqual(assertIsInstance(mul, F.Range), F.Range(2, 30)) - self.assertEqual(assertIsInstance(sub, F.Range), F.Range(-2, 8)) - self.assertEqual(assertIsInstance(div, F.Range), F.Range(1 / 3, 10 / 2)) - - # Set - S_FIVE_NINE = F.Set(set(F.Constant(x) for x in range(5, 10))) - self.assertEqual( - assertIsInstance(S_FIVE_NINE + ONE, F.Set).params, - set(F.Constant(x) for x in range(6, 11)), - ) + voltage1.alias_is(voltage2) + voltage3.alias_is(voltage1 + voltage2) - S_TEN_TWENTY_THIRTY = F.Set(set(F.Constant(x) for x in [10, 20, 30])) - self.assertEqual( - assertIsInstance(S_FIVE_NINE + S_TEN_TWENTY_THIRTY, F.Set), - F.Set(F.Constant(x + y) for x in range(5, 10) for y in [10, 20, 30]), - ) + voltage1.alias_is(Range(1 * P.V, 3 * P.V)) + voltage3.alias_is(Range(4 * P.V, 6 * P.V)) - # conjunctions - # with static values - R_ONE_TEN = F.Range(1, 10) - R_TWO_THREE = F.Range(2, 3) - self.assertEqual(R_ONE_TEN & R_TWO_THREE, F.Range(2, 3)) - self.assertEqual(R_ONE_TEN & F.Range(5, 20), F.Range(5, 10)) - self.assertEqual(R_ONE_TEN & 5, F.Constant(5)) - self.assertEqual(R_ONE_TEN & F.Constant(5), F.Constant(5)) - self.assertEqual(R_ONE_TEN & F.Set([1, 5, 8, 12]), F.Set([1, 5, 8])) - self.assertEqual(F.Set([1, 2, 3]) & F.Set([2, 3, 4]), F.Set([2, 3])) - self.assertEqual(F.Set([1, 2, 3]) & 3, F.Constant(3)) - self.assertEqual(F.Constant(3) & 3, F.Constant(3)) - self.assertEqual(F.Constant(2) & 3, F.Set([])) - self.assertEqual(R_ONE_TEN & {1, 2, 11}, F.Set([1, 2])) - self.assertEqual(R_ONE_TEN & F.Range(12, 13), F.Set([])) - # with tbd - a = F.TBD() - b = F.TBD() - RTBD = F.Range(a, b) - r_one_ten_con_tbd = R_ONE_TEN & RTBD - assertIsInstance(r_one_ten_con_tbd, F.Operation) - a.merge(2) - b.merge(20) - self.assertEqual(assertIsInstance(r_one_ten_con_tbd, F.Range), F.Range(2, 10)) - - # TODO disjunctions - - # F.Operation - token = F.TBD() - op = assertIsInstance(ONE + token, F.Operation) - op2 = assertIsInstance(op + 10, F.Operation) - - self.assertEqual(op.operands, (ONE, F.TBD())) - self.assertEqual(op.operation(1, 2), 3) - - token.merge(F.Constant(2)) - self.assertEqual(op.get_most_narrow(), F.Constant(3)) - - self.assertEqual(op + 5, F.Constant(8)) - self.assertEqual(op2.get_most_narrow(), F.Constant(13)) - - # Any - assertIsInstance(ONE + F.ANY(), F.Operation) - assertIsInstance(F.TBD() + F.ANY(), F.Operation) - assertIsInstance((F.TBD() + F.TBD()) + F.ANY(), F.Operation) - - # Test quantities - self.assertEqual(F.Constant(1 * P.baud), 1 * P.baud) - self.assertEqual(F.Constant(1) * P.baud, 1 * P.baud) - self.assertEqual(F.Range(1, 10) * P.baud, F.Range(1 * P.baud, 10 * P.baud)) - self.assertEqual(F.Set([1, 2]) * P.baud, F.Set([1 * P.baud, 2 * P.baud])) - - def test_resolution(self): - def assertIsInstance[T](obj, cls: type[T]) -> T: - self.assertIsInstance(obj, cls) - assert isinstance(obj, cls) - return obj - - ONE = F.Constant(1) - self.assertEqual( - assertIsInstance(Parameter.resolve_all([ONE, ONE]), F.Constant).value, 1 - ) + solver.phase_one_no_guess_solving(voltage1.get_graph()) - TWO = F.Constant(2) - self.assertEqual( - assertIsInstance( - Parameter.resolve_all([F.Operation([ONE, ONE], add), TWO]), F.Constant - ).value, - 2, - ) - self.assertEqual(F.TBD(), F.TBD()) - self.assertEqual(F.ANY(), F.ANY()) - - def test_merge( - a: Parameter | set[int] | int | tuple[int, int], - b: Parameter | set[int] | int | tuple[int, int], - expected, - ): - a = Parameter.from_literal(a) - expected = Parameter.from_literal(expected) - self.assertEqual(a.merge(b), expected) - - def fail_merge(a, b): - a = Parameter.from_literal(a) - self.assertRaises(Parameter.MergeException, lambda: a.merge(b)) - - # F.Sets ---- - - # F.Ranges - test_merge((0, 10), (5, 15), (5, 10)) - test_merge((0, 10), (5, 8), (5, 8)) - fail_merge((0, 10), (11, 15)) - test_merge((5, 10), 5, 5) - fail_merge((0, 10), 11) - test_merge((5, 10), {5, 6, 12}, {5, 6}) - - # Empty set - fail_merge({1, 2}, set()) - fail_merge((1, 5), set()) - fail_merge(5, set()) - test_merge(set(), set(), set()) - test_merge(F.TBD(), set(), set()) - test_merge(F.ANY(), set(), set()) - - test_merge({1, 2}, {2, 3}, {2}) - fail_merge({1, 2}, {3, 4}) - test_merge({1, 2}, 2, 2) - - # F.TBD/F.ANY -- - - test_merge(F.TBD(), F.TBD(), F.TBD()) - test_merge(F.ANY(), F.ANY(), F.ANY()) - test_merge(F.TBD(), F.ANY(), F.ANY()) - - def test_specific(self): - def test_spec( - a: Parameter | set[int] | int | tuple[int, int], - b: Parameter | set[int] | int | tuple[int, int], - expected: bool = True, - ): - b = Parameter.from_literal(b) - if expected: - self.assertTrue(b.is_subset_of(a)) - else: - self.assertFalse(b.is_subset_of(a)) - - test_spec(1, 1) - test_spec(1, 2, False) - - test_spec((1, 2), 1) - test_spec(1, (1, 2), False) - - test_spec({1, 2}, 1) - test_spec(1, {1, 2}, False) - test_spec(1, {1}) - - test_spec((1, 2), (1, 2)) - test_spec((1, 2), (1, 3), False) - test_spec((1, 10), (1, 3)) - - test_spec(1, F.ANY(), False) - test_spec(F.ANY(), 1) - test_spec(F.TBD(), 1, False) - test_spec(F.ANY(), F.Operation((1, 2), add)) - test_spec(F.ANY(), F.Operation((1, F.TBD()), add)) - - test_spec(F.Operation((1, 2), add), 3) - test_spec(F.Operation((1, F.TBD()), add), F.TBD(), False) - - def test_compress(self): - def test_comp( - a: Parameter.LIT_OR_PARAM, - expected: Parameter.LIT_OR_PARAM, - ): - a = Parameter.from_literal(a) - expected = Parameter.from_literal(expected) - self.assertEqual(a.get_most_narrow(), expected) - - test_comp(1, 1) - test_comp(F.Constant(F.Constant(1)), 1) - test_comp(F.Constant(F.Constant(F.Constant(1))), 1) - test_comp({1}, 1) - test_comp(F.Range(1), 1) - test_comp(F.Range(F.Range(1)), 1) - test_comp(F.Constant(F.Set([F.Range(F.Range(1))])), 1) - - def test_modules(self): - class Modules(Module): - UART_A: F.UART_Base - UART_B: F.UART_Base - UART_C: F.UART_Base - - m = Modules() - - UART_A = m.UART_A - UART_B = m.UART_B - UART_C = m.UART_C - - UART_A.connect(UART_B) - - UART_A.baud.merge(F.Constant(9600 * P.baud)) - - resolve_dynamic_parameters(m.get_graph()) - - for uart in [UART_A, UART_B]: - self.assertEqual(uart.baud.get_most_narrow(), 9600 * P.baud) - - UART_C.baud.merge(F.Range(1200 * P.baud, 115200 * P.baud)) - UART_A.connect(UART_C) - resolve_dynamic_parameters(m.get_graph()) - - for uart in [UART_A, UART_B, UART_C]: - self.assertEqual(uart.baud.get_most_narrow(), 9600 * P.baud) - - resistor = F.Resistor() - - self.assertIsInstance( - resistor.get_current_flow_by_voltage_resistance(F.Constant(0.5)), - F.Operation, +def test_simplify(): + class App(Module): + ops = L.list_field( + 10, + lambda: Parameter( + units=dimensionless, within=Range(0, 1, units=dimensionless) + ), ) - def test_comparisons(self): - # same type - self.assertGreater(F.Constant(2), F.Constant(1)) - self.assertLess(F.Constant(1), F.Constant(2)) - self.assertLessEqual(F.Constant(2), F.Constant(2)) - self.assertGreaterEqual(F.Constant(2), F.Constant(2)) - self.assertLess(F.Range(1, 2), F.Range(3, 4)) - self.assertEqual( - min(F.Range(1, 2), F.Range(3, 4), F.Range(5, 6)), F.Range(1, 2) - ) + app = App() - # mixed - self.assertLess(F.Constant(1), F.Range(2, 3)) - self.assertGreater(F.Constant(4), F.Range(2, 3)) - self.assertFalse(F.Constant(3) < F.Range(2, 4)) - self.assertFalse(F.Constant(3) > F.Range(2, 4)) - self.assertFalse(F.Constant(3) == F.Range(2, 4)) - self.assertEqual( - min(F.Constant(3), F.Range(5, 6), F.Constant(4)), F.Constant(3) - ) + # (((((((((((A + B + 1) + C + 2) * D * 3) * E * 4) * F * 5) * G * (A - A)) + H + 7) + # + I + 8) + J + 9) - 3) - 4) < 11 + # => (H + I + J + 17) < 11 + constants = [c * dimensionless for c in range(0, 10)] + constants[5] = app.ops[0] - app.ops[0] + constants[9] = Ranges(Range(0 * dimensionless, 1 * dimensionless)) + acc = app.ops[0] + for i, p in enumerate(app.ops[1:3]): + acc += p + constants[i] + for i, p in enumerate(app.ops[3:7]): + acc *= p * constants[i + 3] + for i, p in enumerate(app.ops[7:]): + acc += p + constants[i + 7] - # nested - self.assertLess(F.Constant(1), F.Set([F.Constant(2), F.Constant(3)])) - self.assertLess(F.Range(1, 2), F.Range(F.Constant(3), F.Constant(4))) - self.assertLess(F.Range(1, 2), F.Set([F.Constant(4), F.Constant(3)])) - self.assertLess(F.Constant(F.Constant(F.Constant(1))), 2) - self.assertEqual( - min(F.Constant(F.Constant(F.Constant(1))), F.Constant(F.Constant(2))), - F.Constant(F.Constant(F.Constant(1))), - ) + acc = (acc - 3 * dimensionless) - 4 * dimensionless + (acc < 11 * dimensionless).constrain() + + G = acc.get_graph() + solver = DefaultSolver() + solver.phase_one_no_guess_solving(G) + + +def test_remove_obvious_tautologies(): + p0, p1, p2 = (Parameter(units=dimensionless) for _ in range(3)) + p0.alias_is(p1 + p2) + p1.constrain_ge(0) + p2.constrain_ge(0) + p2.alias_is(p2) + + G = p0.get_graph() + solver = DefaultSolver() + solver.phase_one_no_guess_solving(G) + + +def test_subset_of_literal(): + p0, p1, p2 = ( + Parameter(units=dimensionless, within=Range(0, i, units=dimensionless)) + for i in range(3) + ) + p0.alias_is(p1) + p1.alias_is(p2) - def test_specialize(self): - import faebryk.library._F as F - from faebryk.libs.brightness import TypicalLuminousIntensity + G = p0.get_graph() + solver = DefaultSolver() + solver.phase_one_no_guess_solving(G) - class App(Module): - led: F.PoweredLED - battery: F.Battery - def __preinit__(self) -> None: - self.led.power.connect(self.battery.power) +def test_alias_classes(): + p0, p1, p2, p3, p4 = ( + Parameter(units=dimensionless, within=Range(0, i)) for i in range(5) + ) + p0.alias_is(p1) + addition = p2 + p3 + p1.alias_is(addition) + addition2 = p3 + p2 + p4.alias_is(addition2) - # Parametrize - self.led.led.color.merge(F.LED.Color.YELLOW) - self.led.led.brightness.merge( - TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value - ) + G = p0.get_graph() + solver = DefaultSolver() + solver.phase_one_no_guess_solving(G) - app = App() - bcell = app.battery.specialize(F.ButtonCell()) - bcell.voltage.merge(3 * P.V) - bcell.capacity.merge(F.Range.from_center(225 * P.mAh, 50 * P.mAh)) - bcell.material.merge(F.ButtonCell.Material.Lithium) - bcell.size.merge(F.ButtonCell.Size.N_2032) - bcell.shape.merge(F.ButtonCell.Shape.Round) +def test_solve_realworld(): + app = F.RP2040() + solver = DefaultSolver() + solver.phase_one_no_guess_solving(app.get_graph()) - app.led.led.color.merge(F.LED.Color.YELLOW) - app.led.led.max_brightness.merge(500 * P.millicandela) - app.led.led.forward_voltage.merge(1.2 * P.V) - app.led.led.max_current.merge(20 * P.mA) - resolve_dynamic_parameters(app.get_graph()) +def test_visualize(): + """ + Creates webserver that opens automatically if run in jupyter notebook + """ + from faebryk.exporters.visualize.interactive_params import visualize_parameters - v = app.battery.voltage - # vbcell = bcell.voltage - # print(pretty_param_tree_top(v)) - # print(pretty_param_tree_top(vbcell)) - self.assertEqual(v.get_most_narrow(), 3 * P.V) - r = app.led.current_limiting_resistor.resistance - r = r.get_most_narrow() - self.assertIsInstance(r, F.Range, f"{type(r)}") + class App(Node): + p1 = L.f_field(Parameter)(units=P.V) - def test_units(self): - self.assertEqual(F.Constant(1e-9 * P.F), 1 * P.nF) + app = App() + p2 = Parameter(units=P.V) + p3 = Parameter(units=P.A) + # app.p1.constrain_ge(p2 * 5) + # app.p1.operation_is_ge(p2 * 5).constrain() + (app.p1 >= p2 * 5).constrain() + + (p2 * p3 + app.p1 * 1 * P.A <= 10 * P.W).constrain() + + pytest.raises(ValueError, bool, app.p1 >= p2 * 5) + + G = app.get_graph() + visualize_parameters(G, height=1400) + + +def test_visualize_chain(): + from faebryk.exporters.visualize.interactive_params import visualize_parameters + + params = times(10, Parameter) + sums = [p1 + p2 for p1, p2 in pairwise(params)] + products = [p1 * p2 for p1, p2 in pairwise(sums)] + bigsum = sum(products) + + predicates = [bigsum <= 100] + for p in predicates: + p.constrain() + + G = params[0].get_graph() + visualize_parameters(G, height=1400) + + +def test_visualize_inspect_app(): + from faebryk.exporters.visualize.interactive_params import visualize_parameters + + rp2040 = F.RP2040() + + G = rp2040.get_graph() + visualize_parameters(G, height=1400) + + +# TODO remove if __name__ == "__main__": - unittest.main() + # if run in jupyter notebook + import sys + + func = test_solve_realworld + + if "ipykernel" in sys.modules: + func() + else: + import typer + + setup_basic_logging() + typer.run(func) diff --git a/test/exporters/netlist/kicad/test_netlist_kicad.py b/test/exporters/netlist/kicad/test_netlist_kicad.py index 70bfb944..e6fd9e91 100644 --- a/test/exporters/netlist/kicad/test_netlist_kicad.py +++ b/test/exporters/netlist/kicad/test_netlist_kicad.py @@ -19,8 +19,12 @@ # Netlists -------------------------------------------------------------------- def _test_netlist_graph(): - resistor1 = F.Resistor().builder(lambda r: r.resistance.merge(100 * P.ohm)) - resistor2 = F.Resistor().builder(lambda r: r.resistance.merge(200 * P.ohm)) + resistor1 = F.Resistor().builder( + lambda r: r.resistance.constrain_subset(100 * P.ohm) + ) + resistor2 = F.Resistor().builder( + lambda r: r.resistance.constrain_subset(200 * P.ohm) + ) power = F.ElectricPower() # net labels diff --git a/test/library/nodes/test_electricpower.py b/test/library/nodes/test_electricpower.py index 972098bf..739c8a4c 100644 --- a/test/library/nodes/test_electricpower.py +++ b/test/library/nodes/test_electricpower.py @@ -1,50 +1,56 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import unittest -from itertools import pairwise -import faebryk.library._F as F from faebryk.libs.app.parameters import resolve_dynamic_parameters -from faebryk.libs.units import P -from faebryk.libs.util import times +from faebryk.libs.library import L +from faebryk.libs.test.solver import solve_and_test +from faebryk.libs.util import pairwise, times -class TestFusedPower(unittest.TestCase): - def test_fused_power(self): - power_in = F.ElectricPower() - power_out = F.ElectricPower() +def test_fused_power(): + import faebryk.library._F as F + from faebryk.libs.units import P - power_in.voltage.merge(10 * P.V) - power_in.max_current.merge(500 * P.mA) + power_in = F.ElectricPower() + power_out = F.ElectricPower() - power_in_fused = power_in.fused() + power_in.voltage.constrain_subset(10 * P.V) + power_in.max_current.constrain_subset(500 * P.mA) - power_in_fused.connect(power_out) + power_in_fused = power_in.fused() + power_in_fused.connect(power_out) - fuse = next(iter(power_in_fused.get_children(direct_only=False, types=F.Fuse))) - resolve_dynamic_parameters(fuse.get_graph()) + fuse = next(iter(power_in_fused.get_children(direct_only=False, types=F.Fuse))) + resolve_dynamic_parameters(fuse.get_graph()) - self.assertEqual(fuse.trip_current.get_most_narrow(), F.Constant(500 * P.mA)) - self.assertEqual(power_out.voltage.get_most_narrow(), 10 * P.V) - # self.assertEqual( - # power_in_fused.max_current.get_most_narrow(), F.Range(0 * P.A, 500 * P.mA) - # ) - self.assertEqual(power_out.max_current.get_most_narrow(), F.TBD()) + solve_and_test( + power_in, + fuse.trip_current.operation_is_subset(L.Range.from_center_rel(500 * P.mA, 0.1)), + power_out.voltage.operation_is_subset(10 * P.V), + power_out.max_current.operation_is_le(500 * P.mA * 0.9), + ) - def test_voltage_propagation(self): - powers = times(4, F.ElectricPower) - powers[0].voltage.merge(F.Range(10 * P.V, 15 * P.V)) +def test_voltage_propagation(self): + import faebryk.library._F as F + from faebryk.libs.units import P - for p1, p2 in pairwise(powers): - p1.connect(p2) + powers = times(4, F.ElectricPower) - resolve_dynamic_parameters(powers[0].get_graph()) + powers[0].voltage.alias_is(L.Range(10 * P.V, 15 * P.V)) - self.assertEqual( - powers[-1].voltage.get_most_narrow(), F.Range(10 * P.V, 15 * P.V) - ) + for p1, p2 in pairwise(powers): + p1.connect(p2) - powers[3].voltage.merge(10 * P.V) - self.assertEqual(powers[0].voltage.get_most_narrow(), 10 * P.V) + resolve_dynamic_parameters(powers[0].get_graph()) + solve_and_test( + powers[-1], + powers[-1].voltage.operation_is_subset(L.Range(10 * P.V, 15 * P.V)), + ) + + powers[3].voltage.alias_is(10 * P.V) + solve_and_test( + powers[0], + powers[0].voltage.operation_is_subset(10 * P.V), + ) diff --git a/test/libs/picker/test_pickers.py b/test/libs/picker/test_pickers.py index 6b996761..03ee7298 100644 --- a/test/libs/picker/test_pickers.py +++ b/test/libs/picker/test_pickers.py @@ -222,8 +222,8 @@ def test_find_resistor(self): requirement=F.Resistor().builder( lambda r: ( r.resistance.merge(F.Range.from_center(10 * P.kohm, 1 * P.kohm)), - r.rated_power.merge(F.Range.lower_bound(0.05 * P.W)), - r.rated_voltage.merge(F.Range.lower_bound(25 * P.V)), + r.max_power.merge(F.Range.lower_bound(0.05 * P.W)), + r.max_voltage.merge(F.Range.lower_bound(25 * P.V)), ) ), footprint=[("0402", 2)], @@ -235,8 +235,8 @@ def test_find_resistor(self): requirement=F.Resistor().builder( lambda r: ( r.resistance.merge(F.Range.from_center(69 * P.kohm, 2 * P.kohm)), - r.rated_power.merge(F.Range.lower_bound(0.1 * P.W)), - r.rated_voltage.merge(F.Range.lower_bound(50 * P.V)), + r.max_power.merge(F.Range.lower_bound(0.1 * P.W)), + r.max_voltage.merge(F.Range.lower_bound(50 * P.V)), ) ), footprint=[("0603", 2)], @@ -249,7 +249,7 @@ def test_find_capacitor(self): requirement=F.Capacitor().builder( lambda c: ( c.capacitance.merge(F.Range.from_center(100 * P.nF, 10 * P.nF)), - c.rated_voltage.merge(F.Range.lower_bound(25 * P.V)), + c.max_voltage.merge(F.Range.lower_bound(25 * P.V)), c.temperature_coefficient.merge( F.Range.lower_bound(F.Capacitor.TemperatureCoefficient.X7R) ), @@ -264,7 +264,7 @@ def test_find_capacitor(self): requirement=F.Capacitor().builder( lambda c: ( c.capacitance.merge(F.Range.from_center(47 * P.pF, 4.7 * P.pF)), - c.rated_voltage.merge(F.Range.lower_bound(50 * P.V)), + c.max_voltage.merge(F.Range.lower_bound(50 * P.V)), c.temperature_coefficient.merge( F.Range.lower_bound(F.Capacitor.TemperatureCoefficient.C0G) ), @@ -280,7 +280,7 @@ def test_find_inductor(self): requirement=F.Inductor().builder( lambda i: ( i.inductance.merge(F.Range.from_center(4.7 * P.nH, 0.47 * P.nH)), - i.rated_current.merge(F.Range.lower_bound(0.01 * P.A)), + i.max_current.merge(F.Range.lower_bound(0.01 * P.A)), i.dc_resistance.merge(F.Range.upper_bound(1 * P.ohm)), i.self_resonant_frequency.merge( F.Range.lower_bound(100 * P.Mhertz) @@ -417,8 +417,8 @@ def r_builder(resistance_kohm: float): r.resistance.merge( F.Range.from_center_rel(resistance_kohm * P.kohm, 0.1) ), - r.rated_power.merge(F.ANY()), - r.rated_voltage.merge(F.ANY()), + r.max_power.merge(F.ANY()), + r.max_voltage.merge(F.ANY()), ) ) @@ -428,7 +428,7 @@ def c_builder(capacitance_pf: float): c.capacitance.merge( F.Range.from_center_rel(capacitance_pf * P.pF, 0.1) ), - c.rated_voltage.merge(F.ANY()), + c.max_voltage.merge(F.ANY()), c.temperature_coefficient.merge(F.ANY()), ) ) diff --git a/test/libs/test_e_series.py b/test/libs/test_e_series.py index 36a2b7f5..332fca3f 100644 --- a/test/libs/test_e_series.py +++ b/test/libs/test_e_series.py @@ -1,78 +1,54 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import unittest from itertools import pairwise -import faebryk.library._F as F from faebryk.libs.e_series import ( E_SERIES_VALUES, e_series_intersect, e_series_ratio, ) - - -class TestESeries(unittest.TestCase): - def test_intersect(self): - self.assertEqual( - e_series_intersect(F.Range(1, 10), {1, 2, 3}), - F.Set([F.Constant(1), F.Constant(2), F.Constant(3), F.Constant(10)]), - ) - self.assertEqual( - e_series_intersect(F.Range(3, 10), {1, 8, 9}), - F.Set([F.Constant(8), F.Constant(9), F.Constant(10)]), - ) - self.assertEqual( - e_series_intersect(F.Range(10, 1e3), {1, 1.5, 8, 9.9}), - F.Set( - [ - F.Constant(10), - F.Constant(15), - F.Constant(80), - F.Constant(99), - F.Constant(100), - F.Constant(150), - F.Constant(800), - F.Constant(990), - F.Constant(1000), - ] - ), - ) - self.assertEqual( - e_series_intersect(F.Range(2.1e3, 7.9e3), {1, 2, 8, 9}), - F.Set([]), - ) - - def test_ratio(self): - self.assertEqual( - e_series_ratio( - F.Range(100, 10e3), - F.Range(100, 10e3), - F.Constant(1 / 5), - E_SERIES_VALUES.E24, - ), - (F.Constant(1.2e3), F.Constant(300)), - ) - self.assertEqual( - e_series_ratio( - F.Range(100, 10e3), - F.Range(100, 10e3), - F.Range.from_center(0.0123, 0.0123 / 10), - E_SERIES_VALUES.E48, - ), - (F.Constant(9.09e3), F.Constant(115)), - ) - - def test_sets(self): - E = E_SERIES_VALUES - EVs24 = [3 * 2**i for i in range(4)] - EVs192 = [3 * 2**i for i in range(4, 6)] - for EVs in [EVs24, EVs192]: - for i1, i2 in pairwise(EVs): - e1 = getattr(E, f"E{i1}") - e2 = getattr(E, f"E{i2}") - self.assertTrue(e1 < e2, f"{i1} < {i2}") - - -if __name__ == "__main__": - unittest.main() +from faebryk.libs.library import L +from faebryk.libs.units import dimensionless + + +def test_intersect(): + assert e_series_intersect(L.Range(1, 10), frozenset({1, 2, 3})) == L.Singles( + 1, 2, 3, 10 + ) + assert e_series_intersect(L.Range(3, 10), frozenset({1, 8, 9})) == L.Singles( + 8, 9, 10 + ) + assert e_series_intersect( + L.Range(10, 1e3), frozenset({1, 1.5, 8, 9.9}) + ) == L.Singles(10, 15, 80, 99, 100, 150, 800, 990, 1000) + assert e_series_intersect( + L.Range(2.1e3, 7.9e3), frozenset({1, 2, 8, 9}) + ) == L.Empty(units=dimensionless) + + +def test_ratio(): + assert e_series_ratio( + L.Range(100, 10e3), + L.Range(100, 10e3), + L.Single(1 / 5), + E_SERIES_VALUES.E24, + ) == (1.2e3, 300) + + assert e_series_ratio( + L.Range(100, 10e3), + L.Range(100, 10e3), + L.Range.from_center(0.0123, 0.0123 / 10), + E_SERIES_VALUES.E48, + ) == (9.09e3, 115) + + +def test_sets(): + E = E_SERIES_VALUES + EVs24 = [3 * 2**i for i in range(4)] + EVs192 = [3 * 2**i for i in range(4, 6)] + for EVs in [EVs24, EVs192]: + for i1, i2 in pairwise(EVs): + e1 = getattr(E, f"E{i1}") + e2 = getattr(E, f"E{i2}") + assert e1 < e2, f"{i1} < {i2}" diff --git a/test/libs/test_sets.py b/test/libs/test_sets.py new file mode 100644 index 00000000..55b07e72 --- /dev/null +++ b/test/libs/test_sets.py @@ -0,0 +1,190 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import pytest + +from faebryk.libs.sets import ( + Empty, + Range, + Ranges, + Single, + Singles, +) +from faebryk.libs.units import P, Unit, dimensionless +from faebryk.libs.util import cast_assert + + +def test_range_intersection_simple(): + x = Range(0, 10) + y = x.op_intersect_range(Range(5, 15)) + assert y == Range(5, 10) + + +def test_range_intersection_empty(): + x = Range(0, 10) + y = x.op_intersect_range(Range(15, 20)) + assert y == Empty(dimensionless) + + +def test_range_unit_none(): + x = Range(0, 10) + assert not x.units.is_compatible_with(P.V) + + +def test_range_unit_same(): + y = Range(0 * P.V, 10 * P.V) + assert y.units.is_compatible_with(P.V) + + +def test_range_unit_different(): + with pytest.raises(ValueError): + Range(0 * P.V, 10 * P.A) + with pytest.raises(ValueError): + Range(0 * P.V, 10 * P.V, units=cast_assert(Unit, P.A)) + with pytest.raises(ValueError): + Range(max=10 * P.V, units=cast_assert(Unit, P.A)) + with pytest.raises(ValueError): + Range(min=10 * P.V, units=cast_assert(Unit, P.A)) + + +def test_set_min_elem(): + x = Singles(5, 3, 2, 4, 1) + assert x.min_elem() == 1 + + +def test_set_closest_elem(): + x = Ranges((5, 6), (7, 8), Singles(2, 4, 1)) + assert x.closest_elem(0 * dimensionless) == 1 + assert x.closest_elem(1 * dimensionless) == 1 + assert x.closest_elem(5.1 * dimensionless) == 5.1 * dimensionless + assert x.closest_elem(4.9 * dimensionless) == 5 * dimensionless + assert x.closest_elem(4.1 * dimensionless) == 4 * dimensionless + assert x.closest_elem(6.9 * dimensionless) == 7 * dimensionless + + +def test_set_contains(): + x = Singles(5, 3, 2, 4, 1) + assert 3 * dimensionless in x + assert 6 * dimensionless not in x + + +def test_union_min_elem(): + x = Ranges( + (4, 5), + (3, 7), + Single(9), + Ranges(Range(1, 2), Ranges(Range(0, 1))), + ) + assert x.min_elem() == 0 + + +def test_union_contains(): + x = Ranges( + (4, 5), + (3, 7), + Single(9), + Ranges((1, 2), Ranges((0, 1))), + ) + assert 0 * dimensionless in x + assert 1 * dimensionless in x + assert 2 * dimensionless in x + assert 3 * dimensionless in x + assert 4 * dimensionless in x + assert 5 * dimensionless in x + assert 6 * dimensionless in x + assert 7 * dimensionless in x + assert 8 * dimensionless not in x + assert 9 * dimensionless in x + assert 10 * dimensionless not in x + + x = Ranges(Range(max=1.5 * P.V), Range(2.5 * P.V, 3.5 * P.V)) + assert float("-inf") * P.V in x + assert 1 * P.V in x + assert 1.5 * P.V in x + assert 2 * P.V not in x + assert 2.5 * P.V in x + assert 3 * P.V in x + assert 3.5 * P.V in x + assert 4 * P.V not in x + assert float("inf") * P.V not in x + assert 1 not in x + assert 1 * dimensionless not in x + + +def test_union_empty(): + x = Ranges( + Empty(dimensionless), + Ranges(Empty(dimensionless), Singles(units=dimensionless)), + ) + assert x.is_empty() + + +def test_add_empty(): + assert (Empty(dimensionless).op_add_ranges(Ranges((0, 1)))) == Empty(dimensionless) + + +def test_addition(): + assert Range(0, 1).op_add_range(Range(2, 3)) == Range(2, 4) + assert Range(0, 1).op_add_range(Single(2)) == Range(2, 3) + assert Ranges(Single(2), Single(3)).op_add_ranges(Ranges((0, 1))) == Range(2, 4) + assert Ranges(Single(10), Range(20, 21)).op_add_ranges( + Ranges((0, 1), (100, 101)) + ) == Ranges((10, 11), (110, 111), (20, 22), (120, 122)) + + +def test_addition_unit(): + assert Range(0 * P.V, 1 * P.V).op_add_range(Range(2 * P.V, 3 * P.V)) == Range( + 2 * P.V, 4 * P.V + ) + + +def test_subtraction(): + assert Range(0, 1).op_subtract_range(Range(2, 3)) == Range(-3, -1) + assert Range(0, 1).op_subtract_range(Single(2)) == Range(-2, -1) + + +def test_subtraction_unit(): + assert Range(0 * P.V, 1 * P.V).op_subtract_range(Range(2 * P.V, 3 * P.V)) == Range( + -3 * P.V, -1 * P.V + ) + + +def test_multiplication(): + assert Range(0, 2).op_mul_range(Range(2, 3)) == Range(0, 6) + assert Range(0, 1).op_mul_range(Single(2)) == Range(0, 2) + assert Range(0, 1).op_mul_range(Single(-2)) == Range(-2, 0) + assert Range(-1, 1).op_mul_range(Range(2, 4)) == Range(-4, 4) + assert Singles(0, 1).op_mul_ranges(Singles(2, 3)) == Singles(0, 2, 3) + assert Singles(0, 1).op_mul_ranges(Singles(2, 3)).op_mul_ranges( + Ranges((-1, 0)) + ) == Ranges((0, 0), (-2, 0), (-3, 0)) + + +def test_multiplication_unit(): + assert Range(0 * P.V, 2 * P.V).op_mul_range(Range(2 * P.A, 3 * P.A)) == Range( + 0 * P.W, 6 * P.W + ) + + +def test_invert(): + assert Range(1, 2).op_invert() == Range(0.5, 1) + assert Range(-2, -1).op_invert() == Range(-1, -0.5) + assert Range(-1, 1).op_invert() == Ranges((float("-inf"), -1), (1, float("inf"))) + assert Ranges((-4, 2), (-1, 3)).op_invert() == Ranges( + Range(max=-0.25), Range(min=1 / 3) + ) + + +def test_invert_unit(): + assert Range(1 * P.V, 2 * P.V).op_invert() == Range(1 / (2 * P.V), 1 / (1 * P.V)) + + +def test_division(): + assert Range(0, 1).op_div_range(Range(2, 3)) == Range(0, 0.5) + assert Range(0, 1).op_div_range(Range(0, 3)) == Range(min=0.0) + + +def test_division_unit(): + assert Range(0 * P.V, 1 * P.V).op_div_range(Range(2 * P.A, 3 * P.A)) == Range( + 0 * P.ohm, 1 / 2 * P.ohm + )