From f5e6bda26215e36cd6f2b3529d93f1659ae542fa Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 15 Aug 2024 12:10:15 -0700 Subject: [PATCH 1/6] Library: Add NetTie to library --- src/faebryk/library/KicadFootprint.py | 4 +++ src/faebryk/library/NetTie.py | 51 +++++++++++++++++++++++++++ src/faebryk/library/_F.py | 1 + 3 files changed, 56 insertions(+) create mode 100644 src/faebryk/library/NetTie.py diff --git a/src/faebryk/library/KicadFootprint.py b/src/faebryk/library/KicadFootprint.py index 9026c68f..a942d110 100644 --- a/src/faebryk/library/KicadFootprint.py +++ b/src/faebryk/library/KicadFootprint.py @@ -9,6 +9,10 @@ class KicadFootprint(F.Footprint): def __init__(self, kicad_identifier: str, pin_names: list[str]) -> None: super().__init__() + assert ":" in kicad_identifier, ( + 'kicad_identifier must be in the format "library:footprint".' + " If not, it'll cause downstream problems" + ) unique_pin_names = sorted(set(pin_names)) self.pin_names_sorted = list(enumerate(unique_pin_names)) diff --git a/src/faebryk/library/NetTie.py b/src/faebryk/library/NetTie.py new file mode 100644 index 00000000..cabca41d --- /dev/null +++ b/src/faebryk/library/NetTie.py @@ -0,0 +1,51 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import logging +from enum import Enum + +import faebryk.library._F as F +from faebryk.core.module import Module +from faebryk.core.moduleinterface import ModuleInterface +from faebryk.library.Electrical import Electrical +from faebryk.library.KicadFootprint import KicadFootprint +from faebryk.libs.picker.picker import has_part_picked_remove + +logger = logging.getLogger(__name__) + + +class NetTie[T: ModuleInterface](Module): + class Size(float, Enum): + _2_0MM = 2.0 + _0_5MM = 0.5 + + unnamed: list[T] + + def __init__( + self, + width: Size, + interface_type: type[T] = Electrical, + ) -> None: + super().__init__() + + # dynamically construct the interfaces + self.unnamed = self.add([interface_type(), interface_type()], "unnamed") + + # add dem trairs + self.add(F.can_bridge_defined(*self.unnamed)) + + width_mm = NetTie.Size(width).value + + self.add(F.can_attach_to_footprint_symmetrically()) + self.add(F.has_designator_prefix_defined("H")) + # TODO: "removed" isn't really true, but seems to work + self.add(has_part_picked_remove()) + + # TODO: generate the kicad footprint instead of loading it + self.add( + F.has_footprint_defined( + KicadFootprint( + f"NetTie:NetTie-2_SMD_Pad{width_mm:.1f}mm", pin_names=["1", "2"] + ) + ) + ) diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 891280d3..c6353e3b 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -114,6 +114,7 @@ from faebryk.library.Capacitor import Capacitor from faebryk.library.Fuse import Fuse from faebryk.library.Inductor import Inductor +from faebryk.library.NetTie import NetTie from faebryk.library.Resistor import Resistor from faebryk.library.Switch import Switch from faebryk.library.B4B_ZR_SM4_TF import B4B_ZR_SM4_TF From 5a372a8145b606ad9b6e287fd29e4c3453e96388 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 15 Aug 2024 14:35:19 -0700 Subject: [PATCH 2/6] Library: Add remove_if util --- src/faebryk/library/has_multi_picker.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/faebryk/library/has_multi_picker.py b/src/faebryk/library/has_multi_picker.py index 106c9e4a..7d188503 100644 --- a/src/faebryk/library/has_multi_picker.py +++ b/src/faebryk/library/has_multi_picker.py @@ -4,13 +4,13 @@ import logging from abc import abstractmethod -from typing import Callable, Mapping +from typing import Callable, Mapping, Self import faebryk.library._F as F from faebryk.core.module import Module from faebryk.core.node import Node from faebryk.core.trait import TraitImpl -from faebryk.libs.picker.picker import PickError +from faebryk.libs.picker.picker import PickError, has_part_picked_remove logger = logging.getLogger(__name__) @@ -84,3 +84,19 @@ def handle_duplicate(self, other: TraitImpl, node: Node) -> bool: other.pickers.extend(self.pickers) other.pickers.sort(key=lambda x: x[0]) return False + + @classmethod + def remove_if[T: Module]( + cls, m: T, condition: Callable[[T], bool], prio: int = -10 + ) -> Self: + def replace(module: Module): + assert module is m + + if condition(module): + module.add_trait(has_part_picked_remove()) + + raise PickError("", m) + + m.add(has_multi_picker(prio, has_multi_picker.FunctionPicker(replace))) + + return m From ed88839b0c479539a293aba9d7985f1837339c9e Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 3 Sep 2024 12:27:08 -0700 Subject: [PATCH 3/6] Core: Remove dev print statement --- src/faebryk/libs/util.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/faebryk/libs/util.py b/src/faebryk/libs/util.py index 65c25eec..120a54e2 100644 --- a/src/faebryk/libs/util.py +++ b/src/faebryk/libs/util.py @@ -688,7 +688,6 @@ def force_init(self): class Lazy(LazyMixin): def __init_subclass__(cls) -> None: - print("SUBCLASS", cls) super().__init_subclass__() lazy_construct(cls) From 1169587411800ed3f75c54a5e8d9ce33d1fabf37 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Tue, 3 Sep 2024 12:37:46 -0700 Subject: [PATCH 4/6] Utils: Interactive viewer improvements: Move FuncSet and FuncDict hashable collections to libs/util. Make colours for links and nodes unique. --- .../exporters/visualize/interactive_graph.py | 64 +++++++++------- src/faebryk/exporters/visualize/util.py | 64 +++++++++++----- src/faebryk/libs/util.py | 76 +++++++++++++++++++ test/libs/util_func_collections.py | 15 ++++ 4 files changed, 172 insertions(+), 47 deletions(-) create mode 100644 test/libs/util_func_collections.py diff --git a/src/faebryk/exporters/visualize/interactive_graph.py b/src/faebryk/exporters/visualize/interactive_graph.py index 5b78d9ee..8f75cb07 100644 --- a/src/faebryk/exporters/visualize/interactive_graph.py +++ b/src/faebryk/exporters/visualize/interactive_graph.py @@ -8,10 +8,16 @@ from faebryk.core.graphinterface import GraphInterface from faebryk.core.link import Link from faebryk.core.node import Node -from faebryk.exporters.visualize.util import IDSet, generate_pastel_palette +from faebryk.exporters.visualize.util import ( + generate_pastel_palette, + offer_missing_install, +) +from faebryk.libs.util import FuncSet def interactive_graph(G: Graph): + offer_missing_install("dash_cytoscape") + offer_missing_install("dash") import dash_cytoscape as cyto from dash import Dash, html @@ -48,7 +54,7 @@ def _node(node: Node): return {"data": data} link_types: set[str] = set() - links_touched = IDSet[Link]() + links_touched = FuncSet[Link]() def _link(link: Link): if link in links_touched: @@ -105,10 +111,16 @@ def _not_none(x): }, ] - def _pastels(iterable): - return zip(iterable, generate_pastel_palette(len(iterable))) + def _pastels(iterable, offset=0, spare=0): + return zip( + iterable, + generate_pastel_palette(len(iterable) + offset + spare)[ + offset : -spare or None + ], + ) - for node_type, color in _pastels(node_types): + print("Node types:") + for node_type, color in _pastels(node_types, spare=len(node_types)): stylesheet.append( { "selector": f'node[type = "{node_type}"]', @@ -116,6 +128,25 @@ def _pastels(iterable): } ) + colored_text = rich.text.Text(f"{node_type}: {color}") + colored_text.stylize(f"on {color}") + rich.print(colored_text) + print("\n") + + print("Link types:") + for link_type, color in _pastels(link_types, offset=len(node_types)): + stylesheet.append( + { + "selector": f'edge[type = "{link_type}"]', + "style": {"line-color": color, "target-arrow-color": color}, + } + ) + + colored_text = rich.text.Text(f"{link_type}: {color}") + colored_text.stylize(f"on {color}") + rich.print(colored_text) + print("\n") + stylesheet.append( { "selector": 'node[type = "group"]', @@ -128,14 +159,6 @@ def _pastels(iterable): } ) - for link_type, color in _pastels(link_types): - stylesheet.append( - { - "selector": f'edge[type = "{link_type}"]', - "style": {"line-color": color, "target-arrow-color": color}, - } - ) - container_style = { "position": "fixed", "display": "flex", @@ -193,19 +216,4 @@ def _pastels(iterable): ], ) - # print the color palette - print("Node types:") - for node_type, color in _pastels(node_types): - colored_text = rich.text.Text(f"{node_type}: {color}") - colored_text.stylize(f"on {color}") - rich.print(colored_text) - print("\n") - - print("Link types:") - for link_type, color in _pastels(link_types): - colored_text = rich.text.Text(f"{link_type}: {color}") - colored_text.stylize(f"on {color}") - rich.print(colored_text) - print("\n") - app.run() diff --git a/src/faebryk/exporters/visualize/util.py b/src/faebryk/exporters/visualize/util.py index 2544c372..69c85054 100644 --- a/src/faebryk/exporters/visualize/util.py +++ b/src/faebryk/exporters/visualize/util.py @@ -1,5 +1,49 @@ import colorsys -from typing import Iterable, Sequence, Set +import importlib +import subprocess +import sys +from types import ModuleType + +from rich.prompt import Confirm + + +class InstallationError(Exception): + """Raised when there's a problem installing a module.""" + + +def offer_install(module_name, install_name=None, ex=None) -> ModuleType | None: + """ + Offer to install a missing module using pip. + """ + cmd = [sys.executable, "-m", "pip", "install", install_name or module_name] + + print(f"The module '{module_name}' is not installed.") + + if Confirm.ask( + f"Do you want to run the install command [cyan mono]`{' '.join(cmd)}`[/]" + ): + try: + # Attempt to install the module using pip + subprocess.check_call(cmd) + + except subprocess.CalledProcessError: + print(f"Failed to install {module_name}. Please install it manually.") + raise ex or InstallationError(f"Failed to install {module_name}") + + print(f"Successfully installed {module_name}") + return importlib.import_module(module_name) + + +def offer_missing_install( + module_name: str, install_name: str = None +) -> ModuleType | None: + """ + Offer to install a missing module using pip. + """ + try: + return importlib.import_module(module_name) + except ModuleNotFoundError: + return offer_install(module_name, install_name) def generate_pastel_palette(num_colors: int) -> list[str]: @@ -32,21 +76,3 @@ def generate_pastel_palette(num_colors: int) -> list[str]: palette.append(hex_color) return palette - - -# TODO: this belongs elsewhere -class IDSet[T](Set[T]): - def __init__(self, data: Sequence[T] | None = None): - self._data = set(data) if data is not None else set() - - def add(self, item: T): - self._data.add(id(item)) - - def __contains__(self, item: T): - return id(item) in self._data - - def __iter__(self) -> Iterable[T]: - return iter(self._data) - - def __len__(self) -> int: - return len(self._data) diff --git a/src/faebryk/libs/util.py b/src/faebryk/libs/util.py index 120a54e2..0236b2e6 100644 --- a/src/faebryk/libs/util.py +++ b/src/faebryk/libs/util.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: MIT import asyncio +import collections.abc import inspect import logging from abc import abstractmethod @@ -13,6 +14,7 @@ from typing import ( Any, Callable, + Hashable, Iterable, Iterator, List, @@ -853,3 +855,77 @@ def zip_exhaust(*args): def join_if_non_empty(sep: str, *args): return sep.join(s for arg in args if (s := str(arg))) + + +class FuncSet[T](collections.abc.Set[T]): + """ + A set by pre-processing the objects with the hasher function. + """ + + def __init__( + self, data: Sequence[T] | None = None, hasher: Callable[[T], Hashable] = id + ): + self._hasher = hasher + self._deref = {hasher(d): d for d in data} if data else {} + + def add(self, item: T): + self._deref[self._hasher(item)] = item + + def __contains__(self, item: T): + return self._hasher(item) in self._deref + + def __iter__(self) -> Iterable[T]: + return iter(self._deref.values()) + + def __len__(self) -> int: + return len(self._deref) + + def __repr__(self) -> str: + assert self._hasher is id, "Cannot reliably repr other hasher functions" + return f"{self.__class__.__name__}({repr(list(self))})" + + +class FuncDict[T, U](collections.abc.MutableMapping[T, U]): + """ + A dict by pre-processing the objects with the hasher function. + """ + + def __init__( + self, + data: Sequence[tuple[T, U]] | None = None, + hasher: Callable[[T], Hashable] = id, + ): + self._hasher = hasher + self._deref = {hasher(d[0]): d[0] for d in data} if data else {} + self._data = {hasher(d[0]): d[1] for d in data} if data else {} + + def __contains__(self, item: T): + return self._hasher(item) in self._deref + + def __iter__(self) -> Iterable[T]: + return iter(self._deref.values()) + + def __len__(self) -> int: + return len(self._deref) + + def __getitem__(self, key: T) -> U: + return self._data[self._hasher(key)] + + def __setitem__(self, key: T, value: U): + hashed_key = self._hasher(key) + self._data[hashed_key] = value + self._deref[hashed_key] = key + + def __delitem__(self, key: T): + hashed_key = self._hasher(key) + del self._data[hashed_key] + del self._deref[hashed_key] + + def items(self) -> Iterable[tuple[T, U]]: + """Iter key-value pairs as items, just like a dict.""" + for key, value in self._data.items(): + yield self._deref[key], value + + def __repr__(self) -> str: + assert self._hasher is id, "Cannot reliably repr other hasher functions" + return f"{self.__class__.__name__}({repr(list(self.items()))})" diff --git a/test/libs/util_func_collections.py b/test/libs/util_func_collections.py new file mode 100644 index 00000000..9e8fe186 --- /dev/null +++ b/test/libs/util_func_collections.py @@ -0,0 +1,15 @@ +from faebryk.libs.util import FuncDict, FuncSet + + +def test_func_dict_contains(): + a = FuncDict([(1, 2), (FuncDict, 4), (FuncSet, 5)]) + assert 1 in a + assert FuncDict in a + assert FuncSet in a + + assert a[1] == 2 + assert a[FuncDict] == 4 + assert a[FuncSet] == 5 + + a[id] = 10 + assert a[id] == 10 From e263abb8de8339cba572d6b386577461becfbdf9 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Wed, 4 Sep 2024 15:55:10 -0700 Subject: [PATCH 5/6] Core: Improve field construction exceptions --- src/faebryk/core/node.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/faebryk/core/node.py b/src/faebryk/core/node.py index 637a9829..f39d3234 100644 --- a/src/faebryk/core/node.py +++ b/src/faebryk/core/node.py @@ -42,6 +42,8 @@ logger = logging.getLogger(__name__) +# TODO: should this be a FaebrykException? +# TODO: should this include node and field information? class FieldError(Exception): pass @@ -110,6 +112,13 @@ def __init__(self, node: "Node", *args: object) -> None: self.node = node +class FieldConstructionError(FaebrykException): + def __init__(self, node: "Node", field: str, *args: object) -> None: + super().__init__(*args) + self.node = node + self.field = field + + class NodeAlreadyBound(NodeException): def __init__(self, node: "Node", other: "Node", *args: object) -> None: super().__init__( @@ -291,7 +300,7 @@ def append(name, inst): return inst - def setup_field(name, obj): + def _setup_field(name, obj): def setup_gen_alias(name, obj): origin = get_origin(obj) assert origin @@ -321,6 +330,16 @@ def setup_gen_alias(name, obj): raise NotImplementedError() + def setup_field(name, obj): + try: + _setup_field(name, obj) + except Exception as e: + raise FieldConstructionError( + self, + name, + f'An exception occurred while constructing field "{name}"', + ) from e + nonrt, rt = partition(lambda x: isinstance(x[1], rt_field), clsfields.items()) for name, obj in nonrt: setup_field(name, obj) From 37622b76081f741e007cc50fd319de02fc96b327 Mon Sep 17 00:00:00 2001 From: Matthew Wildoer Date: Thu, 5 Sep 2024 13:32:39 -0700 Subject: [PATCH 6/6] Utils: Move offer_missing_install to libs/installation.py --- .../exporters/visualize/interactive_graph.py | 3 -- src/faebryk/exporters/visualize/util.py | 45 ------------------ src/faebryk/libs/installation.py | 46 +++++++++++++++++++ 3 files changed, 46 insertions(+), 48 deletions(-) create mode 100644 src/faebryk/libs/installation.py diff --git a/src/faebryk/exporters/visualize/interactive_graph.py b/src/faebryk/exporters/visualize/interactive_graph.py index 8f75cb07..8d0ebbf9 100644 --- a/src/faebryk/exporters/visualize/interactive_graph.py +++ b/src/faebryk/exporters/visualize/interactive_graph.py @@ -10,14 +10,11 @@ from faebryk.core.node import Node from faebryk.exporters.visualize.util import ( generate_pastel_palette, - offer_missing_install, ) from faebryk.libs.util import FuncSet def interactive_graph(G: Graph): - offer_missing_install("dash_cytoscape") - offer_missing_install("dash") import dash_cytoscape as cyto from dash import Dash, html diff --git a/src/faebryk/exporters/visualize/util.py b/src/faebryk/exporters/visualize/util.py index 69c85054..34a85e94 100644 --- a/src/faebryk/exporters/visualize/util.py +++ b/src/faebryk/exporters/visualize/util.py @@ -1,49 +1,4 @@ import colorsys -import importlib -import subprocess -import sys -from types import ModuleType - -from rich.prompt import Confirm - - -class InstallationError(Exception): - """Raised when there's a problem installing a module.""" - - -def offer_install(module_name, install_name=None, ex=None) -> ModuleType | None: - """ - Offer to install a missing module using pip. - """ - cmd = [sys.executable, "-m", "pip", "install", install_name or module_name] - - print(f"The module '{module_name}' is not installed.") - - if Confirm.ask( - f"Do you want to run the install command [cyan mono]`{' '.join(cmd)}`[/]" - ): - try: - # Attempt to install the module using pip - subprocess.check_call(cmd) - - except subprocess.CalledProcessError: - print(f"Failed to install {module_name}. Please install it manually.") - raise ex or InstallationError(f"Failed to install {module_name}") - - print(f"Successfully installed {module_name}") - return importlib.import_module(module_name) - - -def offer_missing_install( - module_name: str, install_name: str = None -) -> ModuleType | None: - """ - Offer to install a missing module using pip. - """ - try: - return importlib.import_module(module_name) - except ModuleNotFoundError: - return offer_install(module_name, install_name) def generate_pastel_palette(num_colors: int) -> list[str]: diff --git a/src/faebryk/libs/installation.py b/src/faebryk/libs/installation.py new file mode 100644 index 00000000..c3f1207e --- /dev/null +++ b/src/faebryk/libs/installation.py @@ -0,0 +1,46 @@ +import importlib +import subprocess +import sys +from types import ModuleType + +from rich.prompt import Confirm + + +class InstallationError(Exception): + """Raised when there's a problem installing a module.""" + + +def offer_install(module_name, install_name=None, ex=None) -> ModuleType | None: + """ + Offer to install a missing module using pip. + """ + cmd = [sys.executable, "-m", "pip", "install", install_name or module_name] + + print(f"The module '{module_name}' is not installed.") + + if Confirm.ask( + f"Do you want to run the install command [cyan mono]`{' '.join(cmd)}`[/]" + ): + try: + # Attempt to install the module using pip + subprocess.check_call(cmd) + + except subprocess.CalledProcessError: + print(f"Failed to install {module_name}. Please install it manually.") + raise ex or InstallationError(f"Failed to install {module_name}") + + print(f"Successfully installed {module_name}") + return importlib.import_module(module_name) + + +def offer_missing_install( + module_name: str, install_name: str = None +) -> ModuleType | None: + """ + Offer to install a missing module using pip. + """ + try: + return importlib.import_module(module_name) + except ModuleNotFoundError: + return offer_install(module_name, install_name) +