diff --git a/examples/minimal_led_orderable.py b/examples/minimal_led_orderable.py index da5777a2..b440a909 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 +from faebryk.libs.app.parameters import replace_tbd_with_any, 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 @@ -130,6 +130,7 @@ def main(): logger.info("Building app") app = App() G = app.get_graph() + resolve_dynamic_parameters(G) # picking ---------------------------------------------------------------- replace_tbd_with_any(app, recursive=True) diff --git a/scripts/find_duplicate_test_files.sh b/scripts/find_duplicate_test_files.sh new file mode 100644 index 00000000..50089ba9 --- /dev/null +++ b/scripts/find_duplicate_test_files.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +parent_dir=$(dirname "$0")/.. +test_dir="$parent_dir/test" + +# find all files in test_dir ending in .py +# make sure the filenames are unique +find "$test_dir" -type f -name "*.py" -exec basename {} \; | sort | uniq -d diff --git a/src/faebryk/core/cpp/CMakeLists.txt b/src/faebryk/core/cpp/CMakeLists.txt index 253fd8da..d79696f3 100644 --- a/src/faebryk/core/cpp/CMakeLists.txt +++ b/src/faebryk/core/cpp/CMakeLists.txt @@ -56,6 +56,11 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") if(${EDITABLE} AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always") endif() + +if(GLOBAL_PRINTF_DEBUG) + add_definitions(-DGLOBAL_PRINTF_DEBUG=1) +endif() + # source files --------------------------------------------------------- include_directories(${CMAKE_SOURCE_DIR}/include) file(GLOB_RECURSE SOURCE_FILES diff --git a/src/faebryk/core/cpp/__init__.py b/src/faebryk/core/cpp/__init__.py index 085cdb7d..58c80c2f 100644 --- a/src/faebryk/core/cpp/__init__.py +++ b/src/faebryk/core/cpp/__init__.py @@ -12,6 +12,7 @@ LEAK_WARNINGS = ConfigFlag("CPP_LEAK_WARNINGS", default=False) DEBUG_BUILD = ConfigFlag("CPP_DEBUG_BUILD", default=False) +PRINTF_DEBUG = ConfigFlag("CPP_PRINTF_DEBUG", default=False) # Check if installed as editable @@ -66,6 +67,7 @@ def compile_and_load(): if DEBUG_BUILD: other_flags += ["-DCMAKE_BUILD_TYPE=Debug"] + other_flags += [f"-DGLOBAL_PRINTF_DEBUG={int(bool(PRINTF_DEBUG))}"] with global_lock(_build_dir / "lock", timeout_s=60): run_live( @@ -115,6 +117,7 @@ def compile_and_load(): is_pyi=True, ) ) + run_live(["ruff", "check", "--fix", pyi_out], logger=logger) # Re-export c++ with type hints provided by __init__.pyi diff --git a/src/faebryk/core/cpp/__init__.pyi b/src/faebryk/core/cpp/__init__.pyi index a4ec03f4..c7224a32 100644 --- a/src/faebryk/core/cpp/__init__.pyi +++ b/src/faebryk/core/cpp/__init__.pyi @@ -8,9 +8,39 @@ import enum from collections.abc import Callable, Sequence, Set from typing import overload +class Counter: + @property + def in_cnt(self) -> int: ... + @property + def weak_in_cnt(self) -> int: ... + @property + def out_weaker(self) -> int: ... + @property + def out_stronger(self) -> int: ... + @property + def out_cnt(self) -> int: ... + @property + def time_spent_s(self) -> float: ... + @property + def hide(self) -> bool: ... + @property + def name(self) -> str: ... + @property + def multi(self) -> bool: ... + @property + def total_counter(self) -> bool: ... + +class Edge: + def __repr__(self) -> str: ... + @property + def to(self) -> GraphInterface: ... + class Graph: def __init__(self) -> None: ... def get_edges(self, arg: GraphInterface, /) -> dict[GraphInterface, Link]: ... + @property + def edges(self) -> list[tuple[GraphInterface, GraphInterface, Link]]: ... + def get_gifs(self) -> set[GraphInterface]: ... def invalidate(self) -> None: ... @property def node_count(self) -> int: ... @@ -50,6 +80,8 @@ class GraphInterface: def connect(self, others: Sequence[GraphInterface]) -> None: ... @overload def connect(self, other: GraphInterface, link: Link) -> None: ... + @overload + def connect(self, others: Sequence[GraphInterface], link: Link) -> None: ... class GraphInterfaceHierarchical(GraphInterface): def __init__(self, is_parent: bool) -> None: ... @@ -77,7 +109,8 @@ class GraphInterfaceSelf(GraphInterface): def __init__(self) -> None: ... class Link: - pass + def __eq__(self, arg: Link, /) -> bool: ... + def is_cloneable(self) -> bool: ... class LinkDirect(Link): def __init__(self) -> None: ... @@ -85,10 +118,8 @@ class LinkDirect(Link): class LinkDirectConditional(LinkDirect): def __init__( self, - arg: Callable[ - [GraphInterface, GraphInterface], LinkDirectConditionalFilterResult - ], - /, + filter: Callable[[Path], LinkDirectConditionalFilterResult], + needs_only_first_in_path: bool = False, ) -> None: ... class LinkDirectConditionalFilterResult(enum.Enum): @@ -98,6 +129,12 @@ class LinkDirectConditionalFilterResult(enum.Enum): FILTER_FAIL_UNRECOVERABLE = 2 +class LinkDirectDerived(LinkDirectConditional): + def __init__(self, arg: Path, /) -> None: ... + +class LinkExists(Exception): + pass + class LinkFilteredException(Exception): pass @@ -145,9 +182,22 @@ class NodeException(Exception): class NodeNoParent(Exception): pass +class Path: + def __repr__(self) -> str: ... + def __len__(self) -> int: ... + def contains(self, arg: GraphInterface, /) -> bool: ... + def last(self) -> GraphInterface: ... + def first(self) -> GraphInterface: ... + def get_link(self, arg: Edge, /) -> Link: ... + def iterate_edges(self, arg: Callable[[Edge], bool], /) -> None: ... + def __getitem__(self, arg: int, /) -> GraphInterface: ... + def add(i: int, j: int = 1) -> int: """A function that adds two numbers""" def call_python_function(func: Callable[[], int]) -> int: ... +def find_paths(src: Node, dst: Sequence[Node]) -> tuple[list[Path], list[Counter]]: ... def print_obj(obj: object) -> None: ... +def set_indiv_measure(value: bool) -> None: ... def set_leak_warnings(value: bool) -> None: ... +def set_max_paths(arg0: int, arg1: int, arg2: int, /) -> None: ... diff --git a/src/faebryk/core/cpp/include/graph/graph.hpp b/src/faebryk/core/cpp/include/graph/graph.hpp index 328727dd..a3dfe595 100644 --- a/src/faebryk/core/cpp/include/graph/graph.hpp +++ b/src/faebryk/core/cpp/include/graph/graph.hpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -35,11 +36,22 @@ using Node_ref = std::shared_ptr; using Graph_ref = std::shared_ptr; using GI_refs_weak = std::vector; using HierarchicalNodeRef = std::pair; +using Link_weak_ref = Link *; -class Node { +class LinkExists : public std::runtime_error { private: - std::optional py_handle{}; + Link_ref existing_link; + Link_ref new_link; + std::string make_msg(Link_ref existing_link, Link_ref new_link, + const std::string &msg); + + public: + LinkExists(Link_ref existing_link, Link_ref new_link, const std::string &msg); + Link_ref get_existing_link(); + Link_ref get_new_link(); +}; +class Node { public: struct NodeException : public std::runtime_error { NodeException(Node &node, const std::string &msg) @@ -53,8 +65,27 @@ class Node { } }; + class Type { + private: + nb::handle type; + bool hack_cache_is_moduleinterface; + + public: + Type(nb::handle type); + bool operator==(const Type &other) const; + std::string get_name(); + // TODO these are weird + bool is_moduleinterface(); + static nb::type_object get_moduleinterface_type(); + }; + + private: + std::optional py_handle{}; + std::optional type{}; + private: std::shared_ptr self; + std::shared_ptr children; std::shared_ptr parent; @@ -80,6 +111,7 @@ class Node { std::string get_full_name(bool types = false); std::string repr(); + Type get_type(); std::string get_type_name(); // TODO replace with constructor void set_py_handle(nb::object handle); @@ -119,6 +151,7 @@ class GraphInterface { void connect(GI_ref_weak other); void connect(GI_refs_weak others); void connect(GI_ref_weak other, Link_ref link); + void connect(GI_refs_weak others, Link_ref link); // TODO replace with set_node(Node_ref node, std::string name) void set_node(Node_ref node); Node_ref get_node(); @@ -128,6 +161,9 @@ class GraphInterface { std::string repr(); // force vtable, for typename virtual void do_stuff() {}; + + /** Index in Graph::v */ + size_t v_i = 0; }; class Link { @@ -138,14 +174,52 @@ class Link { protected: Link(); Link(GI_ref_weak from, GI_ref_weak to); + Link(const Link &other); public: std::pair get_connections(); virtual void set_connections(GI_ref_weak from, GI_ref_weak to); bool is_setup(); + virtual Link_ref clone() const = 0; + virtual bool is_cloneable() const = 0; + virtual bool operator==(const Link &other) const; + virtual std::string str() const; +}; + +struct Edge { + /*const*/ GI_ref_weak from; + /*const*/ GI_ref_weak to; + + std::string str() const; }; -using Path = std::vector; +using TriEdge = std::tuple; + +class Path { + public: + Path(/*const*/ GI_ref_weak path_head); + Path(std::vector path); + Path(const Path &other); + Path(Path &&other); + ~Path(); + + std::vector path; + + /*const*/ Link_weak_ref get_link(Edge edge) /*const*/; + std::optional last_edge() /*const*/; + std::optional last_tri_edge() /*const*/; + /*const*/ GI_ref_weak last() /*const*/; + /*const*/ GI_ref_weak first() /*const*/; + /*const*/ GI_ref_weak operator[](int idx) /*const*/; + size_t size() /*const*/; + bool contains(/*const*/ GI_ref_weak gif) /*const*/; + void iterate_edges(std::function visitor) /*const*/; + /*const*/ std::vector &get_path() /*const*/; + size_t index(/*const*/ GI_ref_weak gif) /*const*/; + + std::string str() const; +}; class Graph { Set v; @@ -176,6 +250,9 @@ class Graph { std::string repr(); + Set get_gifs(); + std::vector> all_edges(); + // Algorithms std::unordered_set node_projection(); std::vector> diff --git a/src/faebryk/core/cpp/include/graph/links.hpp b/src/faebryk/core/cpp/include/graph/links.hpp index d1074198..ca797530 100644 --- a/src/faebryk/core/cpp/include/graph/links.hpp +++ b/src/faebryk/core/cpp/include/graph/links.hpp @@ -11,6 +11,9 @@ class LinkDirect : public Link { public: LinkDirect(); LinkDirect(GI_ref_weak from, GI_ref_weak to); + LinkDirect(const LinkDirect &other); + Link_ref clone() const override; + bool is_cloneable() const override; }; class LinkParent : public Link { @@ -20,10 +23,12 @@ class LinkParent : public Link { public: LinkParent(); LinkParent(GraphInterfaceHierarchical *from, GraphInterfaceHierarchical *to); - + LinkParent(const LinkParent &other); void set_connections(GI_ref_weak from, GI_ref_weak to) override; GraphInterfaceHierarchical *get_parent(); GraphInterfaceHierarchical *get_child(); + Link_ref clone() const override; + bool is_cloneable() const override; }; class LinkNamedParent : public LinkParent { @@ -33,12 +38,11 @@ class LinkNamedParent : public LinkParent { LinkNamedParent(std::string name); LinkNamedParent(std::string name, GraphInterfaceHierarchical *from, GraphInterfaceHierarchical *to); - + LinkNamedParent(const LinkNamedParent &other); std::string get_name(); -}; - -class LinkDirectShallow : public LinkDirect { - // TODO + Link_ref clone() const override; + bool operator==(const Link &other) const override; + bool is_cloneable() const override; }; class LinkPointer : public Link { @@ -48,18 +52,25 @@ class LinkPointer : public Link { public: LinkPointer(); LinkPointer(GI_ref_weak from, GraphInterfaceSelf *to); + LinkPointer(const LinkPointer &other); void set_connections(GI_ref_weak from, GI_ref_weak to) override; GraphInterface *get_pointer(); GraphInterfaceSelf *get_pointee(); + Link_ref clone() const override; + bool is_cloneable() const override; }; class LinkSibling : public LinkPointer { public: LinkSibling(); LinkSibling(GI_ref_weak from, GraphInterfaceSelf *to); + LinkSibling(const LinkSibling &other); + Link_ref clone() const override; + bool is_cloneable() const override; }; class LinkDirectConditional : public LinkDirect { + friend class LinkDirectDerived; public: enum FilterResult { @@ -68,7 +79,7 @@ class LinkDirectConditional : public LinkDirect { FILTER_FAIL_UNRECOVERABLE }; - using FilterF = std::function; + using FilterF = std::function; struct LinkFilteredException : public std::runtime_error { LinkFilteredException(std::string msg) @@ -78,18 +89,47 @@ class LinkDirectConditional : public LinkDirect { private: FilterF filter; + bool needs_only_first_in_path; public: - LinkDirectConditional(FilterF filter); - LinkDirectConditional(FilterF filter, GI_ref_weak from, GI_ref_weak to); + LinkDirectConditional(FilterF filter, bool needs_only_first_in_path); + LinkDirectConditional(FilterF filter, bool needs_only_first_in_path, + GI_ref_weak from, GI_ref_weak to); + LinkDirectConditional(const LinkDirectConditional &other); void set_connections(GI_ref_weak from, GI_ref_weak to) override; + FilterResult run_filter(Path path); + + bool needs_to_check_only_first_in_path(); + Link_ref clone() const override; + bool operator==(const Link &other) const override; + bool is_cloneable() const override; +}; + +class LinkDirectShallow : public LinkDirectConditional { + // TODO + public: + LinkDirectShallow(); + LinkDirectShallow(const LinkDirectShallow &other); + Link_ref clone() const override; + bool operator==(const Link &other) const override; + bool is_cloneable() const override; }; class LinkDirectDerived : public LinkDirectConditional { private: - static LinkDirectConditional::FilterF make_filter_from_path(Path path); + static std::pair + make_filter_from_path(Path path); + + Path path; public: LinkDirectDerived(Path path); + LinkDirectDerived(Path path, std::pair filter); LinkDirectDerived(Path path, GI_ref_weak from, GI_ref_weak to); + LinkDirectDerived(Path path, std::pair filter, GI_ref_weak from, + GI_ref_weak to); + LinkDirectDerived(const LinkDirectDerived &other); + Link_ref clone() const override; + bool operator==(const Link &other) const override; + bool is_cloneable() const override; }; diff --git a/src/faebryk/core/cpp/include/pathfinder/bfs.hpp b/src/faebryk/core/cpp/include/pathfinder/bfs.hpp new file mode 100644 index 00000000..57270aba --- /dev/null +++ b/src/faebryk/core/cpp/include/pathfinder/bfs.hpp @@ -0,0 +1,61 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "graph/graph.hpp" +#include "graph/graphinterfaces.hpp" +#include +#include +#include +#include + +using GI_parent_ref_weak = GraphInterfaceHierarchical *; + +struct PathStackElement { + Node::Type parent_type; + Node::Type child_type; + /*const*/ GI_parent_ref_weak parent_gif; + std::string name; + bool up; + + std::string str() /*const*/; +}; + +struct UnresolvedStackElement { + PathStackElement elem; + bool split; + + bool match(PathStackElement &other); + std::string str() /*const*/; +}; + +using PathStack = std::vector; +using UnresolvedStack = std::vector; + +struct PathData { + UnresolvedStack unresolved_stack; + PathStack split_stack; +}; + +class BFSPath : public Path { + std::shared_ptr path_data; + + public: + double confidence = 1.0; + bool filtered = false; + bool stop = false; + + BFSPath(/*const*/ GI_ref_weak path_head); + BFSPath(const BFSPath &other); + BFSPath(const BFSPath &other, /*const*/ GI_ref_weak new_head); + BFSPath(BFSPath &&other); + BFSPath operator+(/*const*/ GI_ref_weak gif); + + PathData &get_path_data_mut(); + PathData &get_path_data() /*const*/; + bool strong() /*const*/; +}; + +void bfs_visit(/*const*/ GI_ref_weak root, std::function visitor); diff --git a/src/faebryk/core/cpp/include/pathfinder/pathcounter.hpp b/src/faebryk/core/cpp/include/pathfinder/pathcounter.hpp new file mode 100644 index 00000000..72d8c864 --- /dev/null +++ b/src/faebryk/core/cpp/include/pathfinder/pathcounter.hpp @@ -0,0 +1,37 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +inline bool INDIV_MEASURE = true; + +inline void set_indiv_measure(bool v) { + INDIV_MEASURE = v; +} + +class PathFinder; +class BFSPath; + +struct Counter { + size_t in_cnt = 0; + size_t weak_in_cnt = 0; + size_t out_weaker = 0; + size_t out_stronger = 0; + size_t out_cnt = 0; + double time_spent_s = 0; + + bool hide = false; + const char *name = ""; + bool multi = false; + bool total_counter = false; + + bool exec(PathFinder *pf, bool (PathFinder::*filter)(BFSPath &), BFSPath &p); + std::vector + exec_multi(PathFinder *pf, + std::vector (PathFinder::*filter)(std::vector &), + std::vector &p); +}; \ No newline at end of file diff --git a/src/faebryk/core/cpp/include/pathfinder/pathfinder.hpp b/src/faebryk/core/cpp/include/pathfinder/pathfinder.hpp new file mode 100644 index 00000000..df8e3ec6 --- /dev/null +++ b/src/faebryk/core/cpp/include/pathfinder/pathfinder.hpp @@ -0,0 +1,65 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include "graph/graph.hpp" +#include "pathfinder/bfs.hpp" +#include "pathfinder/pathcounter.hpp" +#include "perf.hpp" +#include +#include +#include +#include +#include +#include + +struct PathLimits { + uint32_t absolute = 1 << 31; + uint32_t no_new_weak = 1 << 31; + uint32_t no_weak = 1 << 31; +}; + +inline PathLimits PATH_LIMITS; + +inline void set_max_paths(uint32_t absolute, uint32_t no_new_weak, uint32_t no_weak) { + PATH_LIMITS.absolute = absolute; + PATH_LIMITS.no_new_weak = no_new_weak; + PATH_LIMITS.no_weak = no_weak; +} + +class PathFinder; + +struct Filter { + bool (PathFinder::*filter)(BFSPath &); + bool discovery = false; + Counter counter; + + bool exec(PathFinder *pf, BFSPath &p); +}; + +class PathFinder { + std::vector multi_paths; + size_t path_cnt = 0; + + bool _count(BFSPath &p); + bool _filter_path_by_node_type(BFSPath &p); + bool _filter_path_gif_type(BFSPath &p); + bool _filter_path_by_dead_end_split(BFSPath &p); + bool _build_path_stack(BFSPath &p); + bool _filter_path_by_end_in_self_gif(BFSPath &p); + bool _filter_path_same_end_type(BFSPath &p); + bool _filter_path_by_stack(BFSPath &p); + bool _filter_shallow(BFSPath &p); + bool _filter_conditional_link(BFSPath &p); + std::vector _filter_paths_by_split_join(std::vector &paths); + + public: + PathFinder(); + + std::vector filters; + bool run_filters(BFSPath &p); + std::pair, std::vector> + find_paths(Node_ref src, std::vector dst); +}; diff --git a/src/faebryk/core/cpp/include/perf.hpp b/src/faebryk/core/cpp/include/perf.hpp new file mode 100644 index 00000000..c7205ffb --- /dev/null +++ b/src/faebryk/core/cpp/include/perf.hpp @@ -0,0 +1,32 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#pragma once + +#include +#include + +class PerfCounter { + std::chrono::high_resolution_clock::time_point start; + + public: + PerfCounter(); + int64_t ns(); + double ms(); + double s(); +}; + +class PerfCounterAccumulating { + std::chrono::high_resolution_clock::time_point start; + int64_t time_ns = 0; + bool paused = false; + + public: + PerfCounterAccumulating(); + void pause(); + void resume(); + int64_t ns(); + double ms(); + double s(); +}; diff --git a/src/faebryk/core/cpp/include/pyutil.hpp b/src/faebryk/core/cpp/include/pyutil.hpp index ac9c39a0..c7299bcd 100644 --- a/src/faebryk/core/cpp/include/pyutil.hpp +++ b/src/faebryk/core/cpp/include/pyutil.hpp @@ -27,4 +27,48 @@ inline bool isinstance(nb::object obj, std::vector types) { }); } +inline bool issubclass(nb::handle obj, nb::type_object type) { + nb::object issubclass_func = nb::module_::import_("builtins").attr("issubclass"); + return nb::cast(issubclass_func(obj, type)); +} + +inline std::string get_name(nb::handle type) { + auto out = std::string(nb::repr(type.attr("__name__")).c_str()); + // extract ClassName + // remove quotes + auto pos = out.find_first_of('\''); + if (pos != std::string::npos) { + out = out.substr(pos + 1, out.size() - 2); + } + return out; +} + +template inline std::string get_typename(T *obj) { + auto instance = nb::find(obj); + if (!instance.is_valid()) { + return std::string("unknown type"); + } + auto type = instance.type(); + return get_name(type); +} + +/** + * @brief Only works if T is the actual type (not a base class) + * + * @tparam T + * @param obj + * @return true + * @return false + */ +template inline bool is_cpp_type(T *obj) { + auto instance = nb::find(obj); + if (!instance.is_valid()) { + return true; + } + auto pytype = instance.type(); + assert(nb::type_check(pytype)); + auto cpy_type = nb::type(); + return cpy_type.is(pytype); +} + } // namespace pyutil diff --git a/src/faebryk/core/cpp/include/util.hpp b/src/faebryk/core/cpp/include/util.hpp index 25af413c..14e57c35 100644 --- a/src/faebryk/core/cpp/include/util.hpp +++ b/src/faebryk/core/cpp/include/util.hpp @@ -6,19 +6,51 @@ #include #include +#include #include +#if GLOBAL_PRINTF_DEBUG +#else +#define printf(...) +#endif + namespace util { -template std::string get_type_name(const T *obj) { +template inline std::string get_type_name(const T *obj) { int status; std::unique_ptr demangled_name( abi::__cxa_demangle(typeid(*obj).name(), nullptr, nullptr, &status), std::free); return demangled_name ? demangled_name.get() : "unknown type"; } -template std::string get_type_name(const std::shared_ptr &obj) { +template inline std::string get_type_name(const std::shared_ptr &obj) { return get_type_name(obj.get()); } +inline std::string formatted_ptr(void *ptr) { + std::stringstream ss; + ss << std::hex << std::uppercase << reinterpret_cast(ptr); + auto out = ss.str(); + return "*" + out.substr(out.size() - 4); +} + +// TODO not really used +// template inline std::string str_vec(const std::vector &vec) { +// std::stringstream ss; +// ss << "["; +// for (size_t i = 0; i < vec.size(); ++i) { +// // if T is string just put it into stream directly +// if constexpr (std::is_same_v) { +// ss << '"' << vec[i] << '"'; +// } else { +// ss << vec[i].str(); +// } +// if (i < vec.size() - 1) { +// ss << ", "; +// } +// } +// ss << "]"; +// return ss.str(); +//} + } // namespace util diff --git a/src/faebryk/core/cpp/src/graph/graph.cpp b/src/faebryk/core/cpp/src/graph/graph.cpp index 76bcd349..0badf686 100644 --- a/src/faebryk/core/cpp/src/graph/graph.cpp +++ b/src/faebryk/core/cpp/src/graph/graph.cpp @@ -28,9 +28,11 @@ Graph_ref Graph::merge_graphs(Graph_ref g1, Graph_ref g2) { auto G_source = (g1 == G_target) ? g2 : g1; assert(G_source->node_count() > 0); + size_t v_i_offset = G_target->node_count(); for (auto &v : G_source->v) { v->G = G_target; + v->v_i += v_i_offset; } G_target->merge(*G_source); G_source->invalidate(); @@ -43,11 +45,10 @@ void Graph::add_edge(Link_ref link) { auto G = Graph::merge_graphs(from->G, to->G); - // remove existing link + // existing link if (G->e_cache_simple[from].contains(to)) { - // this->remove_edge(this->e_cache[from][to]); - // TODO: reconsider this - throw std::runtime_error("link already exists"); + // handle policy in the caller + throw LinkExists(G->e_cache[from][to], link, "link already exists"); } G->e_cache_simple[from].insert(to); @@ -198,3 +199,32 @@ Graph::bfs_visit(std::function &, Link_ref)> filte return visited; } + +Set Graph::get_gifs() { + return this->v; +} + +std::vector> Graph::all_edges() { + return this->e; +} + +LinkExists::LinkExists(Link_ref existing_link, Link_ref new_link, const std::string &msg) + : std::runtime_error(LinkExists::make_msg(existing_link, new_link, msg)) + , existing_link(existing_link) + , new_link(new_link) { +} + +std::string LinkExists::make_msg(Link_ref existing_link, Link_ref new_link, + const std::string &msg) { + std::stringstream ss; + ss << msg << ": E:" << existing_link->str() << " N:" << new_link->str(); + return ss.str(); +} + +Link_ref LinkExists::get_existing_link() { + return this->existing_link; +} + +Link_ref LinkExists::get_new_link() { + return this->new_link; +} diff --git a/src/faebryk/core/cpp/src/graph/graphinterface.cpp b/src/faebryk/core/cpp/src/graph/graphinterface.cpp index 256b25fc..d64fc6cd 100644 --- a/src/faebryk/core/cpp/src/graph/graphinterface.cpp +++ b/src/faebryk/core/cpp/src/graph/graphinterface.cpp @@ -31,10 +31,12 @@ std::string GraphInterface::get_name() { } std::string GraphInterface::get_full_name(bool types) { - assert(this->node); - std::stringstream ss; - ss << this->get_node()->get_full_name(types) << "." << this->name; + if (this->node) { + ss << this->get_node()->get_full_name(types) << "." << this->name; + } else { + ss << util::formatted_ptr(this); + } if (types) { ss << "|" << util::get_type_name(this) << "|"; } @@ -42,6 +44,9 @@ std::string GraphInterface::get_full_name(bool types) { } std::string GraphInterface::repr() { + if (this->node) { + return this->get_full_name(true); + } std::stringstream ss; ss << "<" << util::get_type_name(this) << " at " << this << ">"; return ss.str(); @@ -73,6 +78,22 @@ void GraphInterface::connect(GI_ref_weak other, Link_ref link) { Graph::add_edge(link); } +void GraphInterface::connect(GI_refs_weak others, Link_ref link) { + if (others.size() == 1) { + this->connect(others[0], link); + return; + } + // check link is cloneable + if (!link->is_cloneable()) { + throw std::runtime_error(std::string("link is not cloneable: ") + + pyutil::get_typename(link.get())); + } + + for (auto other : others) { + this->connect(other, link->clone()); + } +} + void GraphInterface::register_graph(std::shared_ptr gi) { this->G->hold(gi); } diff --git a/src/faebryk/core/cpp/src/graph/link.cpp b/src/faebryk/core/cpp/src/graph/link.cpp index db84246a..bc95774b 100644 --- a/src/faebryk/core/cpp/src/graph/link.cpp +++ b/src/faebryk/core/cpp/src/graph/link.cpp @@ -4,6 +4,7 @@ #include "graph/graph.hpp" #include "graph/links.hpp" +#include "pyutil.hpp" Link::Link() : from(nullptr) @@ -17,6 +18,12 @@ Link::Link(GI_ref_weak from, GI_ref_weak to) , setup(true) { } +Link::Link(const Link &other) + : from(nullptr) + , to(nullptr) + , setup(false) { +} + std::pair Link::get_connections() { if (!this->setup) { throw std::runtime_error("link not setup"); @@ -33,3 +40,22 @@ void Link::set_connections(GI_ref_weak from, GI_ref_weak to) { bool Link::is_setup() { return this->setup; } + +std::string Link::str() const { + std::stringstream ss; + ss << util::get_type_name(this) << "("; + if (this->setup) { + ss << this->from->get_full_name(false) << " -> " + << this->to->get_full_name(false); + } + ss << ")"; + return ss.str(); +} + +bool Link::operator==(const Link &other) const { + bool same_type = typeid(*this) == typeid(other); + bool same_connections = this->from == other.from && this->to == other.to; + bool both_setup = this->setup && other.setup; + + return same_type && (!both_setup || same_connections); +} diff --git a/src/faebryk/core/cpp/src/graph/links.cpp b/src/faebryk/core/cpp/src/graph/links.cpp index 82ddb4ed..0e21e8bc 100644 --- a/src/faebryk/core/cpp/src/graph/links.cpp +++ b/src/faebryk/core/cpp/src/graph/links.cpp @@ -3,6 +3,7 @@ */ #include "graph/links.hpp" +#include "pyutil.hpp" // LinkDirect -------------------------------------------------------------------------- LinkDirect::LinkDirect() @@ -13,6 +14,18 @@ LinkDirect::LinkDirect(GI_ref_weak from, GI_ref_weak to) : Link(from, to) { } +LinkDirect::LinkDirect(const LinkDirect &other) + : Link(other) { +} + +Link_ref LinkDirect::clone() const { + return std::make_shared(*this); +} + +bool LinkDirect::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkParent -------------------------------------------------------------------------- LinkParent::LinkParent() : Link() @@ -27,6 +40,12 @@ LinkParent::LinkParent(GraphInterfaceHierarchical *from, GraphInterfaceHierarchi this->set_connections(from, to); } +LinkParent::LinkParent(const LinkParent &other) + : Link(other) + , parent(nullptr) + , child(nullptr) { +} + void LinkParent::set_connections(GI_ref_weak from, GI_ref_weak to) { auto from_h = dynamic_cast(from); auto to_h = dynamic_cast(to); @@ -61,6 +80,14 @@ GraphInterfaceHierarchical *LinkParent::get_child() { return this->child; } +Link_ref LinkParent::clone() const { + return std::make_shared(*this); +} + +bool LinkParent::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkNamedParent --------------------------------------------------------------------- LinkNamedParent::LinkNamedParent(std::string name) : LinkParent() @@ -73,10 +100,28 @@ LinkNamedParent::LinkNamedParent(std::string name, GraphInterfaceHierarchical *f , name(name) { } +LinkNamedParent::LinkNamedParent(const LinkNamedParent &other) + : LinkParent(other) + , name(other.name) { +} + std::string LinkNamedParent::get_name() { return this->name; } +Link_ref LinkNamedParent::clone() const { + return std::make_shared(*this); +} + +bool LinkNamedParent::operator==(const Link &other) const { + auto other_np = dynamic_cast(&other); + return Link::operator==(other) && this->name == other_np->name; +} + +bool LinkNamedParent::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkPointer ------------------------------------------------------------------------- LinkPointer::LinkPointer() : Link() @@ -91,6 +136,12 @@ LinkPointer::LinkPointer(GI_ref_weak from, GraphInterfaceSelf *to) this->set_connections(from, to); } +LinkPointer::LinkPointer(const LinkPointer &other) + : Link(other) + , pointee(nullptr) + , pointer(nullptr) { +} + void LinkPointer::set_connections(GI_ref_weak from, GI_ref_weak to) { auto from_s = dynamic_cast(from); auto to_s = dynamic_cast(to); @@ -122,6 +173,14 @@ GraphInterface *LinkPointer::get_pointer() { return this->pointer; } +Link_ref LinkPointer::clone() const { + return std::make_shared(*this); +} + +bool LinkPointer::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkSibling ------------------------------------------------------------------------ LinkSibling::LinkSibling() : LinkPointer() { @@ -131,38 +190,154 @@ LinkSibling::LinkSibling(GI_ref_weak from, GraphInterfaceSelf *to) : LinkPointer(from, to) { } +LinkSibling::LinkSibling(const LinkSibling &other) + : LinkPointer(other) { +} + +Link_ref LinkSibling::clone() const { + return std::make_shared(*this); +} + +bool LinkSibling::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkDirectConditional ---------------------------------------------------------------- -LinkDirectConditional::LinkDirectConditional(FilterF filter) +LinkDirectConditional::LinkDirectConditional(FilterF filter, + bool needs_only_first_in_path) : LinkDirect() - , filter(filter) { + , filter(filter) + , needs_only_first_in_path(needs_only_first_in_path) { } -LinkDirectConditional::LinkDirectConditional(FilterF filter, GI_ref_weak from, - GI_ref_weak to) +LinkDirectConditional::LinkDirectConditional(FilterF filter, + bool needs_only_first_in_path, + GI_ref_weak from, GI_ref_weak to) : LinkDirect(from, to) - , filter(filter) { + , filter(filter) + , needs_only_first_in_path(needs_only_first_in_path) { this->set_connections(from, to); } +LinkDirectConditional::LinkDirectConditional(const LinkDirectConditional &other) + : LinkDirect(other) + , filter(other.filter) + , needs_only_first_in_path(other.needs_only_first_in_path) { +} + void LinkDirectConditional::set_connections(GI_ref_weak from, GI_ref_weak to) { - if (this->filter(from, to) != FilterResult::FILTER_PASS) { + if (this->filter(Path({from, to})) != FilterResult::FILTER_PASS) { throw LinkFilteredException("LinkDirectConditional filtered"); } LinkDirect::set_connections(from, to); } +LinkDirectConditional::FilterResult LinkDirectConditional::run_filter(Path path) { + return this->filter(path); +} + +bool LinkDirectConditional::needs_to_check_only_first_in_path() { + return this->needs_only_first_in_path; +} + +Link_ref LinkDirectConditional::clone() const { + return std::make_shared(*this); +} + +bool LinkDirectConditional::operator==(const Link &other) const { + auto other_dc = dynamic_cast(&other); + // TODO + return Link::operator==(other) && false; +} + +bool LinkDirectConditional::is_cloneable() const { + return pyutil::is_cpp_type(this); +} + // LinkDirectDerived ------------------------------------------------------------------- LinkDirectDerived::LinkDirectDerived(Path path) - : LinkDirectConditional(make_filter_from_path(path)) { + : LinkDirectDerived(path, make_filter_from_path(path)) { } LinkDirectDerived::LinkDirectDerived(Path path, GI_ref_weak from, GI_ref_weak to) - : LinkDirectConditional(make_filter_from_path(path), from, to) { + : LinkDirectDerived(path, make_filter_from_path(path), from, to) { } -LinkDirectConditional::FilterF LinkDirectDerived::make_filter_from_path(Path path) { - // TODO - return [path](GI_ref_weak, GI_ref_weak) { - return LinkDirectConditional::FilterResult::FILTER_PASS; +LinkDirectDerived::LinkDirectDerived(Path path, std::pair filter) + : LinkDirectConditional(filter.first, filter.second) + , path(path) { +} + +LinkDirectDerived::LinkDirectDerived(Path path, std::pair filter, + GI_ref_weak from, GI_ref_weak to) + : LinkDirectConditional(filter.first, filter.second, from, to) + , path(path) { +} + +LinkDirectDerived::LinkDirectDerived(const LinkDirectDerived &other) + : LinkDirectConditional(other) + , path(other.path) { +} + +Link_ref LinkDirectDerived::clone() const { + return std::make_shared(*this); +} + +std::pair +LinkDirectDerived::make_filter_from_path(Path path) { + std::vector derived_filters; + bool needs_only_first_in_path = true; + + // why make implicit path from self ref path + assert(path.size() > 1); + + path.iterate_edges([&](Edge &edge) { + if (auto link_conditional = + dynamic_cast(path.get_link(edge))) { + derived_filters.push_back(link_conditional->filter); + needs_only_first_in_path &= + link_conditional->needs_to_check_only_first_in_path(); + } + return true; + }); + + auto filterf = [path, derived_filters](Path check_path) { + bool ok = true; + bool recoverable = true; + for (auto &filter : derived_filters) { + auto res = filter(check_path); + ok &= res == LinkDirectConditional::FilterResult::FILTER_PASS; + recoverable &= + res != LinkDirectConditional::FilterResult::FILTER_FAIL_UNRECOVERABLE; + } + return ok ? LinkDirectConditional::FilterResult::FILTER_PASS + : (recoverable + ? LinkDirectConditional::FilterResult::FILTER_FAIL_RECOVERABLE + : LinkDirectConditional::FilterResult:: + FILTER_FAIL_UNRECOVERABLE); }; + + return {filterf, needs_only_first_in_path}; +} + +bool LinkDirectDerived::operator==(const Link &other) const { + auto other_dd = dynamic_cast(&other); + return Link::operator==(other) && this->path.path == other_dd->path.path; +} + +bool LinkDirectDerived::is_cloneable() const { + return pyutil::is_cpp_type(this); } + +// LinkDirectShallow ------------------------------------------------------------------- +// LinkDirectShallow::LinkDirectShallow() +// : LinkDirectConditional() { +//} +// +// LinkDirectShallow::LinkDirectShallow(const LinkDirectShallow &other) +// : LinkDirectConditional(other) { +//} +// +// Link_ref LinkDirectShallow::clone() const { +// return std::make_shared(*this); +//} diff --git a/src/faebryk/core/cpp/src/graph/node.cpp b/src/faebryk/core/cpp/src/graph/node.cpp index 61bb9628..b7740613 100644 --- a/src/faebryk/core/cpp/src/graph/node.cpp +++ b/src/faebryk/core/cpp/src/graph/node.cpp @@ -39,6 +39,7 @@ void Node::set_py_handle(nb::object handle) { } assert(handle.is_valid()); this->py_handle = handle; + this->type = Type(handle.type()); } std::shared_ptr Node::get_graph() { @@ -70,10 +71,7 @@ HierarchicalNodeRef Node::get_parent_force() { } std::string Node::get_root_id() { - std::stringstream ss; - ss << std::hex << std::uppercase << reinterpret_cast(this); - auto out = ss.str(); - return "*" + out.substr(out.size() - 4); + return util::formatted_ptr(this); } std::string Node::get_name(bool accept_no_parent) { @@ -122,16 +120,7 @@ std::string Node::repr() { std::string Node::get_type_name() { if (this->py_handle.has_value()) { - auto out = std::string( - nb::repr(this->py_handle.value().type().attr("__name__")).c_str()); - // format : 'ClassName' - // extract ClassName - // remove quotes - auto pos = out.find_first_of('\''); - if (pos != std::string::npos) { - out = out.substr(pos + 1, out.size() - 2); - } - return out; + return this->get_type().get_name(); } return util::get_type_name(this); } @@ -224,3 +213,35 @@ Node::get_children(bool direct_only, std::optional> return children_filtered; } + +Node::Type Node::get_type() { + if (!this->type) { + throw std::runtime_error("Node has no py_handle"); + } + return *this->type; +} + +Node::Type::Type(nb::handle type) + : type(type) { + // TODO can be done in a nicer way + this->hack_cache_is_moduleinterface = + pyutil::issubclass(this->type, this->get_moduleinterface_type()); +} + +bool Node::Type::operator==(const Type &other) const { + // TODO not sure this is ok + return this->type.ptr() == other.type.ptr(); +} + +std::string Node::Type::get_name() { + return pyutil::get_name(this->type); +} + +bool Node::Type::is_moduleinterface() { + return this->hack_cache_is_moduleinterface; +} + +nb::type_object Node::Type::get_moduleinterface_type() { + // TODO can be done in a nicer way + return nb::module_::import_("faebryk.core.moduleinterface").attr("ModuleInterface"); +} diff --git a/src/faebryk/core/cpp/src/graph/path.cpp b/src/faebryk/core/cpp/src/graph/path.cpp new file mode 100644 index 00000000..591526f0 --- /dev/null +++ b/src/faebryk/core/cpp/src/graph/path.cpp @@ -0,0 +1,101 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#include "graph/graph.hpp" +#include +#include + +Path::Path(/*const*/ GI_ref_weak path_head) + : path(std::vector{path_head}) { +} + +Path::Path(std::vector path) + : path(path) { +} + +Path::Path(const Path &other) + : path(other.path) { +} + +Path::Path(Path &&other) + : path(std::move(other.path)) { +} + +Path::~Path() { +} + +/*const*/ Link_weak_ref Path::get_link(Edge edge) /*const*/ { + auto out = edge.from->is_connected(edge.to); + assert(out); + return out->get(); +} + +std::optional Path::last_edge() /*const*/ { + if (path.size() < 2) { + return {}; + } + return Edge{path[path.size() - 2], path.back()}; +} + +std::optional Path::last_tri_edge() /*const*/ { + if (path.size() < 3) { + return {}; + } + return std::make_tuple(path[path.size() - 3], path[path.size() - 2], path.back()); +} + +/*const*/ GI_ref_weak Path::last() /*const*/ { + return path.back(); +} + +/*const*/ GI_ref_weak Path::first() /*const*/ { + return path.front(); +} + +/*const*/ GI_ref_weak Path::operator[](int idx) /*const*/ { + if (idx < 0) { + idx = path.size() + idx; + } + if (idx >= path.size()) { + throw std::out_of_range("Path index out of range"); + } + return path[idx]; +} + +size_t Path::size() /*const*/ { + return path.size(); +} + +bool Path::contains(/*const*/ GI_ref_weak gif) /*const*/ { + return std::find(path.begin(), path.end(), gif) != path.end(); +} + +void Path::iterate_edges(std::function visitor) /*const*/ { + for (size_t i = 1; i < path.size(); i++) { + Edge edge{path[i - 1], path[i]}; + bool res = visitor(edge); + if (!res) { + return; + } + } +} + +/*const*/ std::vector &Path::get_path() /*const*/ { + return path; +} + +size_t Path::index(/*const*/ GI_ref_weak gif) /*const*/ { + return std::distance(path.begin(), std::find(path.begin(), path.end(), gif)); +} + +std::string Path::str() const { + std::stringstream ss; + ss << "Path(" << path.size() << ")"; + ss << "["; + for (auto &gif : path) { + ss << "\n " << gif->get_full_name(false); + } + ss << "]"; + return ss.str(); +} diff --git a/src/faebryk/core/cpp/src/main.cpp b/src/faebryk/core/cpp/src/main.cpp index 4ed85c84..04959dad 100644 --- a/src/faebryk/core/cpp/src/main.cpp +++ b/src/faebryk/core/cpp/src/main.cpp @@ -6,6 +6,7 @@ #include "graph/graphinterfaces.hpp" #include "graph/links.hpp" #include "nano.hpp" +#include "pathfinder/pathfinder.hpp" #include // check if c++20 is used @@ -46,6 +47,17 @@ void print_obj_pyptr(PyObject *pyobj) { print_obj(obj); } +std::pair, std::vector> +find_paths(Node_ref src, std::vector dst) { + PerfCounter pc; + + PathFinder pf; + auto res = pf.find_paths(src, dst); + + printf("TIME: %3.2lf ms C++ find paths\n", pc.ms()); + return res; +} + PYMOD(m) { m.doc() = "faebryk core c++ module"; @@ -54,6 +66,11 @@ PYMOD(m) { m.def("set_leak_warnings", &nb::set_leak_warnings, "value"_a); m.def("print_obj", &print_obj, "obj"_a); + // TODO why this rv_pol needed + m.def("find_paths", &find_paths, "src"_a, "dst"_a, nb::rv_policy::reference); + m.def("set_indiv_measure", &set_indiv_measure, "value"_a); + + m.def("set_max_paths", &set_max_paths); // Graph using GI = GraphInterface; @@ -71,12 +88,16 @@ PYMOD(m) { .def("connect", nb::overload_cast(&GI::connect), "other"_a) .def("connect", nb::overload_cast(&GI::connect), "others"_a) .def("connect", nb::overload_cast(&GI::connect), - "other"_a, "link"_a), + "other"_a, "link"_a) + .def("connect", nb::overload_cast(&GI::connect), + "others"_a, "link"_a), &GraphInterface::factory); nb::class_(m, "Graph") .def(nb::init<>()) .def("get_edges", &Graph::get_edges, nb::rv_policy::reference) + .def_prop_ro("edges", &Graph::all_edges, nb::rv_policy::reference) + .def("get_gifs", &Graph::get_gifs, nb::rv_policy::reference) .def("invalidate", &Graph::invalidate) .def_prop_ro("node_count", &Graph::node_count) .def_prop_ro("edge_count", &Graph::edge_count) @@ -86,6 +107,12 @@ PYMOD(m) { nb::rv_policy::reference) .def("__repr__", &Graph::repr); + nb::exception(m, "LinkExists"); + // nb::class_(m, "LinkExists") + // .def("existing_link", &LinkExists::get_existing_link, + // nb::rv_policy::reference) .def("new_link", &LinkExists::get_new_link, + // nb::rv_policy::reference); + // Graph interfaces FACTORY((nb::class_(m, "GraphInterfaceSelf")), &GraphInterfaceSelf::factory); @@ -117,7 +144,9 @@ PYMOD(m) { &GraphInterfaceModuleConnection::factory); // Links - nb::class_(m, "Link"); + nb::class_(m, "Link") + .def("__eq__", &Link::operator==) + .def("is_cloneable", &Link::is_cloneable); nb::class_(m, "LinkParent").def(nb::init<>()); nb::class_(m, "LinkNamedParent") .def(nb::init()); @@ -125,7 +154,10 @@ PYMOD(m) { nb::class_(m, "LinkPointer").def(nb::init<>()); nb::class_(m, "LinkSibling").def(nb::init<>()); nb::class_(m, "LinkDirectConditional") - .def(nb::init()); + .def(nb::init(), "filter"_a, + "needs_only_first_in_path"_a = false); + nb::class_(m, "LinkDirectDerived") + .def(nb::init()); nb::exception(m, "LinkFilteredException"); @@ -157,4 +189,37 @@ PYMOD(m) { nb::exception(m, "NodeException"); nb::exception(m, "NodeNoParent"); + + // Pathfinder + nb::class_(m, "Counter") + .def_ro("in_cnt", &Counter::in_cnt) + .def_ro("weak_in_cnt", &Counter::weak_in_cnt) + .def_ro("out_weaker", &Counter::out_weaker) + .def_ro("out_stronger", &Counter::out_stronger) + .def_ro("out_cnt", &Counter::out_cnt) + .def_ro("time_spent_s", &Counter::time_spent_s) + .def_ro("hide", &Counter::hide) + .def_ro("name", &Counter::name) + .def_ro("multi", &Counter::multi) + .def_ro("total_counter", &Counter::total_counter); + + // Path + nb::class_(m, "Edge") + .def("__repr__", &Edge::str) + .def_ro("from_", &Edge::from, nb::rv_policy::reference) + .def_ro("to", &Edge::to, nb::rv_policy::reference); + + // nb::class_(m, "TriEdge"); + + nb::class_(m, "Path") + .def("__repr__", &Path::str) + .def("__len__", &Path::size) + .def("contains", &Path::contains) + .def("last", &Path::last, nb::rv_policy::reference) + .def("first", &Path::first, nb::rv_policy::reference) + //.def("last_edge", &Path::last_edge) + //.def("last_tri_edge", &Path::last_tri_edge) + .def("get_link", &Path::get_link) + .def("iterate_edges", &Path::iterate_edges) + .def("__getitem__", &Path::operator[], nb::rv_policy::reference); } diff --git a/src/faebryk/core/cpp/src/pathfinder/bfs.cpp b/src/faebryk/core/cpp/src/pathfinder/bfs.cpp new file mode 100644 index 00000000..fcb0a71c --- /dev/null +++ b/src/faebryk/core/cpp/src/pathfinder/bfs.cpp @@ -0,0 +1,187 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#include "pathfinder/bfs.hpp" +#include "perf.hpp" +#include +#include + +std::string Edge::str() const { + std::stringstream ss; + ss << from->get_full_name(false) << "->" << to->get_full_name(false); + return ss.str(); +} + +std::string PathStackElement::str() /*const*/ { + std::stringstream ss; + if (up) { + ss << child_type.get_name() << "->" << parent_type.get_name() << "." << name; + } else { + ss << parent_type.get_name() << "." << name << "->" << child_type.get_name(); + } + return ss.str(); +} + +bool UnresolvedStackElement::match(PathStackElement &other) { + return elem.parent_type == other.parent_type && + elem.child_type == other.child_type && elem.name == other.name && + elem.up != other.up; +} + +std::string UnresolvedStackElement::str() /*const*/ { + std::stringstream ss; + ss << elem.str(); + if (split) { + ss << " split"; + } + return ss.str(); +} + +// BFSPath implementations +BFSPath::BFSPath(/*const*/ GI_ref_weak path_head) + : Path(path_head) + , path_data(std::make_shared()) { +} + +BFSPath::BFSPath(const BFSPath &other) + : Path(other) + , path_data(std::make_shared(*other.path_data)) + , confidence(other.confidence) + , filtered(other.filtered) + , stop(other.stop) { +} + +BFSPath::BFSPath(const BFSPath &other, /*const*/ GI_ref_weak new_head) + : Path(other) + , path_data(other.path_data) + , confidence(other.confidence) + , filtered(other.filtered) + , stop(other.stop) { + path.push_back(new_head); + assert(!other.filtered); +} + +BFSPath::BFSPath(BFSPath &&other) + : Path(std::move(other)) + , path_data(std::move(other.path_data)) + , confidence(other.confidence) + , filtered(other.filtered) + , stop(other.stop) { +} + +BFSPath BFSPath::operator+(/*const*/ GI_ref_weak gif) { + return BFSPath(*this, gif); +} + +PathData &BFSPath::get_path_data_mut() { + // TODO: this isn't a perfect representation of `unique` + // See: https://en.cppreference.com/w/cpp/memory/shared_ptr/unique + // Unique is removed in C++20, so this is the best we can do for now + // It also comes with issues when multithreading + // See: https://en.cppreference.com/w/cpp/memory/shared_ptr/use_count + if (path_data.use_count() != 1) { + PathData new_data = *path_data; + path_data = std::make_shared(new_data); + } + return *path_data; +} + +PathData &BFSPath::get_path_data() /*const*/ { + return *path_data; +} + +bool BFSPath::strong() /*const*/ { + return confidence == 1.0; +} + +void bfs_visit(/*const*/ GI_ref_weak root, std::function visitor) { + PerfCounterAccumulating pc, pc_search, pc_set_insert, pc_setup, pc_deque_insert, + pc_edges, pc_check_visited, pc_filter, pc_new_path; + pc_set_insert.pause(); + pc_search.pause(); + pc_deque_insert.pause(); + pc_edges.pause(); + pc_check_visited.pause(); + pc_filter.pause(); + pc_new_path.pause(); + + auto node_count = root->get_graph()->node_count(); + std::vector visited(node_count, false); + std::vector visited_weak(node_count, false); + std::deque open_path_queue; + + auto handle_path = [&](BFSPath path) { + pc.pause(); + pc_filter.resume(); + visitor(path); + pc_filter.pause(); + pc.resume(); + + if (path.stop) { + open_path_queue.clear(); + return; + } + + if (path.filtered) { + return; + } + + pc_set_insert.resume(); + visited_weak[path.last()->v_i] = true; + + if (path.strong()) { + visited[path.last()->v_i] = true; + } + pc_set_insert.pause(); + + pc_deque_insert.resume(); + open_path_queue.push_back(std::move(path)); + pc_deque_insert.pause(); + }; + + pc_setup.pause(); + handle_path(std::move(BFSPath(root))); + + pc_search.resume(); + while (!open_path_queue.empty()) { + auto path = std::move(open_path_queue.front()); + open_path_queue.pop_front(); + + pc_edges.resume(); + auto edges = path.last()->get_gif_edges(); + pc_edges.pause(); + for (auto &neighbour : edges) { + pc_check_visited.resume(); + if (visited[neighbour->v_i]) { + pc_check_visited.pause(); + continue; + } + if (visited_weak[neighbour->v_i] && path.contains(neighbour)) { + pc_check_visited.pause(); + continue; + } + pc_check_visited.pause(); + + pc_new_path.resume(); + auto new_path = path + neighbour; + pc_new_path.pause(); + pc_search.pause(); + handle_path(std::move(new_path)); + pc_search.resume(); + } + } + pc_set_insert.pause(); + pc_search.pause(); + pc.pause(); + + printf(" TIME: %3.2lf ms BFS Check Visited\n", pc_check_visited.ms()); + printf(" TIME: %3.2lf ms BFS Edges\n", pc_edges.ms()); + printf(" TIME: %3.2lf ms BFS New Path\n", pc_new_path.ms()); + printf(" TIME: %3.2lf ms BFS Search\n", pc_search.ms()); + printf(" TIME: %3.2lf ms BFS Setup\n", pc_setup.ms()); + printf(" TIME: %3.2lf ms BFS Set Insert\n", pc_set_insert.ms()); + printf(" TIME: %3.2lf ms BFS Deque Insert\n", pc_deque_insert.ms()); + printf(" TIME: %3.2lf ms BFS Non-filter total\n", pc.ms()); + printf(" TIME: %3.2lf ms BFS Filter total\n", pc_filter.ms()); +} diff --git a/src/faebryk/core/cpp/src/pathfinder/pathcounter.cpp b/src/faebryk/core/cpp/src/pathfinder/pathcounter.cpp new file mode 100644 index 00000000..eceff541 --- /dev/null +++ b/src/faebryk/core/cpp/src/pathfinder/pathcounter.cpp @@ -0,0 +1,63 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#include "pathfinder/pathcounter.hpp" +#include "pathfinder/bfs.hpp" +#include "pathfinder/pathfinder.hpp" +#include "perf.hpp" + +bool Counter::exec(PathFinder *pf, bool (PathFinder::*filter)(BFSPath &), BFSPath &p) { + if (!INDIV_MEASURE && !total_counter) { + return (pf->*filter)(p); + } + + // perf pre + in_cnt++; + auto confidence_pre = p.confidence; + if (confidence_pre < 1.0) { + weak_in_cnt++; + } + PerfCounter pc; + + // exec + bool res = (pf->*filter)(p); + + // perf post + int64_t duration_ns = pc.ns(); + time_spent_s += duration_ns * 1e-9; + + if (res) { + out_cnt++; + } + if (p.confidence < confidence_pre) { + out_weaker++; + } else if (p.confidence > confidence_pre) { + out_stronger++; + } + + return res; +} + +std::vector +Counter::exec_multi(PathFinder *pf, + std::vector (PathFinder::*filter)(std::vector &), + std::vector &p) { + if (!INDIV_MEASURE && !total_counter) { + return (pf->*filter)(p); + } + + in_cnt += p.size(); + PerfCounter pc; + + // exec + auto res = (pf->*filter)(p); + + // perf post + int64_t duration_ns = pc.ns(); + time_spent_s += duration_ns * 1e-9; + + out_cnt += res.size(); + + return res; +} \ No newline at end of file diff --git a/src/faebryk/core/cpp/src/pathfinder/pathfinder.cpp b/src/faebryk/core/cpp/src/pathfinder/pathfinder.cpp new file mode 100644 index 00000000..da79e4f6 --- /dev/null +++ b/src/faebryk/core/cpp/src/pathfinder/pathfinder.cpp @@ -0,0 +1,426 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#include "pathfinder/pathfinder.hpp" +#include "graph/links.hpp" +#include "pathfinder/bfs.hpp" +#include "pathfinder/pathcounter.hpp" +#include +#include + +// PathFinder implementations +PathFinder::PathFinder() + : filters{ + Filter{ + .filter = &PathFinder::_count, + .discovery = true, + .counter = + Counter{ + .hide = true, + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_by_node_type, + .discovery = true, + .counter = + Counter{ + .name = "node type", + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_gif_type, + .discovery = true, + .counter = + Counter{ + .name = "gif type", + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_by_dead_end_split, + .discovery = true, + .counter = + Counter{ + .name = "dead end split", + }, + }, + Filter{ + .filter = &PathFinder::_filter_conditional_link, + .discovery = true, + .counter = + Counter{ + .name = "conditional link", + }, + }, + Filter{ + .filter = &PathFinder::_build_path_stack, + .discovery = true, + .counter = + Counter{ + .name = "build stack", + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_by_end_in_self_gif, + .discovery = false, + .counter = + Counter{ + .name = "end in self gif", + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_same_end_type, + .discovery = false, + .counter = + Counter{ + .name = "same end type", + }, + }, + Filter{ + .filter = &PathFinder::_filter_path_by_stack, + .discovery = false, + .counter = + Counter{ + .name = "stack", + }, + }, + } { +} + +// Filter implementations +bool Filter::exec(PathFinder *pf, BFSPath &p) { + bool out = counter.exec(pf, filter, p); + if (!out && discovery) { + p.filtered = true; + } + return out; +} + +bool PathFinder::run_filters(BFSPath &p) { + for (auto &filter : filters) { + bool res = filter.exec(this, p); + if (!res) { + return false; + } + } + return true; +} + +std::pair, std::vector> +PathFinder::find_paths(Node_ref src, std::vector dst) { + if (!src->get_type().is_moduleinterface()) { + throw std::runtime_error("src type is not MODULEINTERFACE"); + } + std::unordered_set dsts; + for (auto &d : dst) { + if (!d->get_type().is_moduleinterface()) { + throw std::runtime_error("dst type is not MODULEINTERFACE"); + } + dsts.insert(d); + } + + std::vector paths; + + Counter total_counter{.name = "total", .total_counter = true}; + + PerfCounter pc_bfs; + + bfs_visit(src->get_self_gif().get(), [&](BFSPath &p) { + bool res = total_counter.exec(this, &PathFinder::run_filters, p); + if (!res) { + return; + } + // shortcut if path to dst found + auto last = p.last()->get_node(); + if (dsts.contains(last)) { + dsts.erase(last); + if (dsts.empty()) { + p.stop = true; + } + } + paths.push_back(p); + }); + + printf("TIME: %3.2lf ms BFS\n", pc_bfs.ms()); + + Counter counter_split_join{ + .name = "split join", + .multi = true, + }; + auto multi_paths = counter_split_join.exec_multi( + this, &PathFinder::_filter_paths_by_split_join, this->multi_paths); + + std::vector paths_out; + for (auto &p : paths) { + paths_out.push_back(Path(std::move(p.get_path()))); + } + for (auto &p : multi_paths) { + paths_out.push_back(Path(std::move(p.get_path()))); + } + + std::vector counters; + for (auto &f : filters) { + auto &counter = f.counter; + if (counter.hide) { + continue; + } + counters.push_back(counter); + } + counters.push_back(counter_split_join); + counters.push_back(total_counter); + + return std::make_pair(paths_out, counters); +} + +bool PathFinder::_count(BFSPath &p) { + path_cnt++; + if (path_cnt % 50000 == 0) { + printf("path_cnt: %lld\n", path_cnt); + } + if (path_cnt > PATH_LIMITS.absolute) { + p.stop = true; + } + return true; +} + +bool PathFinder::_filter_path_by_node_type(BFSPath &p) { + return (p.last()->get_node()->get_type().is_moduleinterface()); +} + +bool PathFinder::_filter_path_gif_type(BFSPath &p) { + auto last = p.last(); + return (dynamic_cast(last) || + dynamic_cast(last) || + dynamic_cast(last)); +} + +bool PathFinder::_filter_path_by_end_in_self_gif(BFSPath &p) { + return dynamic_cast(p.last()); +} + +bool PathFinder::_filter_path_same_end_type(BFSPath &p) { + return p.last()->get_node()->get_type() == p.first()->get_node()->get_type(); +} + +std::optional _extend_path_hierarchy_stack(Edge &edge) { + bool up = GraphInterfaceHierarchical::is_uplink(edge.from, edge.to); + if (!up && !GraphInterfaceHierarchical::is_downlink(edge.from, edge.to)) { + return {}; + } + auto child_gif = dynamic_cast(up ? edge.from : edge.to); + auto parent_gif = dynamic_cast(up ? edge.to : edge.from); + + auto name = child_gif->get_parent()->second; + return PathStackElement{parent_gif->get_node()->get_type(), + child_gif->get_node()->get_type(), parent_gif, name, up}; +} + +void _extend_fold_stack(PathStackElement &elem, UnresolvedStack &unresolved_stack, + PathStack &split_stack) { + if (!unresolved_stack.empty() && unresolved_stack.back().match(elem)) { + auto split = unresolved_stack.back().split; + if (split) { + split_stack.push_back(elem); + } + unresolved_stack.pop_back(); + } else { + bool multi_child = elem.parent_gif->get_children().size() > 1; + // if down and multipath -> split + bool split = !elem.up && multi_child; + + unresolved_stack.push_back(UnresolvedStackElement{elem, split}); + if (split) { + split_stack.push_back(elem); + } + } +} + +bool PathFinder::_build_path_stack(BFSPath &p) { + auto edge = p.last_edge(); + if (!edge) { + return true; + } + + auto elem = _extend_path_hierarchy_stack(*edge); + if (!elem) { + return true; + } + + auto &splits = p.get_path_data_mut(); + auto &unresolved_stack = splits.unresolved_stack; + auto &split_stack = splits.split_stack; + + size_t split_cnt = split_stack.size(); + if (split_cnt > 0 && path_cnt > PATH_LIMITS.no_weak) { + return false; + } + + _extend_fold_stack(elem.value(), unresolved_stack, split_stack); + + int split_growth = split_stack.size() - split_cnt; + p.confidence *= std::pow(0.5, split_growth); + + // heuristic, stop making weaker paths after limit + if (split_growth > 0 && path_cnt > PATH_LIMITS.no_new_weak) { + return false; + } + + return true; +} + +bool PathFinder::_filter_path_by_stack(BFSPath &p) { + const auto splits = p.get_path_data(); + auto &unresolved_stack = splits.unresolved_stack; + auto &split_stack = splits.split_stack; + + if (!unresolved_stack.empty()) { + return false; + } + + if (!split_stack.empty()) { + this->multi_paths.push_back(p); + return false; + } + + return true; +} + +bool PathFinder::_filter_path_by_dead_end_split(BFSPath &p) { + auto last_tri_edge = p.last_tri_edge(); + if (!last_tri_edge) { + return true; + } + auto &[one, two, three] = *last_tri_edge; + + auto one_h = dynamic_cast(one); + auto two_h = dynamic_cast(two); + auto three_h = dynamic_cast(three); + if (!one_h || !two_h || !three_h) { + return true; + } + + // check if child->parent->child + if (!one_h->get_is_parent() && two_h->get_is_parent() && !three_h->get_is_parent()) { + return false; + } + + return true; +} + +bool PathFinder::_filter_conditional_link(BFSPath &p) { + auto edge = p.last_edge(); + if (!edge) { + return true; + } + /*const*/ auto linkobj = p.get_link(*edge); + // printf("Path: %s\n", p.str().c_str()); + // printf("Edge: %s\n", edge->str().c_str()); + + bool ok = true; + p.iterate_edges([&](Edge &edge) { + auto link_conditional = dynamic_cast(p.get_link(edge)); + if (!link_conditional) { + return true; + } + bool is_last_edge = edge.to == p.last(); + if (link_conditional->needs_to_check_only_first_in_path() && !is_last_edge) { + return true; + } + bool filtered_out = link_conditional->run_filter(p) != + LinkDirectConditional::FilterResult::FILTER_PASS; + ok &= !filtered_out; + // no need to iterate further + return ok; + }); + + return ok; +} + +template +std::unordered_map> groupby(const std::vector &vec, + std::function f) { + std::unordered_map> out; + for (auto &t : vec) { + out[f(t)].push_back(t); + } + return out; +} + +std::vector +PathFinder::_filter_paths_by_split_join(std::vector &paths) { + std::unordered_set filtered; + std::unordered_map> split; + + // build split map + for (auto &p : paths) { + auto &splits = p.get_path_data(); + auto &unresolved_stack = splits.unresolved_stack; + auto &split_stack = splits.split_stack; + + assert(unresolved_stack.empty()); + assert(!split_stack.empty()); + + // printf("Path: %s\n", p.str().c_str()); + + for (auto &elem : split_stack) { + if (elem.up) { + // join + continue; + } + // split + split[elem.parent_gif].push_back(&p); + } + } + + // printf("Split map: %zu\n", split.size()); + // for (auto &[start_gif, split_paths] : split) { + // printf(" Start gif[%zu]: %s\n", split_paths.size(), + // start_gif->get_full_name().c_str()); + // } + + // check split map + for (auto &[start_gif, split_paths] : split) { + auto children = start_gif->get_node()->get_children( + true, {{Node::Type::get_moduleinterface_type()}}, false); + auto children_set = + std::unordered_set(children.begin(), children.end()); + + assert(split_paths.size()); + auto index = split_paths[0]->index(start_gif); + + std::function f = + [index](/*const*/ BFSPath *p) -> /*const*/ GI_ref_weak { + return p->last(); + }; + auto grouped_by_end = groupby(split_paths, f); + + // printf("Grouped by end: %zu\n", grouped_by_end.size()); + for (auto &[end_gif, grouped_paths] : grouped_by_end) { + // printf(" End gif[%zu]: %s\n", grouped_paths.size(), + // end_gif->get_full_name().c_str()); + + std::unordered_set covered_children; + for (auto &p : grouped_paths) { + covered_children.insert((*p)[index + 1]->get_node()); + } + // printf(" Covered children: %zu/%zu\n", covered_children.size(), + // children_set.size()); + + if (covered_children != children_set) { + filtered.insert(grouped_paths.begin(), grouped_paths.end()); + continue; + } + } + } + + std::vector paths_out; + for (BFSPath &p : paths) { + if (filtered.contains(&p)) { + continue; + } + p.confidence = 1.0; + paths_out.push_back(p); + } + printf("Filtered paths: %zu\n", paths_out.size()); + return paths_out; +} diff --git a/src/faebryk/core/cpp/src/perf.cpp b/src/faebryk/core/cpp/src/perf.cpp new file mode 100644 index 00000000..84c69ab0 --- /dev/null +++ b/src/faebryk/core/cpp/src/perf.cpp @@ -0,0 +1,60 @@ +/* This file is part of the faebryk project + * SPDX-License-Identifier: MIT + */ + +#include "perf.hpp" + +// PerfCounter implementations +PerfCounter::PerfCounter() { + start = std::chrono::high_resolution_clock::now(); +} + +int64_t PerfCounter::ns() { + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + return duration.count(); +} + +double PerfCounter::ms() { + return ns() / 1e6; +} + +double PerfCounter::s() { + return ns() / 1e9; +} + +// PerfCounterAccumulating implementations +PerfCounterAccumulating::PerfCounterAccumulating() { + start = std::chrono::high_resolution_clock::now(); +} + +void PerfCounterAccumulating::pause() { + if (paused) { + return; + } + auto end = std::chrono::high_resolution_clock::now(); + auto duration = std::chrono::duration_cast(end - start); + this->time_ns += duration.count(); + paused = true; +} + +void PerfCounterAccumulating::resume() { + if (!paused) { + return; + } + start = std::chrono::high_resolution_clock::now(); + paused = false; +} + +int64_t PerfCounterAccumulating::ns() { + pause(); + return this->time_ns; +} + +double PerfCounterAccumulating::ms() { + return ns() / 1e6; +} + +double PerfCounterAccumulating::s() { + return ns() / 1e9; +} diff --git a/src/faebryk/core/graph_backends/default.py b/src/faebryk/core/graph_backends/default.py deleted file mode 100644 index bfb25aa1..00000000 --- a/src/faebryk/core/graph_backends/default.py +++ /dev/null @@ -1,25 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -from enum import StrEnum, auto - -from faebryk.libs.util import ConfigFlagEnum - - -class Backends(StrEnum): - NX = auto() - GT = auto() - PY = auto() - - -BACKEND = ConfigFlagEnum(Backends, "BACKEND", Backends.PY, "Graph backend") - -if BACKEND == Backends.GT: - from faebryk.core.graph_backends.graphgt import GraphGT as GraphImpl # noqa: F401 -elif BACKEND == Backends.NX: - from faebryk.core.graph_backends.graphnx import GraphNX as GraphImpl # noqa: F401 -elif BACKEND == Backends.PY: - from faebryk.core.graph_backends.graphpy import GraphPY as GraphImpl # noqa: F401 -else: - print(BACKEND) - assert False diff --git a/src/faebryk/core/graph_backends/graphgt.py b/src/faebryk/core/graph_backends/graphgt.py deleted file mode 100644 index d8f25bc8..00000000 --- a/src/faebryk/core/graph_backends/graphgt.py +++ /dev/null @@ -1,173 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import logging -from collections import defaultdict -from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Mapping - -import graph_tool as gt -from graph_tool.generation import graph_union - -from faebryk.core.graph import Graph - -logger = logging.getLogger(__name__) - -# only for typechecker - -if TYPE_CHECKING: - from faebryk.core.link import Link - - -class GraphGT[T](Graph[T, gt.Graph]): - GI = gt.Graph - - PROPS: dict[gt.Graph, dict[str, gt.PropertyMap | dict]] = defaultdict(dict) - - def __init__(self): - G = gt.Graph(directed=False) - super().__init__(G) - - lookup: dict[T, int] = {} - - # direct - # G.vp["KV"] = G.new_vertex_property("object") - # G.gp["VK"] = G.new_graph_property("object", lookup) - # G.ep["L"] = G.new_edge_property("object") - - # indirect (2x faster) - type(self).PROPS[self()]["KV"] = G.new_vertex_property("object") - type(self).PROPS[self()]["VK"] = lookup - type(self).PROPS[self()]["L"] = G.new_edge_property("object") - - # full python (2x faster than indirect, but no searching for props in C++) - # kv: dict[int, T] = {} - # lp: dict[tuple[int, int], "Link"] = {} - # type(self).PROPS[self()]["KV"] = kv - # type(self).PROPS[self()]["VK"] = lookup - # type(self).PROPS[self()]["L"] = lp - - @classmethod - def ckv(cls, g: gt.Graph) -> gt.VertexPropertyMap: - return cls.PROPS[g]["KV"] - - @classmethod - def cvk(cls, g: gt.Graph) -> dict[T, int]: - return cls.PROPS[g]["VK"] - - @classmethod - def clp(cls, g: gt.Graph) -> gt.EdgePropertyMap: - return cls.PROPS[g]["L"] - - @property - def kv(self) -> gt.VertexPropertyMap: - return type(self).ckv(self()) - - @property - def vk(self) -> dict[T, int]: - return type(self).cvk(self()) - - @property - def lp(self) -> gt.EdgePropertyMap: - return type(self).clp(self()) - - @property - def node_cnt(self) -> int: - return self().num_vertices() - - @property - def edge_cnt(self) -> int: - return self().num_edges() - - def v(self, obj: T): - v_i = self.vk.get(obj) - if v_i is not None: - return self().vertex(v_i) - - v = self().add_vertex() - v_i = self().vertex_index[v] - self.kv[v] = obj - self.vk[obj] = v_i - return v - - def _v_to_obj(self, v: gt.VertexBase | int) -> T: - return self.kv[v] - - def _as_graph_vertex_func[O]( - self, f: Callable[[T], O] - ) -> Callable[[gt.VertexBase | int], O]: - return lambda v: f(self._v_to_obj(v)) - - def add_edge(self, from_obj: T, to_obj: T, link: "Link"): - from_v = self.v(from_obj) - to_v = self.v(to_obj) - e = self().add_edge(from_v, to_v, add_missing=False) - self.lp[e] = link - - def is_connected(self, from_obj: T, to_obj: T) -> "Link | None": - from_v = self.v(from_obj) - to_v = self.v(to_obj) - e = self().edge(from_v, to_v, add_missing=False) - if not e: - return None - return self.lp[e] - - def get_edges(self, obj: T) -> Mapping[T, "Link"]: - v = self.v(obj) - v_i = self().vertex_index[v] - - def other(v_i_l, v_i_r): - return v_i_l if v_i_r == v_i else v_i_r - - return { - self._v_to_obj(other(v_i_l, v_i_r)): self.lp[v_i_l, v_i_r] - for v_i_l, v_i_r in self().get_all_edges(v) - } - - @classmethod - def _union(cls, g1: gt.Graph, g2: gt.Graph) -> gt.Graph: - v_is = len(g1.get_vertices()) - # slower than manual, but merges properties - graph_union( - g1, - g2, - internal_props=True, - include=True, - props=[ - (cls.ckv(g1), cls.ckv(g2)), - (cls.clp(g1), cls.clp(g2)), - ], - ) - - # manual - # g1.add_vertex(g2.num_vertices()) - # g1.add_edge_list(g2.get_edges() + v_is) - # this does not work with objects - # cls.ckv(g1).a = np.append(cls.ckv(g1).a, cls.ckv(g2).a) - # cls.clp(g1).a = np.append(cls.clp(g1).a, cls.clp(g2).a) - - # full python - # cls.ckv(g1).update({v: o for v, o in cls.ckv(g2).items()}) - # cls.clp(g1).update({e: l for e, l in cls.clp(g2).items()}) - - cls.cvk(g1).update({k: v + v_is for k, v in cls.cvk(g2).items()}) - - del cls.PROPS[g2] - - return g1 - - def bfs_visit( - self, filter: Callable[[T], bool], start: Iterable[T], G: gt.Graph | None = None - ): - # TODO implement with gt bfs - return super().bfs_visit(filter, start, G) - - def _iter(self, g: gt.Graph): - return (self._v_to_obj(v) for v in g.iter_vertices()) - - def __iter__(self) -> Iterator[T]: - return self._iter(self()) - - def subgraph(self, node_filter: Callable[[T], bool]): - return self._iter( - gt.GraphView(self(), vfilt=self._as_graph_vertex_func(node_filter)) - ) diff --git a/src/faebryk/core/graph_backends/graphig.py b/src/faebryk/core/graph_backends/graphig.py deleted file mode 100644 index 4e9f0f1e..00000000 --- a/src/faebryk/core/graph_backends/graphig.py +++ /dev/null @@ -1,50 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - - -# class GraphIG[T](Graph[T, ig.Graph]): -# # Notes: -# # - union is slow -# # - add_edge is slowish -# -# def __init__(self): -# super().__init__(ig.Graph(vertex_attrs={"name": "name"})) -# -# @property -# def node_cnt(self) -> int: -# return len(self().vs) -# -# @property -# def edge_cnt(self) -> int: -# return len(self().es) -# -# def v(self, obj: T, add=False) -> ig.Vertex: -# out = str(id(obj)) -# if add and out not in self().vs["name"]: -# return self().add_vertex(name=out, obj=obj) -# return out -# -# def add_edge(self, from_obj: T, to_obj: T, link: "Link") -> ig.Edge: -# from_v = self.v(from_obj, True) -# to_v = self.v(to_obj, True) -# return self().add_edge(from_v, to_v, link=link) -# -# def is_connected(self, from_obj: T, to_obj: T) -> "Link | None": -# try: -# v_from = self().vs.find(name=self.v(from_obj)) -# v_to = self().vs.find(name=self.v(to_obj)) -# except ValueError: -# return None -# edge = self().es.select(_source=v_from, _target=v_to) -# if not edge: -# return None -# -# return edge[0]["link"] -# -# def get_edges(self, obj: T) -> Mapping[T, "Link"]: -# edges = self().es.select(_source=self.v(obj)) -# return {self().vs[edge.target]["name"]: edge["link"] for edge in edges} -# -# @staticmethod -# def _union(rep: ig.Graph, old: ig.Graph): -# return rep + old # faster, but correct? diff --git a/src/faebryk/core/graph_backends/graphnx.py b/src/faebryk/core/graph_backends/graphnx.py deleted file mode 100644 index ea22051f..00000000 --- a/src/faebryk/core/graph_backends/graphnx.py +++ /dev/null @@ -1,99 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import logging -from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Mapping - -import networkx as nx - -from faebryk.core.graph import Graph - -logger = logging.getLogger(__name__) - -# only for typechecker - -if TYPE_CHECKING: - from faebryk.core.link import Link - - -class GraphNX[T](Graph[T, nx.Graph]): - GI = nx.Graph - - def __init__(self): - super().__init__(self.GI()) - - @property - def node_cnt(self) -> int: - return len(self()) - - @property - def edge_cnt(self) -> int: - return self().size() - - def v(self, obj: T): - return obj - - def add_edge(self, from_obj: T, to_obj: T, link: "Link"): - self().add_edge(from_obj, to_obj, link=link) - - def is_connected(self, from_obj: T, to_obj: T) -> "Link | None": - return self.get_edges(from_obj).get(to_obj) - - def get_edges(self, obj: T) -> Mapping[T, "Link"]: - return {other: d["link"] for other, d in self().adj.get(obj, {}).items()} - - def bfs_visit(self, filter: Callable[[T], bool], start: Iterable[T], G=None): - # nx impl, >3x slower - # fG = nx.subgraph_view(G, filter_node=filter) - # return [o for _, o in nx.bfs_edges(fG, start[0])] - return super().bfs_visit(filter, start, G) - - @staticmethod - def _union(rep: GI, old: GI): - # merge big into small - if len(old) > len(rep): - rep, old = old, rep - - # print(f"union: {len(rep.nodes)=} {len(old.nodes)=}") - rep.update(old) - - return rep - - def subgraph(self, node_filter: Callable[[T], bool]): - return nx.subgraph_view(self(), filter_node=node_filter) - - def __repr__(self) -> str: - from textwrap import dedent - - return dedent(f""" - {type(self).__name__}( - {self.graph_repr(self())} - ) - """) - - @staticmethod - def graph_repr(G: nx.Graph) -> str: - from textwrap import dedent, indent - - nodes = indent("\n".join(f"{k}" for k in G.nodes), " " * 4 * 5) - longest_node_name = max(len(str(k)) for k in G.nodes) - - def edge_repr(u, v, d) -> str: - if "link" not in d: - link = "" - else: - link = f"({type(d['link']).__name__})" - return f"{str(u)+' ':-<{longest_node_name+1}}--{link:-^20}" f"--> {v}" - - edges = indent( - "\n".join(edge_repr(u, v, d) for u, v, d in G.edges(data=True)), - " " * 4 * 5, - ) - - return dedent(f""" - Nodes ----- {len(G)}\n{nodes} - Edges ----- {G.size()}\n{edges} - """) - - def __iter__(self) -> Iterator[T]: - return iter(self()) diff --git a/src/faebryk/core/graph_backends/graphpy.py b/src/faebryk/core/graph_backends/graphpy.py deleted file mode 100644 index 8eb45bd4..00000000 --- a/src/faebryk/core/graph_backends/graphpy.py +++ /dev/null @@ -1,136 +0,0 @@ -# This file is part of the faebryk project -# SPDX-License-Identifier: MIT - -import logging -from collections import defaultdict -from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Mapping, Sized - -from faebryk.core.graph import Graph - -logger = logging.getLogger(__name__) - -# only for typechecker - -if TYPE_CHECKING: - from faebryk.core.link import Link - -type L = "Link" - - -class PyGraph[T](Sized, Iterable[T]): - def __init__(self, filter: Callable[[T], bool] | None = None): - # undirected - self._e = list[tuple[T, T, L]]() - self._e_cache = defaultdict[T, dict[T, L]](dict) - self._v = set[T]() - - def __iter__(self) -> Iterator[T]: - return iter(self._v) - - def __len__(self) -> int: - return len(self._v) - - def size(self) -> int: - return len(self._e) - - def add_edge(self, from_obj: T, to_obj: T, link: L): - self._e.append((from_obj, to_obj, link)) - self._e_cache[from_obj][to_obj] = link - self._e_cache[to_obj][from_obj] = link - self._v.add(from_obj) - self._v.add(to_obj) - - def remove_edge(self, from_obj: T, to_obj: T | None = None): - targets = [to_obj] if to_obj else list(self.edges(from_obj).keys()) - for target in targets: - self._e.remove((from_obj, target, self._e_cache[from_obj][target])) - del self._e_cache[from_obj][target] - del self._e_cache[target][from_obj] - - def update(self, other: "PyGraph[T]"): - self._v.update(other._v) - self._e.extend(other._e) - self._e_cache.update(other._e_cache) - - def view(self, filter_node: Callable[[T], bool]) -> "PyGraph[T]": - return PyGraphView[T](self, filter_node) - - def edges(self, obj: T) -> Mapping[T, L]: - return self._e_cache[obj] - - -class PyGraphView[T](PyGraph[T]): - def __init__(self, parent: PyGraph[T], filter: Callable[[T], bool]): - self._parent = parent - self._filter = filter - - def update(self, other: "PyGraph[T]"): - raise TypeError("Cannot update a view") - - def add_edge(self, from_obj: T, to_obj: T, link: L): - raise TypeError("Cannot add edge to a view") - - def __iter__(self) -> Iterator[T]: - return filter(self._filter, iter(self._parent)) - - def __len__(self) -> int: - return sum(1 for _ in self) - - def size(self) -> int: - return sum( - 1 for _ in self._parent._e if self._filter(_[0]) and self._filter(_[1]) - ) - - def edges(self, obj: T) -> Mapping[T, L]: - return {k: v for k, v in self._parent.edges(obj).items() if self._filter(k)} - - def view(self, filter_node: Callable[[T], bool]) -> "PyGraph[T]": - return PyGraphView[T]( - self._parent, lambda x: self._filter(x) and filter_node(x) - ) - - -class GraphPY[T](Graph[T, PyGraph[T]]): - type GI = PyGraph[T] - - def __init__(self): - super().__init__(PyGraph[T]()) - - @property - def node_cnt(self) -> int: - return len(self()) - - @property - def edge_cnt(self) -> int: - return self().size() - - def v(self, obj: T): - return obj - - def add_edge(self, from_obj: T, to_obj: T, link: L): - self().add_edge(from_obj, to_obj, link=link) - - def remove_edge(self, from_obj: T, to_obj: T | None = None): - return self().remove_edge(from_obj, to_obj) - - def is_connected(self, from_obj: T, to_obj: T) -> "Link | None": - return self.get_edges(from_obj).get(to_obj) - - def get_edges(self, obj: T) -> Mapping[T, L]: - return self().edges(obj) - - @staticmethod - def _union(rep: GI, old: GI): - # merge big into small - if len(old) > len(rep): - rep, old = old, rep - - rep.update(old) - - return rep - - def subgraph(self, node_filter: Callable[[T], bool]): - return self().view(node_filter) - - def __iter__(self) -> Iterator[T]: - return iter(self()) diff --git a/src/faebryk/core/link.py b/src/faebryk/core/link.py index b9d2e510..4b7402d0 100644 --- a/src/faebryk/core/link.py +++ b/src/faebryk/core/link.py @@ -10,6 +10,7 @@ LinkDirect, LinkDirectConditional, LinkDirectConditionalFilterResult, + LinkDirectDerived, LinkFilteredException, LinkNamedParent, LinkParent, diff --git a/src/faebryk/core/module.py b/src/faebryk/core/module.py index 9218f8e6..bf1287e0 100644 --- a/src/faebryk/core/module.py +++ b/src/faebryk/core/module.py @@ -3,7 +3,7 @@ import logging from typing import TYPE_CHECKING, Callable, Iterable -from faebryk.core.moduleinterface import GraphInterfaceModuleSibling +from faebryk.core.cpp import GraphInterfaceModuleSibling from faebryk.core.node import Node, NodeException, f_field from faebryk.core.trait import Trait from faebryk.libs.util import cast_assert, unique_ref @@ -145,6 +145,10 @@ def connect_all_interfaces_by_name( for k, (src_m, dst_m) in src_.zip_children_by_name_with( dst_, ModuleInterface ).items(): + # TODO: careful this also connects runtime children + # for now skip stuff prefixed with _ + if k.startswith("_"): + continue if src_m is None or dst_m is None: if not allow_partial: raise Exception(f"Node with name {k} not present in both") diff --git a/src/faebryk/core/moduleinterface.py b/src/faebryk/core/moduleinterface.py index 12ea622d..43c12718 100644 --- a/src/faebryk/core/moduleinterface.py +++ b/src/faebryk/core/moduleinterface.py @@ -1,6 +1,7 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT import logging +from itertools import pairwise from typing import ( Iterable, Sequence, @@ -9,9 +10,9 @@ from typing_extensions import Self -from faebryk.core.cpp import ( # noqa: F401 +from faebryk.core.cpp import ( GraphInterfaceModuleConnection, - GraphInterfaceModuleSibling, + Path, ) from faebryk.core.graphinterface import GraphInterface from faebryk.core.link import ( @@ -19,83 +20,18 @@ LinkDirect, LinkDirectConditional, LinkDirectConditionalFilterResult, - LinkFilteredException, + LinkDirectDerived, ) -from faebryk.core.node import CNode, Node +from faebryk.core.node import CNode, Node, NodeException +from faebryk.core.pathfinder import find_paths from faebryk.core.trait import Trait from faebryk.library.can_specialize import can_specialize -from faebryk.libs.util import cast_assert, is_type_set_subclasses, once +from faebryk.libs.util import ConfigFlag, cast_assert, groupby, once logger = logging.getLogger(__name__) -# The resolve functions are really weird -# You have to look into where they are called to make sense of what they are doing -# Chain resolve is for deciding what to do in a case like this -# if1 -> link1 -> if2 -> link2 -> if3 -# This will then decide with which link if1 and if3 are connected -def _resolve_link_transitive(links: set[type[Link]]) -> type[Link]: - if len(links) == 1: - return next(iter(links)) - - if is_type_set_subclasses(links, {LinkDirectConditional}): - # TODO this only works if the filter is identical - raise NotImplementedError() - - if is_type_set_subclasses(links, {LinkDirect, LinkDirectConditional}): - return [u for u in links if issubclass(u, LinkDirectConditional)][0] - - raise NotImplementedError() - - -# This one resolves the case if1 -> link1 -> if2; if1 -> link2 -> if2 -def _resolve_link_duplicate(links: Iterable[type[Link]]) -> type[Link]: - uniq = set(links) - assert uniq - - if len(uniq) == 1: - return next(iter(uniq)) - - if is_type_set_subclasses(uniq, {LinkDirect, LinkDirectConditional}): - return [u for u in uniq if not issubclass(u, LinkDirectConditional)][0] - - raise NotImplementedError() - - -class _LEVEL: - """connect depth counter to debug connections in ModuleInterface""" - - def __init__(self) -> None: - self.value = 0 - - def inc(self): - self.value += 1 - return self.value - 1 - - def dec(self): - self.value -= 1 - - -_CONNECT_DEPTH = _LEVEL() - - -# CONNECT PROCEDURE -# connect -# connect_siblings -# - check not same ref -# - check not connected -# - connect_hierarchies -# - resolve link (if exists) -# - connect gifs -# - signal on_connect -# - connect_down -# - connect direct children by name -# - connect_up -# - check for each parent if all direct children by name connected -# - connect -# - check not filtered -# - cross connect_hierarchies transitive hull -# - cross connect_hierarchies siblings +IMPLIED_PATHS = ConfigFlag("IMPLIED_PATHS", default=False, descr="Use implied paths") class ModuleInterface(Node): @@ -105,24 +41,39 @@ class TraitT(Trait): ... specialized: GraphInterface connected: GraphInterfaceModuleConnection + # TODO: move to cpp class _LinkDirectShallow(LinkDirectConditional): """ Make link that only connects up but not down """ - def has_no_parent_with_type(self, node: CNode): - parents = (p[0] for p in node.get_hierarchy()[:-1]) - return not any(isinstance(p, self.test_type) for p in parents) + def is_childtype_of_test_type(self, node: CNode): + return isinstance(node, self.children_types) + # return type(node) in self.children_types + + def check_path(self, path: Path) -> LinkDirectConditionalFilterResult: + out = ( + LinkDirectConditionalFilterResult.FILTER_PASS + if not self.is_childtype_of_test_type(path[0].node) + else LinkDirectConditionalFilterResult.FILTER_FAIL_UNRECOVERABLE + ) + return out def __init__(self, test_type: type["ModuleInterface"]): self.test_type = test_type + # TODO this is a bit of a hack to get the children types + # better to do on set_connections + self.children_types = tuple( + type(c) + for c in test_type().get_children( + direct_only=False, types=ModuleInterface, include_root=False + ) + ) super().__init__( - lambda src, dst: LinkDirectConditionalFilterResult.FILTER_PASS - if self.has_no_parent_with_type(dst.node) - else LinkDirectConditionalFilterResult.FILTER_FAIL_UNRECOVERABLE + self.check_path, + needs_only_first_in_path=True, ) - # TODO rename @classmethod @once def LinkDirectShallow(cls): @@ -134,221 +85,83 @@ def __init__(self): def __preinit__(self) -> None: ... - @staticmethod - def _get_connected(gif: GraphInterface, clss: bool): - assert isinstance(gif.node, ModuleInterface) - connections = gif.edges.items() - - # check if ambiguous links between mifs - assert len(connections) == len({c[0] for c in connections}) - - return { - cast_assert(ModuleInterface, s.node): (link if not clss else type(link)) - for s, link in connections - if s.node is not gif.node - } - - def get_connected(self, clss: bool = False): - return self._get_connected(self.connected, clss) - - def get_specialized(self, clss: bool = False): - return self._get_connected(self.specialized, clss) - - def get_specializes(self, clss: bool = False): - return self._get_connected(self.specializes, clss) - - @staticmethod - def _cross_connect( - s_group: dict["ModuleInterface", type[Link]], - d_group: dict["ModuleInterface", type[Link]], - linkcls: type[Link], - hint=None, - ): - if logger.isEnabledFor(logging.DEBUG) and hint is not None: - logger.debug(f"Connect {hint} {s_group} -> {d_group}") - - for s, slink in s_group.items(): - linkclss = {slink, linkcls} - linkclss_ambiguous = len(linkclss) > 1 - for d, dlink in d_group.items(): - # can happen while connection trees are resolving - if s is d: - continue - if not linkclss_ambiguous and dlink in linkclss: - link = linkcls - else: - link = _resolve_link_transitive(linkclss | {dlink}) - - s._connect_across_hierarchies(d, linkcls=link) - - def _connect_siblings_and_connections( - self, other: "ModuleInterface", linkcls: type[Link] + def connect( + self: Self, *other: Self, link: type[Link] | Link | None = None ) -> Self: - if other is self: - return self - - # Already connected - if self.is_connected_to(other): - return self - - # if link is filtered, cancel here - self._connect_across_hierarchies(other, linkcls) - if not self.is_connected_to(other): - return self - - if logger.isEnabledFor(logging.DEBUG): - logger.debug(f"MIF connection: {self} to {other}") - - # Connect to all connections - s_con = self.get_connected(clss=True) | {self: linkcls} - d_con = other.get_connected(clss=True) | {other: linkcls} - ModuleInterface._cross_connect(s_con, d_con, linkcls, "connections") - - # Connect to all siblings - s_sib = ( - self.get_specialized(clss=True) - | self.get_specializes(clss=True) - | {self: linkcls} - ) - d_sib = ( - other.get_specialized(clss=True) - | other.get_specializes(clss=True) - | {other: linkcls} - ) - ModuleInterface._cross_connect(s_sib, d_sib, linkcls, "siblings") - - return self - - def _on_connect(self, other: "ModuleInterface"): - """override to handle custom connection logic""" - ... - - def _try_connect_down(self, other: "ModuleInterface", linkcls: type[Link]) -> None: - if not isinstance(other, type(self)): - return - - for _, (src, dst) in self.zip_children_by_name_with( - other, ModuleInterface - ).items(): - if src is None or dst is None: - continue - src.connect(dst, linkcls=linkcls) - - def _try_connect_up(self, other: "ModuleInterface") -> None: - p1 = self.get_parent() - p2 = other.get_parent() - if not ( - p1 - and p2 - and p1[0] is not p2[0] - and isinstance(p1[0], type(p2[0])) - and isinstance(p1[0], ModuleInterface) - ): - return - - src_m = p1[0] - dst_m = p2[0] - assert isinstance(dst_m, ModuleInterface) - - def _is_connected(a, b): - assert isinstance(a, ModuleInterface) - assert isinstance(b, ModuleInterface) - return a.is_connected_to(b) - - connection_map = [ - (src_i, dst_i, _is_connected(src_i, dst_i)) - for src_i, dst_i in src_m.zip_children_by_name_with( - dst_m, sub_type=ModuleInterface - ).values() - ] - - assert connection_map - - if not all(connected for _, _, connected in connection_map): - return - - # decide which LinkType to use here - # depends on connections between src_i & dst_i - # e.g. if any Shallow, we need to choose shallow - link = _resolve_link_transitive( - {type(sublink) for _, _, sublink in connection_map if sublink} - ) - - if logger.isEnabledFor(logging.DEBUG): - logger.debug(f"Up connect {src_m} -> {dst_m}") - src_m.connect(dst_m, linkcls=link) - - def _connect_across_hierarchies( - self, other: "ModuleInterface", linkcls: type[Link] - ): - existing_link = self.is_connected_to(other) - if existing_link: - if isinstance(existing_link, linkcls): - return - resolved = _resolve_link_duplicate([type(existing_link), linkcls]) - if resolved is type(existing_link): - return - raise NotImplementedError( - "Overriding existing links not implemented, tried to override " - + f"{existing_link} with {resolved}" + if not {type(o) for o in other}.issubset({type(self)}): + raise NodeException( + self, + f"Can only connect modules of same type: {{{type(self)}}}," + f" got {{{','.join(str(type(o)) for o in other)}}}", ) - # level 0 connect - try: - self.connected.connect(other.connected, linkcls()) - except LinkFilteredException: - return - - if logger.isEnabledFor(logging.DEBUG): - logger.debug(f"{' '*2*_CONNECT_DEPTH.inc()}Connect {self} to {other}") - self._on_connect(other) - - con_depth_one = _CONNECT_DEPTH.value == 1 - recursion_error = None - try: - # level +1 (down) connect - self._try_connect_down(other, linkcls=linkcls) - - # level -1 (up) connect - self._try_connect_up(other) - - except RecursionError as e: - recursion_error = e - if not con_depth_one: - raise - - if recursion_error: - raise Exception(f"Recursion error while connecting {self} to {other}") - - _CONNECT_DEPTH.dec() + # TODO: consider returning self always + # - con: if construcing anonymous stuff in connection no ref + # - pro: more intuitive + ret = other[-1] if other else self + + if link is None: + link = LinkDirect + if isinstance(link, type): + link = link() + + # resolve duplicate links + new_links = [ + o.connected + for o in other + if not (existing_link := self.connected.is_connected_to(o.connected)) + or existing_link != link + ] - def connect(self: Self, *other: Self, linkcls=None) -> Self: - # TODO consider some type of check at the end within the graph instead - # assert type(other) is type(self) - if linkcls is None: - linkcls = LinkDirect + self.connected.connect(new_links, link=link) - for o in other: - self._connect_siblings_and_connections(o, linkcls=linkcls) - return other[-1] if other else self + return ret - def connect_via(self, bridge: Node | Sequence[Node], *other: Self, linkcls=None): + def connect_via(self, bridge: Node | Sequence[Node], *other: Self, link=None): from faebryk.library.can_bridge import can_bridge bridges = [bridge] if isinstance(bridge, Node) else bridge intf = self for sub_bridge in bridges: t = sub_bridge.get_trait(can_bridge) - intf.connect(t.get_in(), linkcls=linkcls) + intf.connect(t.get_in(), link=link) intf = t.get_out() - intf.connect(*other, linkcls=linkcls) + intf.connect(*other, link=link) + + def connect_shallow(self, *other: Self) -> Self: + # TODO: clone limitation, waiting for c++ LinkShallow + if len(other) > 1: + for o in other: + self.connect_shallow(o) + return self + + return self.connect(*other, link=type(self).LinkDirectShallow()) + + def get_connected(self, include_self: bool = False) -> dict[Self, Path]: + paths = find_paths(self, []) + # TODO theoretically we could get multiple paths for the same MIF + # practically this won't happen in the current implementation + paths_per_mif = groupby(paths, lambda p: cast_assert(type(self), p[-1].node)) - def connect_shallow(self, other: Self) -> Self: - return self.connect(other, linkcls=type(self).LinkDirectShallow()) + def choose_path(_paths: list[Path]) -> Path: + return self._path_with_least_conditionals(_paths) - def is_connected_to(self, other: "ModuleInterface"): - return self.connected.is_connected_to(other.connected) + path_per_mif = { + mif: choose_path(paths) + for mif, paths in paths_per_mif.items() + if mif is not self or include_self + } + if include_self: + assert self in path_per_mif + for mif, paths in paths_per_mif.items(): + self._connect_via_implied_paths(mif, paths) + return path_per_mif + + def is_connected_to(self, other: "ModuleInterface") -> list[Path]: + return [ + path for path in find_paths(self, [other]) if path[-1] is other.self_gif + ] def specialize[T: ModuleInterface](self, special: T) -> T: logger.debug(f"Specializing MIF {self} with {special}") @@ -363,9 +176,62 @@ def specialize[T: ModuleInterface](self, special: T) -> T: ) # This is doing the heavy lifting - self.connect(special) + self.connected.connect(special.connected) # Establish sibling relationship self.specialized.connect(special.specializes) return cast(T, special) + + # def get_general(self): + # out = self.specializes.get_parent() + # if out: + # return out[0] + # return None + + def __init_subclass__(cls, *, init: bool = True) -> None: + if hasattr(cls, "_on_connect"): + raise TypeError("Overriding _on_connect is deprecated") + + return super().__init_subclass__(init=init) + + @staticmethod + def _path_with_least_conditionals(paths: list["Path"]) -> "Path": + if len(paths) == 1: + return paths[0] + + paths_links = [ + ( + path, + [ + e1.is_connected_to(e2) + for e1, e2 in pairwise(cast(Iterable[GraphInterface], path)) + ], + ) + for path in paths + ] + paths_conditionals = [ + ( + path, + [link for link in links if isinstance(link, LinkDirectConditional)], + ) + for path, links in paths_links + ] + path = min(paths_conditionals, key=lambda x: len(x[1]))[0] + return path + + def _connect_via_implied_paths(self, other: Self, paths: list["Path"]): + if not IMPLIED_PATHS: + return + + if self is other: + return + + if self.connected.is_connected_to(other.connected): + # TODO link resolution + return + + # heuristic: choose path with fewest conditionals + path = self._path_with_least_conditionals(paths) + + self.connect(other, link=LinkDirectDerived(path)) diff --git a/src/faebryk/core/pathfinder.py b/src/faebryk/core/pathfinder.py new file mode 100644 index 00000000..ec012ac7 --- /dev/null +++ b/src/faebryk/core/pathfinder.py @@ -0,0 +1,125 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import io +import logging +from typing import Sequence + +from more_itertools import partition +from rich.console import Console +from rich.table import Table + +from faebryk.core.cpp import Counter, Path, set_indiv_measure, set_max_paths +from faebryk.core.cpp import find_paths as find_paths_cpp +from faebryk.core.node import Node +from faebryk.libs.util import ConfigFlag, ConfigFlagInt + +logger = logging.getLogger(__name__) + +# Also in C++ +INDIV_MEASURE = ConfigFlag( + "INDIV_MEASURE", default=True, descr="Measure individual paths" +) +set_indiv_measure(bool(INDIV_MEASURE)) + + +MAX_PATHS = ConfigFlagInt("MAX_PATHS", default=int(1e6), descr="Max paths to search") +MAX_PATHS_NO_NEW_WEAK = ConfigFlagInt( + "MAX_PATHS_NO_NEW_WEAK", default=int(1e4), descr="Max paths with no new weak" +) +MAX_PATHS_NO_WEAK = ConfigFlagInt( + "MAX_PATHS_NO_WEAK", default=int(1e5), descr="Max paths with no weak" +) +set_max_paths(int(MAX_PATHS), int(MAX_PATHS_NO_NEW_WEAK), int(MAX_PATHS_NO_WEAK)) + + +def find_paths(src: Node, dst: Sequence[Node]) -> Sequence[Path]: + paths, counters = find_paths_cpp(src, dst) + + if logger.isEnabledFor(logging.DEBUG): + logger.debug(Counters(counters)) + return paths + + +class Counters: + def __init__(self, counters: list[Counter]): + self.counters: dict[str, Counter] = {c.name: c for c in counters} + + def __repr__(self): + table = Table(title="Filter Counters") + table.add_column("func", style="cyan", width=20) + table.add_column("in", style="green", justify="right") + table.add_column("weak in", style="green", justify="right") + table.add_column("out", style="green", justify="right") + # table.add_column("drop", style="cyan", justify="center") + table.add_column("filt", style="magenta", justify="right") + table.add_column("weaker", style="green", justify="right") + table.add_column("stronger", style="green", justify="right") + table.add_column("time", style="yellow", justify="right") + table.add_column("time/in", style="yellow", justify="right") + + individual, total = partition( + lambda x: x[1].total_counter, self.counters.items() + ) + individual = list(individual) + for section in partition(lambda x: x[1].multi, individual): + for k, v in sorted( + section, + key=lambda x: (x[1].out_cnt, x[1].in_cnt), + reverse=True, + ): + k_clean = ( + k.split("path_")[-1] + .replace("_", " ") + .removeprefix("by ") + .removeprefix("with ") + ) + if v.in_cnt == 0: + continue + table.add_row( + k_clean, + str(v.in_cnt), + str(v.weak_in_cnt), + str(v.out_cnt), + # "x" if getattr(k, "discovery_filter", False) else "", + f"{(1-v.out_cnt/v.in_cnt)*100:.1f} %" if v.in_cnt else "-", + str(v.out_weaker), + str(v.out_stronger), + f"{v.time_spent_s*1000:.2f} ms", + f"{v.time_spent_s/v.in_cnt*1000*1000:.2f} us" if v.in_cnt else "-", + ) + table.add_section() + + table.add_section() + for k, v in total: + if v.in_cnt == 0: + continue + table.add_row( + k, + str(v.in_cnt), + str(v.weak_in_cnt), + str(v.out_cnt), + # "x" if getattr(k, "discovery_filter", False) else "", + f"{(1-v.out_cnt/v.in_cnt)*100:.1f} %" if v.in_cnt else "-", + str(v.out_weaker), + str(v.out_stronger), + f"{v.time_spent_s*1000:.2f} ms", + f"{v.time_spent_s/v.in_cnt*1000*1000:.2f} us" if v.in_cnt else "-", + ) + if INDIV_MEASURE: + table.add_row( + "Total", + "", + "", + "", + # "", + "", + "", + "", + f"{sum(v.time_spent_s for _,v in individual)*1000:.2f} ms", + f"{sum(v.time_spent_s/v.in_cnt for _,v in individual if v.in_cnt)*1000*1000:.2f} us", # noqa: E501 + ) + + console = Console(record=True, width=120, file=io.StringIO()) + console.print(table) + return console.export_text(styles=True) diff --git a/src/faebryk/exporters/visualize/interactive_graph.py b/src/faebryk/exporters/visualize/interactive_graph.py index 5b78d9ee..3f5b9e2d 100644 --- a/src/faebryk/exporters/visualize/interactive_graph.py +++ b/src/faebryk/exporters/visualize/interactive_graph.py @@ -1,87 +1,87 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import rich -import rich.text +from typing import Collection, Iterable -from faebryk.core.graph import Graph +import dash_cytoscape as cyto +from dash import Dash, html +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 +from faebryk.core.module import Module +from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node -from faebryk.exporters.visualize.util import IDSet, generate_pastel_palette +from faebryk.core.parameter import Parameter +from faebryk.core.trait import Trait +from faebryk.exporters.visualize.util import generate_pastel_palette +from faebryk.libs.util import KeyErrorAmbiguous, find_or, typename -def interactive_graph(G: Graph): - import dash_cytoscape as cyto - from dash import Dash, html +# Transformers ------------------------------------------------------------------------- +def _gif(gif: GraphInterface): + return { + "data": { + "id": id(gif), + "label": gif.name, + "type": typename(gif), + "parent": id(gif.node), + } + } - # Register the fcose layout - cyto.load_extra_layouts() - app = Dash(__name__) +def _link(source, target, link: Link): + return { + "data": { + "source": id(source), + "target": id(target), + "type": typename(link), + } + } - node_types: set[str] = set() - groups = {} - - def _group(gif: GraphInterface) -> str: - node = gif.node - my_node_id = str(id(node)) - if my_node_id not in groups: - label = f"{node.get_full_name()} ({type(node).__name__})" - groups[my_node_id] = { - "data": { - "id": my_node_id, - "label": label, - "type": "group", - } - } - return my_node_id - - def _node(node: Node): - full_name = node.get_full_name() - type_name = type(node).__name__ - node_types.add(type_name) - data = {"id": str(id(node)), "label": full_name, "type": type_name} - if isinstance(node, GraphInterface): - data["parent"] = _group(node) - return {"data": data} - - link_types: set[str] = set() - links_touched = IDSet[Link]() - - def _link(link: Link): - if link in links_touched: - return None - links_touched.add(link) - - try: - source, target = tuple(str(id(n)) for n in link.get_connections()) - except ValueError: - return None - - type_name = type(link).__name__ - link_types.add(type_name) - - return {"data": {"source": source, "target": target, "type": type_name}} - - def _not_none(x): - return x is not None - - elements = [ - *(filter(_not_none, (_node(gif) for gif in G))), - *( - filter( - _not_none, - (_link(link) for gif in G for link in gif.get_links()), - ) - ), - *( - groups.values() - ), # must go after nodes because the node iteration creates the groups - ] +_GROUP_TYPES = { + 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): + try: + subtype = find_or(_GROUP_TYPES, lambda t: isinstance(node, t), default=Node) + except KeyErrorAmbiguous as e: + subtype = e.duplicates[0] + + return { + "data": { + "id": id(node), + "label": f"{node.get_name(accept_no_parent=True)}\n({typename(node)})", + "type": "group", + "subtype": typename(subtype), + "parent": id(node.get_parent_force()[0]) if node.get_parent() else None, + } + } + + +# Style -------------------------------------------------------------------------------- + + +def _with_pastels[T](iterable: Collection[T]): + return zip(sorted(iterable), generate_pastel_palette(len(iterable))) # type: ignore - stylesheet = [ + +class _Stylesheet: + _BASE = [ { "selector": "node", "style": { @@ -89,7 +89,13 @@ def _not_none(x): "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, }, }, { @@ -101,22 +107,10 @@ def _not_none(x): "target-arrow-shape": "triangle", "arrow-scale": 1, "target-arrow-color": "#A3C4BC", + "text-outline-color": "#FFFFFF", + "text-outline-width": 2, }, }, - ] - - def _pastels(iterable): - return zip(iterable, generate_pastel_palette(len(iterable))) - - for node_type, color in _pastels(node_types): - stylesheet.append( - { - "selector": f'node[type = "{node_type}"]', - "style": {"background-color": color}, - } - ) - - stylesheet.append( { "selector": 'node[type = "group"]', "style": { @@ -124,88 +118,180 @@ def _pastels(iterable): "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, }, - } - ) + }, + ] - for link_type, color in _pastels(link_types): - stylesheet.append( + 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}, } ) - container_style = { - "position": "fixed", - "display": "flex", - "flex-direction": "column", - "height": "100%", - "width": "100%", - } + def add_group_type(self, group_type: str, color: str): + self.stylesheet.append( + { + "selector": f'node[subtype = "{group_type}"]', + "style": {"background-color": color}, + } + ) - graph_view_style = { - "position": "absolute", - "width": "100%", - "height": "100%", - "zIndex": 999, - } - _cyto = cyto.Cytoscape( - id="graph-view", - stylesheet=stylesheet, - style=graph_view_style, - elements=elements, - layout={ - "name": "fcose", - "quality": "proof", - "animate": False, - "randomize": False, - "fit": True, - "padding": 50, - "nodeDimensionsIncludeLabels": True, - "uniformNodeDimensions": False, - "packComponents": True, - "nodeRepulsion": 8000, - "idealEdgeLength": 50, - "edgeElasticity": 0.45, - "nestingFactor": 0.1, - "gravity": 0.25, - "numIter": 2500, - "tile": True, - "tilingPaddingVertical": 10, - "tilingPaddingHorizontal": 10, - "gravityRangeCompound": 1.5, - "gravityCompound": 1.0, - "gravityRange": 3.8, - "initialEnergyOnIncremental": 0.5, +def _Layout(stylesheet: _Stylesheet, elements: list[dict[str, dict]]): + return html.Div( + style={ + "position": "fixed", + "display": "flex", + "flex-direction": "column", + "height": "100%", + "width": "100%", }, - ) - - app.layout = html.Div( - style=container_style, children=[ html.Div( className="cy-container", style={"flex": "1", "position": "relative"}, - children=[_cyto], + children=[ + cyto.Cytoscape( + id="graph-view", + stylesheet=stylesheet.stylesheet, + style={ + "position": "absolute", + "width": "100%", + "height": "100%", + "zIndex": 999, + }, + elements=elements, + layout={ + "name": "fcose", + "quality": "proof", + "animate": False, + "randomize": False, + "fit": True, + "padding": 50, + "nodeDimensionsIncludeLabels": True, + "uniformNodeDimensions": False, + "packComponents": True, + "nodeRepulsion": 1000, + "idealEdgeLength": 50, + "edgeElasticity": 0.45, + "nestingFactor": 0.1, + "gravity": 0.25, + "numIter": 2500, + "tile": True, + "tilingPaddingVertical": 10, + "tilingPaddingHorizontal": 10, + "gravityRangeCompound": 1.5, + "gravityCompound": 1.5, + "gravityRange": 3.8, + "initialEnergyOnIncremental": 0.5, + "componentSpacing": 40, + }, + ) + ], ), ], ) - # 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() + +# -------------------------------------------------------------------------------------- + + +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} + + elements = ( + [_gif(gif) for gif in gifs] + + [_link(*edge) for edge in edges] + + [_group(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) + + # 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) + + # + 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) > 1] + + edges = [edge for edge in G.edges if edge[0] in gifs and edge[1] in gifs] + return interactive_subgraph(edges, gifs, nodes, height=height) diff --git a/src/faebryk/library/CH344Q_ReferenceDesign.py b/src/faebryk/library/CH344Q_ReferenceDesign.py index d1be0982..8a2d37c4 100644 --- a/src/faebryk/library/CH344Q_ReferenceDesign.py +++ b/src/faebryk/library/CH344Q_ReferenceDesign.py @@ -101,7 +101,7 @@ def __preinit__(self): self.usb_uart_converter.osc[0].connect(self.oscillator.xtal_if.xout) self.oscillator.xtal_if.gnd.connect(pwr_3v3.lv) - self.reset_lowpass.out.connect(self.usb_uart_converter.reset) + self.reset_lowpass.out.signal.connect(self.usb_uart_converter.reset.signal) self.reset_lowpass.in_.signal.connect( self.usb_uart_converter.reset.reference.hv ) diff --git a/src/faebryk/library/ElectricLogic.py b/src/faebryk/library/ElectricLogic.py index 2bab0db7..fc2b6aae 100644 --- a/src/faebryk/library/ElectricLogic.py +++ b/src/faebryk/library/ElectricLogic.py @@ -1,7 +1,6 @@ # This file is part of the faebryk project # SPDX-License-Identifier: MIT -import sys from abc import abstractmethod from enum import Enum, auto from typing import Self @@ -137,10 +136,3 @@ def connect_shallow( self.reference.lv.connect(other.reference.lv) return super().connect_shallow(other) - - def connect(self, *other: Self, linkcls=None): - recursion_depth = sys.getrecursionlimit() - sys.setrecursionlimit(10000) - ret = super().connect(*other, linkcls=linkcls) - sys.setrecursionlimit(recursion_depth) - return ret diff --git a/src/faebryk/library/ElectricPower.py b/src/faebryk/library/ElectricPower.py index 2d103eaf..6a8cebc8 100644 --- a/src/faebryk/library/ElectricPower.py +++ b/src/faebryk/library/ElectricPower.py @@ -3,14 +3,12 @@ import math -from typing import Self import faebryk.library._F as F -from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node from faebryk.libs.library import L from faebryk.libs.units import P -from faebryk.libs.util import RecursionGuard +from faebryk.libs.util import cast_assert class ElectricPower(F.Power): @@ -76,20 +74,11 @@ def fused(self, attach_to: Node | None = None): return fused_power def __preinit__(self) -> None: - ... # self.voltage.merge( # self.hv.potential - self.lv.potential # ) - - def _on_connect(self, other: ModuleInterface) -> None: - super()._on_connect(other) - - if not isinstance(other, ElectricPower): - return - - self.voltage.merge(other.voltage) - - # TODO remove with lazy mifs - def connect(self: Self, *other: Self, linkcls=None) -> Self: - with RecursionGuard(): - return super().connect(*other, linkcls=linkcls) + self.voltage.add( + F.is_dynamic_by_connections( + lambda mif: cast_assert(ElectricPower, mif).voltage + ) + ) diff --git a/src/faebryk/library/I2C.py b/src/faebryk/library/I2C.py index e96c1ef2..b01ee3e7 100644 --- a/src/faebryk/library/I2C.py +++ b/src/faebryk/library/I2C.py @@ -7,6 +7,7 @@ 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 logger = logging.getLogger(__name__) @@ -29,11 +30,6 @@ def terminate(self): self.sda.pulled.pull(up=True) self.scl.pulled.pull(up=True) - def _on_connect(self, other: "I2C"): - super()._on_connect(other) - - self.frequency.merge(other.frequency) - class SpeedMode(Enum): low_speed = 10 * P.khertz standard_speed = 100 * P.khertz @@ -43,3 +39,8 @@ class SpeedMode(Enum): @staticmethod def define_max_frequency_capability(mode: SpeedMode): return F.Range(I2C.SpeedMode.low_speed, mode) + + def __preinit__(self) -> None: + self.frequency.add( + F.is_dynamic_by_connections(lambda mif: cast_assert(I2C, mif).frequency) + ) diff --git a/src/faebryk/library/Power.py b/src/faebryk/library/Power.py index 594a3c39..56e7646a 100644 --- a/src/faebryk/library/Power.py +++ b/src/faebryk/library/Power.py @@ -5,26 +5,14 @@ class Power(ModuleInterface): - class PowerSourcesShortedError(Exception): ... - class is_power_source(ModuleInterface.TraitT): ... - class is_power_source_defined(is_power_source.impl()): ... - class is_power_sink(ModuleInterface.TraitT): ... - class is_power_sink_defined(is_power_sink.impl()): ... - def make_source(self): - self.add(self.is_power_source_defined()) + self.add(self.is_power_source.impl()()) return self def make_sink(self): - self.add(self.is_power_sink_defined()) + self.add(self.is_power_sink.impl()()) return self - - def _on_connect(self, other: "Power"): - if self.has_trait(self.is_power_source) and other.has_trait( - self.is_power_source - ): - raise self.PowerSourcesShortedError(self, other) diff --git a/src/faebryk/library/RP2040_ReferenceDesign.py b/src/faebryk/library/RP2040_ReferenceDesign.py index c2a8056b..1abb106e 100644 --- a/src/faebryk/library/RP2040_ReferenceDesign.py +++ b/src/faebryk/library/RP2040_ReferenceDesign.py @@ -108,8 +108,10 @@ def __preinit__(self): ) # USB - terminated_usb = self.usb.usb_if.d.terminated() - terminated_usb.impedance.merge(F.Range.from_center_rel(27.4 * P.ohm, 0.05)) + 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)) # Flash self.flash.memory_size.merge(16 * P.Mbit) @@ -157,7 +159,7 @@ def __preinit__(self): self.flash.qspi.connect(self.rp2040.qspi) self.flash.qspi.chip_select.connect(self.boot_selector.logic_out) - terminated_usb.connect(self.rp2040.usb) + terminated_usb_data.connect(self.rp2040.usb) self.rp2040.xtal_if.connect(self.clock_source.xtal_if) diff --git a/src/faebryk/library/RS485_Bus_Protection.py b/src/faebryk/library/RS485_Bus_Protection.py index 030f5dfd..621a05cc 100644 --- a/src/faebryk/library/RS485_Bus_Protection.py +++ b/src/faebryk/library/RS485_Bus_Protection.py @@ -105,8 +105,8 @@ def __preinit__(self): termination_resistor.resistance.merge( F.Range.from_center_rel(120 * P.ohm, 0.05) ) - self.rs485_ufp.diff_pair.p.connect_via( - termination_resistor, self.rs485_ufp.diff_pair.n + self.rs485_ufp.diff_pair.p.signal.connect_via( + termination_resistor, self.rs485_ufp.diff_pair.n.signal ) if self._polarization: polarization_resistors = self.add_to_container(2, F.Resistor) diff --git a/src/faebryk/library/SignalElectrical.py b/src/faebryk/library/SignalElectrical.py index fa28488b..07d242c7 100644 --- a/src/faebryk/library/SignalElectrical.py +++ b/src/faebryk/library/SignalElectrical.py @@ -18,9 +18,10 @@ def test(self, node: CNode): def __init__(self) -> None: super().__init__( - lambda src, dst: LinkDirectConditionalFilterResult.FILTER_PASS - if self.test(dst.node) - else LinkDirectConditionalFilterResult.FILTER_FAIL_UNRECOVERABLE + lambda path: LinkDirectConditionalFilterResult.FILTER_PASS + if all(self.test(dst.node) for dst in path) + else LinkDirectConditionalFilterResult.FILTER_FAIL_UNRECOVERABLE, + needs_only_first_in_path=False, ) # ---------------------------------------- diff --git a/src/faebryk/library/UART_Base.py b/src/faebryk/library/UART_Base.py index e6477593..92c16f25 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.util import cast_assert class UART_Base(ModuleInterface): @@ -18,7 +19,7 @@ def single_electric_reference(self): F.ElectricLogic.connect_all_module_references(self) ) - def _on_connect(self, other: "UART_Base"): - super()._on_connect(other) - - self.baud.merge(other.baud) + def __preinit__(self) -> None: + self.baud.add( + F.is_dynamic_by_connections(lambda mif: cast_assert(UART_Base, mif).baud) + ) diff --git a/src/faebryk/library/USB2_0_IF.py b/src/faebryk/library/USB2_0_IF.py index 0a892f70..7da0933b 100644 --- a/src/faebryk/library/USB2_0_IF.py +++ b/src/faebryk/library/USB2_0_IF.py @@ -9,6 +9,7 @@ class USB2_0_IF(ModuleInterface): class Data(F.DifferentialPair): + # FIXME: this should be in diffpair right? @L.rt_field def single_electric_reference(self): return F.has_single_electric_reference_defined( @@ -16,8 +17,9 @@ def single_electric_reference(self): ) def __preinit__(self): - self.p.reference.voltage.merge(F.Range(0 * P.V, 3.6 * P.V)) - self.n.reference.voltage.merge(F.Range(0 * P.V, 3.6 * P.V)) + self.single_electric_reference.get_reference().voltage.merge( + F.Range(0 * P.V, 3.6 * P.V) + ) d: Data buspower: F.ElectricPower diff --git a/src/faebryk/library/USB_RS485.py b/src/faebryk/library/USB_RS485.py index d15c9bf7..bb585106 100644 --- a/src/faebryk/library/USB_RS485.py +++ b/src/faebryk/library/USB_RS485.py @@ -31,8 +31,8 @@ def __preinit__(self): self.usb.usb_if.buspower.connect(self.usb_uart.usb.usb_if.buspower) # connect termination resistor between RS485 A and B - self.uart_rs485.rs485.diff_pair.n.connect_via( - self.termination, self.uart_rs485.rs485.diff_pair.p + self.uart_rs485.rs485.diff_pair.n.signal.connect_via( + self.termination, self.uart_rs485.rs485.diff_pair.p.signal ) # connect polarization resistors to RS485 A and B diff --git a/src/faebryk/library/_F.py b/src/faebryk/library/_F.py index 308fb51c..880cef3c 100644 --- a/src/faebryk/library/_F.py +++ b/src/faebryk/library/_F.py @@ -26,6 +26,7 @@ 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 diff --git a/src/faebryk/library/is_dynamic_by_connections.py b/src/faebryk/library/is_dynamic_by_connections.py new file mode 100644 index 00000000..678aec92 --- /dev/null +++ b/src/faebryk/library/is_dynamic_by_connections.py @@ -0,0 +1,68 @@ +# This file is part of the faebryk project +# SPDX-License-Identifier: MIT + +import logging +from typing import Callable + +from faebryk.core.moduleinterface import ModuleInterface +from faebryk.core.node import NodeException +from faebryk.core.parameter import Parameter +from faebryk.libs.util import cast_assert, not_none, once + +logger = logging.getLogger(__name__) + + +class is_dynamic_by_connections(Parameter.is_dynamic.impl()): + def __init__(self, key: Callable[[ModuleInterface], Parameter]) -> None: + super().__init__() + self._key = key + self._guard = False + self._merged: set[int] = set() + + @once + def mif_parent(self) -> ModuleInterface: + return cast_assert(ModuleInterface, not_none(self.obj.get_parent())[0]) + + def exec_for_mifs(self, mifs: set[ModuleInterface]): + if self._guard: + return + + mif_parent = self.mif_parent() + self_param = self.get_obj(Parameter) + if self._key(mif_parent) is not self_param: + raise NodeException(self, "Key not mapping to parameter") + + # only self + if len(mifs) == 1: + return + + params = [self._key(mif) for mif in mifs] + params_with_guard = [ + ( + param, + cast_assert( + is_dynamic_by_connections, param.get_trait(Parameter.is_dynamic) + ), + ) + for param in params + ] + + # Disable guards to prevent infinite recursion + for param, guard in params_with_guard: + guard._guard = True + guard._merged.add(id(self_param)) + + # Merge parameters + for param in params: + if id(param) in self._merged: + continue + self._merged.add(id(param)) + self_param.merge(param) + + # Enable guards again + for _, guard in params_with_guard: + guard._guard = False + + def exec(self): + mif_parent = self.mif_parent() + self.exec_for_mifs(set(mif_parent.get_connected())) diff --git a/src/faebryk/libs/app/erc.py b/src/faebryk/libs/app/erc.py index 058e768b..8295a9b6 100644 --- a/src/faebryk/libs/app/erc.py +++ b/src/faebryk/libs/app/erc.py @@ -24,8 +24,8 @@ def __init__(self, faulting_ifs: Sequence[ModuleInterface], *args: object) -> No class ERCFaultShort(ERCFault): def __init__(self, faulting_ifs: Sequence[ModuleInterface], *args: object) -> None: - link = faulting_ifs[0].is_connected_to(faulting_ifs[1]) - assert link + paths = faulting_ifs[0].is_connected_to(faulting_ifs[1]) + assert paths super().__init__(faulting_ifs, *args) diff --git a/src/faebryk/libs/app/parameters.py b/src/faebryk/libs/app/parameters.py index 1e19e0b8..7d8640a2 100644 --- a/src/faebryk/libs/app/parameters.py +++ b/src/faebryk/libs/app/parameters.py @@ -2,10 +2,18 @@ # SPDX-License-Identifier: MIT import logging +from typing import Iterable, cast + +from more_itertools import partition 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 +from faebryk.libs.util import find, groupby logger = logging.getLogger(__name__) @@ -33,3 +41,75 @@ def replace_tbd_with_any(module: Module, recursive: bool, loglvl: int | None = N 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 + ) + ], + ) + + # non-connection + for _, trait in other_dynamic_params: + trait.execute() + + # connection + _resolve_dynamic_parameters_connection( + cast( + list[tuple[Parameter, F.is_dynamic_by_connections]], + connection_dynamic_params, + ) + ) + + +def _resolve_dynamic_parameters_connection( + params: Iterable[tuple[Parameter, F.is_dynamic_by_connections]], +): + times = Times() + + busses: list[set[ModuleInterface]] = [] + + params_grouped_by_mif = groupby(params, lambda p: p[1].mif_parent()) + + # find for all busses a mif that represents it, and puts its dynamic params here + # we use the that connected mifs are the same type and thus have the same params + # TODO: limitation: specialization (need to subgroup by type) (see exception) + param_bus_representatives: set[tuple[Parameter, F.is_dynamic_by_connections]] = ( + set() + ) + + while params_grouped_by_mif: + bus_representative_mif, bus_representative_params = ( + params_grouped_by_mif.popitem() + ) + # expensive call + paths = bus_representative_mif.get_connected(include_self=True) + connections = set(paths.keys()) + + busses.append(connections) + if len(set(map(type, connections))) > 1: + raise NotImplementedError( + "No support for specialized bus with dynamic params" + ) + + for m in connections: + if m in params_grouped_by_mif: + del params_grouped_by_mif[m] + param_bus_representatives |= set(bus_representative_params) + + times.add("get parameter connections") + + # exec resolution + for _, trait in param_bus_representatives: + bus_representative_mif = trait.mif_parent() + param_bus = find(busses, lambda bus: bus_representative_mif in bus) + trait.exec_for_mifs(param_bus) + + times.add("merge parameters") + if logger.isEnabledFor(logging.DEBUG): + logger.debug(times) diff --git a/src/faebryk/libs/app/pcb.py b/src/faebryk/libs/app/pcb.py index bec525a8..9a6ed0c7 100644 --- a/src/faebryk/libs/app/pcb.py +++ b/src/faebryk/libs/app/pcb.py @@ -16,6 +16,7 @@ from faebryk.exporters.pcb.kicad.transformer import PCB_Transformer from faebryk.exporters.pcb.routing.util import apply_route_in_pcb from faebryk.libs.app.kicad_netlist import write_netlist +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.kicad.fileformats import ( C_kicad_fp_lib_table_file, C_kicad_pcb_file, @@ -74,6 +75,8 @@ def apply_design( app: Module, transform: Callable[[PCB_Transformer], Any] | None = None, ): + resolve_dynamic_parameters(G) + logger.info(f"Writing netlist to {netlist_path}") changed = write_netlist(G, netlist_path, use_kicad_designators=True) apply_netlist(pcb_path, netlist_path, changed) diff --git a/src/faebryk/libs/examples/buildutil.py b/src/faebryk/libs/examples/buildutil.py index 6be8ad97..362aa9e3 100644 --- a/src/faebryk/libs/examples/buildutil.py +++ b/src/faebryk/libs/examples/buildutil.py @@ -10,7 +10,7 @@ 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 +from faebryk.libs.app.parameters import replace_tbd_with_any, 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 @@ -57,6 +57,7 @@ def apply_design_to_pcb( ) G = m.get_graph() + resolve_dynamic_parameters(G) run_checks(m, G) # TODO this can be prettier diff --git a/src/faebryk/libs/header.py b/src/faebryk/libs/header.py index 85b45ff0..767c98e7 100644 --- a/src/faebryk/libs/header.py +++ b/src/faebryk/libs/header.py @@ -2,7 +2,13 @@ # SPDX-License-Identifier: MIT +import logging +import re + import black +import black.parsing + +logger = logging.getLogger(__name__) def get_header(): @@ -13,9 +19,22 @@ def get_header(): def formatted_file_contents(file_contents: str, is_pyi: bool = False) -> str: - return black.format_str( - file_contents, - mode=black.Mode( - is_pyi=is_pyi, - ), - ) + try: + return black.format_str( + file_contents, + mode=black.Mode( + is_pyi=is_pyi, + ), + ) + + except black.parsing.InvalidInput as e: + lineno, column = None, None + match = re.match(r"Cannot parse: (\d+):(\d+):", e.args[0]) + if match: + lineno, column = map(int, match.groups()) + with_line_numbers = "\n".join( + f"{'>>' if i+1==lineno else ' '}{i + 1:3d}: {line}" + for i, line in enumerate(file_contents.split("\n")) + ) + logger.warning("black failed to format file:\n" + with_line_numbers) + raise diff --git a/src/faebryk/libs/util.py b/src/faebryk/libs/util.py index e1d6b23e..849645b6 100644 --- a/src/faebryk/libs/util.py +++ b/src/faebryk/libs/util.py @@ -120,8 +120,8 @@ def __init__(self, duplicates: list[T], *args: object) -> None: self.duplicates = duplicates -def find[T](haystack: Iterable[T], needle: Callable[[T], bool]) -> T: - results = list(filter(needle, haystack)) +def find[T](haystack: Iterable[T], needle: Callable[[T], Any]) -> T: + results = [x for x in haystack if needle(x)] if not results: raise KeyErrorNotFound() if len(results) != 1: @@ -865,6 +865,9 @@ def __init__(self, name: str, default: int = 0, descr: str = "") -> None: def _convert(self, raw_val: str) -> int: return int(raw_val) + def __int__(self) -> int: + return self.get() + def zip_dicts_by_key(*dicts): keys = {k for d in dicts for k in d} diff --git a/test/core/cpp/test_importcpp.py b/test/core/cpp/test_importcpp.py index ea4636b0..e7d59773 100644 --- a/test/core/cpp/test_importcpp.py +++ b/test/core/cpp/test_importcpp.py @@ -155,7 +155,24 @@ def test_mif_link(): mif1 = ModuleInterface() mif2 = ModuleInterface() mif1.connect_shallow(mif2) - assert isinstance(mif1.is_connected_to(mif2), LinkDirectConditional) + paths = mif1.is_connected_to(mif2) + assert len(paths) == 1 + path = paths[0] + assert len(path) == 4 + assert isinstance(path[1].is_connected_to(path[2]), LinkDirectConditional) + + +def test_cpp_type(): + from faebryk.core.cpp import LinkDirect + + class LinkDirectDerived(LinkDirect): + pass + + obj = LinkDirect() + assert LinkDirect.is_cloneable(obj) + + obj2 = LinkDirectDerived() + assert not LinkDirect.is_cloneable(obj2) if __name__ == "__main__": diff --git a/test/core/test_core.py b/test/core/test_core.py index 19a66d43..f8cccef6 100644 --- a/test/core/test_core.py +++ b/test/core/test_core.py @@ -3,7 +3,20 @@ import unittest -from faebryk.core.link import LinkDirect, LinkParent, LinkSibling +from faebryk.core.cpp import ( + GraphInterface, + GraphInterfaceHierarchical, + LinkExists, + LinkNamedParent, +) +from faebryk.core.link import ( + LinkDirect, + LinkDirectConditional, + LinkDirectConditionalFilterResult, + LinkParent, + LinkSibling, +) +from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node from faebryk.libs.library import L @@ -29,8 +42,8 @@ class linkcls(LinkDirect): self.assertIsInstance(gif1.is_connected_to(gif3), linkcls) self.assertEqual(gif1.is_connected_to(gif3), gif3.is_connected_to(gif1)) - self.assertRaises(RuntimeError, lambda: gif1.connect(gif3)) - self.assertRaises(RuntimeError, lambda: gif1.connect(gif3, linkcls())) + self.assertRaises(LinkExists, lambda: gif1.connect(gif3)) + self.assertRaises(LinkExists, lambda: gif1.connect(gif3, linkcls())) self.assertEqual(gif1.G, gif2.G) @@ -97,6 +110,44 @@ def test_fab_ll_chain_tree(self): x.get_full_name(), "[*][0-9A-F]{4}.i0.i1.i2.i3.i4.i5.i6.i7.i8.i9" ) + def test_link_eq_direct(self): + gif1 = GraphInterface() + gif2 = GraphInterface() + + gif1.connect(gif2) + + self.assertEqual(gif1.is_connected_to(gif2), LinkDirect()) + self.assertNotEqual(gif1.is_connected_to(gif2), LinkSibling()) + + def test_link_eq_args(self): + gif1 = GraphInterfaceHierarchical(is_parent=True) + gif2 = GraphInterfaceHierarchical(is_parent=False) + + gif1.connect(gif2, link=LinkNamedParent("bla")) + + self.assertEqual(gif1.is_connected_to(gif2), LinkNamedParent("bla")) + self.assertNotEqual(gif1.is_connected_to(gif2), LinkNamedParent("blub")) + self.assertNotEqual(gif1.is_connected_to(gif2), LinkDirect()) + + def test_inherited_link(self): + class _Link(LinkDirectConditional): + def __init__(self): + super().__init__( + lambda path: LinkDirectConditionalFilterResult.FILTER_PASS + ) + + gif1 = GraphInterface() + gif2 = GraphInterface() + + gif1.connect(gif2, link=_Link()) + self.assertIsInstance(gif1.is_connected_to(gif2), _Link) + + def test_unique_mif_shallow_link(self): + class MIFType(ModuleInterface): + pass + + assert MIFType.LinkDirectShallow() is MIFType.LinkDirectShallow() + if __name__ == "__main__": unittest.main() diff --git a/test/core/test_util.py b/test/core/test_core_util.py similarity index 100% rename from test/core/test_util.py rename to test/core/test_core_util.py diff --git a/test/core/test_hierarchy_connect.py b/test/core/test_hierarchy_connect.py index f6291130..1c16a11a 100644 --- a/test/core/test_hierarchy_connect.py +++ b/test/core/test_hierarchy_connect.py @@ -2,288 +2,563 @@ # SPDX-License-Identifier: MIT import logging -import unittest from itertools import chain +import pytest + import faebryk.library._F as F -from faebryk.core.core import logger as core_logger from faebryk.core.link import ( LinkDirect, LinkDirectConditional, LinkDirectConditionalFilterResult, + LinkDirectDerived, ) from faebryk.core.module import Module -from faebryk.core.moduleinterface import ModuleInterface +from faebryk.core.moduleinterface import IMPLIED_PATHS, ModuleInterface +from faebryk.libs.app.erc import ERCPowerSourcesShortedError, simple_erc +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.library import L -from faebryk.libs.util import times +from faebryk.libs.util import cast_assert, times logger = logging.getLogger(__name__) -core_logger.setLevel(logger.getEffectiveLevel()) - - -class TestHierarchy(unittest.TestCase): - def test_up_connect(self): - class UARTBuffer(Module): - bus_in: F.UART_Base - bus_out: F.UART_Base - - def __preinit__(self) -> None: - bus_in = self.bus_in - bus_out = self.bus_out - - bus_in.rx.signal.connect(bus_out.rx.signal) - bus_in.tx.signal.connect(bus_out.tx.signal) - bus_in.rx.reference.connect(bus_out.rx.reference) - - app = UARTBuffer() - - self.assertTrue(app.bus_in.rx.is_connected_to(app.bus_out.rx)) - self.assertTrue(app.bus_in.tx.is_connected_to(app.bus_out.tx)) - self.assertTrue(app.bus_in.is_connected_to(app.bus_out)) - - def test_chains(self): - mifs = times(3, ModuleInterface) - mifs[0].connect(mifs[1]) - mifs[1].connect(mifs[2]) - self.assertTrue(mifs[0].is_connected_to(mifs[2])) - - mifs = times(3, ModuleInterface) - mifs[0].connect_shallow(mifs[1]) - mifs[1].connect_shallow(mifs[2]) - self.assertTrue(mifs[0].is_connected_to(mifs[2])) - self.assertIsInstance(mifs[0].is_connected_to(mifs[2]), LinkDirectConditional) - - mifs = times(3, ModuleInterface) - mifs[0].connect_shallow(mifs[1]) - mifs[1].connect(mifs[2]) - self.assertTrue(mifs[0].is_connected_to(mifs[2])) - self.assertIsInstance(mifs[0].is_connected_to(mifs[2]), LinkDirectConditional) - - # Test hierarchy down filter & chain resolution - mifs = times(3, F.ElectricLogic) - mifs[0].connect_shallow(mifs[1]) - mifs[1].connect(mifs[2]) - self.assertTrue(mifs[0].is_connected_to(mifs[2])) - self.assertIsInstance(mifs[0].is_connected_to(mifs[2]), LinkDirectConditional) - - self.assertTrue(mifs[1].signal.is_connected_to(mifs[2].signal)) - self.assertTrue(mifs[1].reference.is_connected_to(mifs[2].reference)) - self.assertFalse(mifs[0].signal.is_connected_to(mifs[1].signal)) - self.assertFalse(mifs[0].reference.is_connected_to(mifs[1].reference)) - self.assertFalse(mifs[0].signal.is_connected_to(mifs[2].signal)) - self.assertFalse(mifs[0].reference.is_connected_to(mifs[2].reference)) - - # Test duplicate resolution - mifs[0].signal.connect(mifs[1].signal) - mifs[0].reference.connect(mifs[1].reference) - self.assertIsInstance(mifs[0].is_connected_to(mifs[1]), LinkDirect) - self.assertIsInstance(mifs[0].is_connected_to(mifs[2]), LinkDirect) - - def test_bridge(self): - self_ = self - - # U1 ---> _________B________ ---> U2 - # TX IL ===> OL TX - # S --> I -> S S -> O --> S - # R -------- R ----- R -------- R - - class Buffer(Module): - ins = L.list_field(2, F.Electrical) - outs = L.list_field(2, F.Electrical) - - ins_l = L.list_field(2, F.ElectricLogic) - outs_l = L.list_field(2, F.ElectricLogic) - - def __preinit__(self) -> None: - self_.assertIs( - self.ins_l[0].reference, - self.ins_l[0].single_electric_reference.get_reference(), - ) - - for el, lo in chain( - zip(self.ins, self.ins_l), - zip(self.outs, self.outs_l), - ): - lo.signal.connect(el) - - for l1, l2 in zip(self.ins_l, self.outs_l): - l1.connect_shallow(l2) - - @L.rt_field - def single_electric_reference(self): - return F.has_single_electric_reference_defined( - F.ElectricLogic.connect_all_module_references(self) - ) - - class UARTBuffer(Module): - buf: Buffer - bus_in: F.UART_Base - bus_out: F.UART_Base - - def __preinit__(self) -> None: - bus1 = self.bus_in - bus2 = self.bus_out - buf = self.buf - - bus1.tx.signal.connect(buf.ins[0]) - bus1.rx.signal.connect(buf.ins[1]) - bus2.tx.signal.connect(buf.outs[0]) - bus2.rx.signal.connect(buf.outs[1]) - - @L.rt_field - def single_electric_reference(self): - return F.has_single_electric_reference_defined( - F.ElectricLogic.connect_all_module_references(self) - ) - - # Enable to see the stack trace of invalid connections - # c.LINK_TB = True - app = UARTBuffer() - - def _assert_no_link(mif1, mif2): - link = mif1.is_connected_to(mif2) - self.assertFalse(link) - - def _assert_link(mif1: ModuleInterface, mif2: ModuleInterface, link=None): - out = mif1.is_connected_to(mif2) - if link: - self.assertIsInstance(out, link) - return - self.assertIsNotNone(out) - - bus1 = app.bus_in - bus2 = app.bus_out - buf = app.buf - - # Check that the two buffer sides are not connected electrically - _assert_no_link(buf.ins[0], buf.outs[0]) - _assert_no_link(buf.ins[1], buf.outs[1]) - _assert_no_link(bus1.rx.signal, bus2.rx.signal) - _assert_no_link(bus1.tx.signal, bus2.tx.signal) - - # direct connect - _assert_link(bus1.tx.signal, buf.ins[0]) - _assert_link(bus1.rx.signal, buf.ins[1]) - _assert_link(bus2.tx.signal, buf.outs[0]) - _assert_link(bus2.rx.signal, buf.outs[1]) - - # connect through trait - self.assertIs( - buf.ins_l[0].single_electric_reference.get_reference(), - buf.ins_l[0].reference, - ) - _assert_link(buf.ins_l[0].reference, buf.outs_l[0].reference) - _assert_link(buf.outs_l[1].reference, buf.ins_l[0].reference) - _assert_link(bus1.rx.reference, bus2.rx.reference, LinkDirect) - - # connect through up - _assert_link(bus1.tx, buf.ins_l[0], LinkDirect) - _assert_link(bus2.tx, buf.outs_l[0], LinkDirect) - - # connect shallow - _assert_link(buf.ins_l[0], buf.outs_l[0], LinkDirectConditional) - - # Check that the two buffer sides are connected logically - _assert_link(bus1.tx, bus2.tx) - _assert_link(bus1.rx, bus2.rx) - _assert_link(bus1, bus2) - - def test_specialize(self): - class Specialized(ModuleInterface): ... - - # general connection -> specialized connection - mifs = times(3, ModuleInterface) - mifs_special = times(3, Specialized) - - mifs[0].connect(mifs[1]) - mifs[1].connect(mifs[2]) - - mifs[0].specialize(mifs_special[0]) - mifs[2].specialize(mifs_special[2]) - - self.assertTrue(mifs_special[0].is_connected_to(mifs_special[2])) - - # specialized connection -> general connection - mifs = times(3, ModuleInterface) - mifs_special = times(3, Specialized) - - mifs_special[0].connect(mifs_special[1]) - mifs_special[1].connect(mifs_special[2]) - - mifs[0].specialize(mifs_special[0]) - mifs[2].specialize(mifs_special[2]) - - self.assertTrue(mifs[0].is_connected_to(mifs[2])) - # test special link - class _Link(LinkDirectConditional): - def __init__(self): - super().__init__( - lambda src, dst: LinkDirectConditionalFilterResult.FILTER_PASS - ) - - mifs = times(3, ModuleInterface) - mifs_special = times(3, Specialized) - mifs[0].connect(mifs[1], linkcls=_Link) - mifs[1].connect(mifs[2]) +def test_self(): + mif = ModuleInterface() + assert mif.is_connected_to(mif) + + +def test_up_connect_simple_single(): + class High(ModuleInterface): + lower: ModuleInterface + + high1 = High() + high2 = High() + + high1.lower.connect(high2.lower) + assert high1.is_connected_to(high2) + + +def test_up_connect_simple_multiple(): + class High(ModuleInterface): + lower1: ModuleInterface + lower2: ModuleInterface + + high1 = High() + high2 = High() + + high1.lower1.connect(high2.lower1) + high1.lower2.connect(high2.lower2) + assert high1.is_connected_to(high2) + + +def test_up_connect_simple_multiple_negative(): + class High(ModuleInterface): + lower1: ModuleInterface + lower2: ModuleInterface + + high1 = High() + high2 = High() + + high1.lower1.connect(high2.lower1) + assert not high1.is_connected_to(high2) + + +def test_up_connect(): + class UARTBuffer(Module): + bus_in: F.UART_Base + bus_out: F.UART_Base + + def __preinit__(self) -> None: + self.bus_in.rx.signal.connect(self.bus_out.rx.signal) + self.bus_in.tx.signal.connect(self.bus_out.tx.signal) + self.bus_in.rx.reference.connect(self.bus_out.rx.reference) + + app = UARTBuffer() + + assert app.bus_in.rx.signal.is_connected_to(app.bus_out.rx.signal) + assert app.bus_in.rx.reference.is_connected_to(app.bus_out.rx.reference) + assert app.bus_in.rx.is_connected_to(app.bus_out.rx) + assert app.bus_in.tx.is_connected_to(app.bus_out.tx) + assert app.bus_in.is_connected_to(app.bus_out) + + +def test_down_connect(): + ep = times(2, F.ElectricPower) + ep[0].connect(ep[1]) + + assert ep[0].is_connected_to(ep[1]) + assert ep[0].hv.is_connected_to(ep[1].hv) + assert ep[0].lv.is_connected_to(ep[1].lv) + + +def test_chains_direct(): + mifs = times(3, ModuleInterface) + mifs[0].connect(mifs[1]) + mifs[1].connect(mifs[2]) + assert mifs[0].is_connected_to(mifs[2]) + + +def test_chains_double_shallow_flat(): + mifs = times(3, ModuleInterface) + mifs[0].connect_shallow(mifs[1]) + mifs[1].connect_shallow(mifs[2]) + assert mifs[0].is_connected_to(mifs[2]) + + +def test_chains_mixed_shallow_flat(): + mifs = times(3, ModuleInterface) + mifs[0].connect_shallow(mifs[1]) + mifs[1].connect(mifs[2]) + assert mifs[0].is_connected_to(mifs[2]) + + +def test_chains_mixed_shallow_nested(): + # Test hierarchy down filter & chain resolution + el = times(3, F.ElectricLogic) + el[0].connect_shallow(el[1]) + el[1].connect(el[2]) + assert el[0].is_connected_to(el[2]) + + assert el[1].signal.is_connected_to(el[2].signal) + assert el[1].reference.is_connected_to(el[2].reference) + assert not el[0].signal.is_connected_to(el[1].signal) + assert not el[0].reference.is_connected_to(el[1].reference) + assert not el[0].signal.is_connected_to(el[2].signal) + assert not el[0].reference.is_connected_to(el[2].reference) + + # Test duplicate resolution + el[0].signal.connect(el[1].signal) + el[0].reference.connect(el[1].reference) + assert el[0].is_connected_to(el[1]) + assert el[0].is_connected_to(el[2]) + + +def test_shallow_bridge_simple(): + class Low(ModuleInterface): ... + + class High(ModuleInterface): + lower1: Low + lower2: Low + + class ShallowBridge(Module): + high_in: High + high_out: High + + def __preinit__(self) -> None: + self.high_in.connect_shallow(self.high_out) + + @L.rt_field + def can_bridge(self): + return F.can_bridge_defined(self.high_in, self.high_out) + + bridge = ShallowBridge() + high1 = High() + high2 = High() + high1.connect_via(bridge, high2) + + assert high1.is_connected_to(high2) + assert not bridge.high_in.lower1.is_connected_to(bridge.high_out.lower1) + assert not bridge.high_in.lower2.is_connected_to(bridge.high_out.lower2) + assert not high1.lower1.is_connected_to(high2.lower1) + assert not high1.lower2.is_connected_to(high2.lower2) + + +def test_shallow_bridge(): + """ + Test the bridge connection between two UART interfaces through a buffer: + + ``` + U1 ---> _________B________ ---> U2 + TX IL ===> OL TX + S --> I -> S S -> O --> S + R -------- R ----- R -------- R + ``` + + Where: + - U1, U2: UART interfaces + - B: Buffer + - TX: Transmit + - S: Signal + - R: Reference + - I: Input + - O: Output + - IL: Input Logic + - OL: Output Logic + """ + + class Buffer(Module): + ins = L.list_field(2, F.Electrical) + outs = L.list_field(2, F.Electrical) + + ins_l = L.list_field(2, F.ElectricLogic) + outs_l = L.list_field(2, F.ElectricLogic) + + def __preinit__(self) -> None: + assert ( + self.ins_l[0].reference + is self.ins_l[0].single_electric_reference.get_reference() + ) + + for el, lo in chain( + zip(self.ins, self.ins_l), + zip(self.outs, self.outs_l), + ): + lo.signal.connect(el) + + for l1, l2 in zip(self.ins_l, self.outs_l): + l1.connect_shallow(l2) + + @L.rt_field + def single_electric_reference(self): + return F.has_single_electric_reference_defined( + F.ElectricLogic.connect_all_module_references(self) + ) + + class UARTBuffer(Module): + buf: Buffer + bus_in: F.UART_Base + bus_out: F.UART_Base + + def __preinit__(self) -> None: + bus_i = self.bus_in + bus_o = self.bus_out + buf = self.buf + + bus_i.tx.signal.connect(buf.ins[0]) + bus_i.rx.signal.connect(buf.ins[1]) + bus_o.tx.signal.connect(buf.outs[0]) + bus_o.rx.signal.connect(buf.outs[1]) + + @L.rt_field + def single_electric_reference(self): + return F.has_single_electric_reference_defined( + F.ElectricLogic.connect_all_module_references(self) + ) + + app = UARTBuffer() + + bus_i = app.bus_in + bus_o = app.bus_out + buf = app.buf + + # Check that the two buffer sides are not connected electrically + assert not buf.ins[0].is_connected_to(buf.outs[0]) + assert not buf.ins[1].is_connected_to(buf.outs[1]) + assert not bus_i.rx.signal.is_connected_to(bus_o.rx.signal) + assert not bus_i.tx.signal.is_connected_to(bus_o.tx.signal) + + # direct connect + assert bus_i.tx.signal.is_connected_to(buf.ins[0]) + assert bus_i.rx.signal.is_connected_to(buf.ins[1]) + assert bus_o.tx.signal.is_connected_to(buf.outs[0]) + assert bus_o.rx.signal.is_connected_to(buf.outs[1]) + + # connect through trait + assert ( + buf.ins_l[0].single_electric_reference.get_reference() is buf.ins_l[0].reference + ) + assert buf.ins_l[0].reference.is_connected_to(buf.outs_l[0].reference) + assert buf.outs_l[1].reference.is_connected_to(buf.ins_l[0].reference) + assert bus_i.rx.reference.is_connected_to(bus_o.rx.reference) + + # connect through up + assert bus_i.tx.is_connected_to(buf.ins_l[0]) + assert bus_o.tx.is_connected_to(buf.outs_l[0]) + + # connect shallow + assert buf.ins_l[0].is_connected_to(buf.outs_l[0]) + + # Check that the two buffer sides are connected logically + assert bus_i.tx.is_connected_to(bus_o.tx) + assert bus_i.rx.is_connected_to(bus_o.rx) + assert bus_i.is_connected_to(bus_o) + + +class Specialized(ModuleInterface): ... + + +class DoubleSpecialized(Specialized): ... + + +def test_specialize_general_to_special(): + # general connection -> specialized connection + mifs = times(3, ModuleInterface) + mifs_special = times(3, Specialized) + + mifs[0].connect(mifs[1]) + mifs[1].connect(mifs[2]) + + mifs[0].specialize(mifs_special[0]) + mifs[2].specialize(mifs_special[2]) + + assert mifs_special[0].is_connected_to(mifs_special[2]) + + +def test_specialize_special_to_general(): + # specialized connection -> general connection + mifs = times(3, ModuleInterface) + mifs_special = times(3, Specialized) + + mifs_special[0].connect(mifs_special[1]) + mifs_special[1].connect(mifs_special[2]) + + mifs[0].specialize(mifs_special[0]) + mifs[2].specialize(mifs_special[2]) + + assert mifs[0].is_connected_to(mifs[2]) + + +def test_specialize_link(): + # test special link + class _Link(LinkDirectConditional): + def __init__(self): + super().__init__(lambda path: LinkDirectConditionalFilterResult.FILTER_PASS) + + mifs = times(3, ModuleInterface) + mifs_special = times(3, Specialized) + + mifs[0].connect(mifs[1], link=_Link) + mifs[1].connect(mifs[2]) + + mifs[0].specialize(mifs_special[0]) + mifs[2].specialize(mifs_special[2]) + + assert mifs_special[0].is_connected_to(mifs_special[2]) + + +def test_specialize_double_with_gap(): + # double specialization with gap + mifs = times(2, ModuleInterface) + mifs_special = times(1, Specialized) + mifs_double_special = times(2, DoubleSpecialized) + + mifs[0].connect(mifs[1]) + mifs[0].specialize(mifs_special[0]) + mifs_special[0].specialize(mifs_double_special[0]) + mifs[1].specialize(mifs_double_special[1]) + + assert mifs_double_special[0].is_connected_to(mifs_double_special[1]) + + +def test_specialize_double_with_gap_2(): + mifs = times(2, ModuleInterface) + mifs_special = times(1, Specialized) + mifs_double_special = times(2, DoubleSpecialized) + + mifs_double_special[0].connect(mifs_double_special[1]) + mifs[0].specialize(mifs_special[0]) + mifs_special[0].specialize(mifs_double_special[0]) + mifs[1].specialize(mifs_double_special[1]) + + assert mifs[0].is_connected_to(mifs[1]) + + +def test_specialize_module(): + battery = F.Battery() + power = F.ElectricPower() + + battery.power.connect(power) + buttoncell = battery.specialize(F.ButtonCell()) + + assert buttoncell.power.is_connected_to(battery.power) + assert power.is_connected_to(buttoncell.power) + + +def test_isolated_connect_simple(): + x1 = F.ElectricLogic() + x2 = F.ElectricLogic() + x1.connect(x2, link=F.ElectricLogic.LinkIsolatedReference) + + assert x1.is_connected_to(x2) + assert x1.signal.is_connected_to(x2.signal) + + assert not x1.reference.is_connected_to(x2.reference) + assert not x1.reference.hv.is_connected_to(x2.reference.hv) + + +def test_isolated_connect_erc(): + y1 = F.ElectricPower() + y2 = F.ElectricPower() + + y1.make_source() + y2.make_source() + + with pytest.raises(ERCPowerSourcesShortedError): + y1.connect(y2) + simple_erc(y1.get_graph()) + + ldo1 = F.LDO() + ldo2 = F.LDO() + + with pytest.raises(ERCPowerSourcesShortedError): + ldo1.power_out.connect(ldo2.power_out) + simple_erc(ldo1.get_graph()) + + a1 = F.I2C() + b1 = F.I2C() + + a1.connect(b1, link=F.ElectricLogic.LinkIsolatedReference) + assert a1.is_connected_to(b1) + assert a1.scl.signal.is_connected_to(b1.scl.signal) + assert a1.sda.signal.is_connected_to(b1.sda.signal) + + assert not a1.scl.reference.is_connected_to(b1.scl.reference) + assert not a1.sda.reference.is_connected_to(b1.sda.reference) + + +@pytest.mark.skipif(not IMPLIED_PATHS, reason="IMPLIED_PATHS is not set") +def test_direct_implied_paths(): + powers = times(2, F.ElectricPower) + + # direct implied + powers[0].connect(powers[1]) + + assert powers[1].hv in powers[0].hv.get_connected() + + paths = powers[0].hv.is_connected_to(powers[1].hv) + assert paths + path = paths[0] + assert len(path) == 4 + assert isinstance(path[1].is_connected_to(path[2]), LinkDirectDerived) + + +@pytest.mark.skipif(not IMPLIED_PATHS, reason="IMPLIED_PATHS is not set") +def test_children_implied_paths(): + powers = times(3, F.ElectricPower) + + # children implied + powers[0].connect(powers[1]) + powers[1].hv.connect(powers[2].hv) + powers[1].lv.connect(powers[2].lv) + + assert powers[2] in powers[0].get_connected() + + paths = list(powers[0].is_connected_to(powers[2])) + assert paths + assert len(paths[0]) == 4 + assert isinstance(paths[0][1].is_connected_to(paths[0][2]), LinkDirectDerived) + + +@pytest.mark.skipif(not IMPLIED_PATHS, reason="IMPLIED_PATHS is not set") +def test_shallow_implied_paths(): + powers = times(4, F.ElectricPower) + + # shallow implied + powers[0].connect(powers[1]) + powers[1].hv.connect(powers[2].hv) + powers[1].lv.connect(powers[2].lv) + powers[2].connect_shallow(powers[3]) + + assert powers[3] in powers[0].get_connected() + + assert not powers[0].hv.is_connected_to(powers[3].hv) + + +def test_direct_shallow_instance(): + class MIFType(ModuleInterface): + pass - mifs[0].specialize(mifs_special[0]) - mifs[2].specialize(mifs_special[2]) + mif1 = MIFType() + mif2 = MIFType() + mif3 = MIFType() - self.assertIsInstance(mifs_special[0].is_connected_to(mifs_special[2]), _Link) + mif1.connect_shallow(mif2, mif3) + assert isinstance( + mif1.connected.is_connected_to(mif2.connected), MIFType.LinkDirectShallow() + ) + assert isinstance( + mif1.connected.is_connected_to(mif3.connected), MIFType.LinkDirectShallow() + ) - def test_isolated_connect(self): - x1 = F.ElectricLogic() - x2 = F.ElectricLogic() - x1.connect(x2, linkcls=F.ElectricLogic.LinkIsolatedReference) - self.assertIsInstance( - x1.is_connected_to(x2), F.ElectricLogic.LinkIsolatedReference - ) - self.assertIsInstance( - x1.signal.is_connected_to(x2.signal), - F.ElectricLogic.LinkIsolatedReference, - ) +def test_regression_rp2040_usb_diffpair_minimal(): + usb = F.USB2_0_IF.Data() + terminated_usb = usb.terminated() - self.assertIsNone(x1.reference.is_connected_to(x2.reference)) + other_usb = F.USB2_0_IF.Data() + terminated_usb.connect(other_usb) - self.assertIsNone(x1.reference.hv.is_connected_to(x2.reference.hv)) + n_ref = usb.n.reference + p_ref = usb.p.reference + t_n_ref = terminated_usb.n.reference + t_p_ref = terminated_usb.p.reference + o_n_ref = other_usb.n.reference + o_p_ref = other_usb.p.reference + refs = {n_ref, p_ref, t_n_ref, t_p_ref, o_n_ref, o_p_ref} - y1 = F.ElectricPower() - y2 = F.ElectricPower() + assert isinstance( + usb.connected.is_connected_to(terminated_usb.connected), + F.USB2_0_IF.Data.LinkDirectShallow(), + ) + assert isinstance( + other_usb.connected.is_connected_to(terminated_usb.connected), LinkDirect + ) + assert usb.connected.is_connected_to(other_usb.connected) is None - y1.make_source() - y2.make_source() + connected_per_mif = {ref: ref.get_connected(include_self=True) for ref in refs} - with self.assertRaises(F.Power.PowerSourcesShortedError): - y1.connect(y2) + assert not {n_ref, p_ref} & connected_per_mif[t_n_ref].keys() + assert not {n_ref, p_ref} & connected_per_mif[t_p_ref].keys() + assert not {t_n_ref, t_p_ref} & connected_per_mif[n_ref].keys() + assert not {t_n_ref, t_p_ref} & connected_per_mif[p_ref].keys() - ldo1 = F.LDO() - ldo2 = F.LDO() + assert set(connected_per_mif[n_ref].keys()) == {n_ref, p_ref} + assert set(connected_per_mif[p_ref].keys()) == {n_ref, p_ref} + assert set(connected_per_mif[t_n_ref].keys()) == { + t_n_ref, + t_p_ref, + o_n_ref, + o_p_ref, + } + assert set(connected_per_mif[t_p_ref].keys()) == { + t_n_ref, + t_p_ref, + o_n_ref, + o_p_ref, + } - with self.assertRaises(F.Power.PowerSourcesShortedError): - ldo1.power_out.connect(ldo2.power_out) + # close references + p_ref.connect(other_usb.p.reference) + + connected_per_mif_post = {ref: ref.get_connected(include_self=True) for ref in refs} + for _, connected in connected_per_mif_post.items(): + assert set(connected.keys()).issuperset(refs) - a1 = F.I2C() - b1 = F.I2C() - a1.connect(b1, linkcls=F.ElectricLogic.LinkIsolatedReference) - self.assertIsInstance( - a1.is_connected_to(b1), F.ElectricLogic.LinkIsolatedReference - ) - self.assertIsInstance( - a1.scl.signal.is_connected_to(b1.scl.signal), - F.ElectricLogic.LinkIsolatedReference, - ) - self.assertIsInstance( - a1.sda.signal.is_connected_to(b1.sda.signal), - F.ElectricLogic.LinkIsolatedReference, - ) +def test_regression_rp2040_usb_diffpair(): + app = F.RP2040_ReferenceDesign() + + terminated_usb = cast_assert(F.USB2_0_IF.Data, app.runtime["_terminated_usb_data"]) + rp_usb = app.rp2040.usb - self.assertIsNone(a1.scl.reference.is_connected_to(b1.scl.reference)) - self.assertIsNone(a1.sda.reference.is_connected_to(b1.sda.reference)) + t_p_ref = terminated_usb.p.reference + t_n_ref = terminated_usb.n.reference + r_p_ref = rp_usb.p.reference + r_n_ref = rp_usb.n.reference + refs = [ + r_p_ref, + r_n_ref, + t_p_ref, + t_n_ref, + ] + + connected_per_mif = {ref: ref.get_connected(include_self=True) for ref in refs} + for connected in connected_per_mif.values(): + assert set(connected.keys()) == set(refs) + + +def test_regression_rp2040_usb_diffpair_full(): + app = F.RP2040_ReferenceDesign() + rp2040_2 = F.RP2040() + rp2040_3 = F.RP2040() + + # make graph bigger + app.rp2040.i2c[0].connect(rp2040_2.i2c[0]) + app.rp2040.i2c[0].connect(rp2040_3.i2c[0]) + + resolve_dynamic_parameters(app.get_graph()) if __name__ == "__main__": - unittest.main() + test_regression_rp2040_usb_diffpair() diff --git a/test/core/test_parameters.py b/test/core/test_parameters.py index a8e0d569..c6c4f026 100644 --- a/test/core/test_parameters.py +++ b/test/core/test_parameters.py @@ -9,6 +9,7 @@ from faebryk.core.core import logger as core_logger from faebryk.core.module import Module from faebryk.core.parameter import Parameter +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.units import P logger = logging.getLogger(__name__) @@ -247,11 +248,6 @@ def test_comp( test_comp(F.Constant(F.Set([F.Range(F.Range(1))])), 1) def test_modules(self): - def assertIsInstance[T](obj, cls: type[T]) -> T: - self.assertIsInstance(obj, cls) - assert isinstance(obj, cls) - return obj - class Modules(Module): UART_A: F.UART_Base UART_B: F.UART_Base @@ -267,24 +263,21 @@ class Modules(Module): UART_A.baud.merge(F.Constant(9600 * P.baud)) + resolve_dynamic_parameters(m.get_graph()) + for uart in [UART_A, UART_B]: - self.assertEqual( - assertIsInstance(uart.baud.get_most_narrow(), F.Constant).value, - 9600 * P.baud, - ) + 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( - assertIsInstance(uart.baud.get_most_narrow(), F.Constant).value, - 9600 * P.baud, - ) + self.assertEqual(uart.baud.get_most_narrow(), 9600 * P.baud) resistor = F.Resistor() - assertIsInstance( + self.assertIsInstance( resistor.get_current_flow_by_voltage_resistance(F.Constant(0.5)), F.Operation, ) @@ -324,43 +317,43 @@ def test_specialize(self): import faebryk.library._F as F from faebryk.libs.brightness import TypicalLuminousIntensity - for i in range(10): - - class App(Module): - led: F.PoweredLED - battery: F.Battery - - 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( - TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value - ) - - 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) - - 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) - - 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(Module): + led: F.PoweredLED + battery: F.Battery + + 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( + TypicalLuminousIntensity.APPLICATION_LED_INDICATOR_INSIDE.value.value + ) + + 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) + + 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()) + + 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)}") def test_units(self): self.assertEqual(F.Constant(1e-9 * P.F), 1 * P.nF) diff --git a/test/core/test_performance.py b/test/core/test_performance.py index 727a5f16..ef0fbfa9 100644 --- a/test/core/test_performance.py +++ b/test/core/test_performance.py @@ -12,6 +12,7 @@ from faebryk.core.module import Module from faebryk.core.moduleinterface import ModuleInterface from faebryk.core.node import Node +from faebryk.libs.app.parameters import resolve_dynamic_parameters from faebryk.libs.library import L from faebryk.libs.test.times import Times from faebryk.libs.util import times @@ -237,13 +238,16 @@ def test_mif_connect_hull(self): def test_complex_module(self): timings = Times() - modules = [F.USB2514B, F.RP2040] + modules = [ + F.USB2514B, + F.RP2040, + ] for t in modules: app = t() # noqa: F841 timings.add(f"{t.__name__}: construct") - # resolve_dynamic_parameters(app.get_graph()) + resolve_dynamic_parameters(app.get_graph()) timings.add(f"{t.__name__}: resolve") logger.info(f"\n{timings}") diff --git a/test/exporters/pcb/kicad/test_transformer.py b/test/exporters/pcb/kicad/test_pcb_transformer.py similarity index 100% rename from test/exporters/pcb/kicad/test_transformer.py rename to test/exporters/pcb/kicad/test_pcb_transformer.py diff --git a/test/exporters/schematic/kicad/test_transformer.py b/test/exporters/schematic/kicad/test_schematic_transformer.py similarity index 100% rename from test/exporters/schematic/kicad/test_transformer.py rename to test/exporters/schematic/kicad/test_schematic_transformer.py diff --git a/test/library/nodes/test_electricpower.py b/test/library/nodes/test_electricpower.py index d41f19ee..972098bf 100644 --- a/test/library/nodes/test_electricpower.py +++ b/test/library/nodes/test_electricpower.py @@ -2,13 +2,16 @@ # 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 class TestFusedPower(unittest.TestCase): def test_fused_power(self): - import faebryk.library._F as F - from faebryk.libs.units import P - power_in = F.ElectricPower() power_out = F.ElectricPower() @@ -20,6 +23,7 @@ def test_fused_power(self): 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()) self.assertEqual(fuse.trip_current.get_most_narrow(), F.Constant(500 * P.mA)) self.assertEqual(power_out.voltage.get_most_narrow(), 10 * P.V) @@ -27,3 +31,20 @@ def test_fused_power(self): # 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()) + + def test_voltage_propagation(self): + powers = times(4, F.ElectricPower) + + powers[0].voltage.merge(F.Range(10 * P.V, 15 * P.V)) + + for p1, p2 in pairwise(powers): + p1.connect(p2) + + resolve_dynamic_parameters(powers[0].get_graph()) + + self.assertEqual( + powers[-1].voltage.get_most_narrow(), F.Range(10 * P.V, 15 * P.V) + ) + + powers[3].voltage.merge(10 * P.V) + self.assertEqual(powers[0].voltage.get_most_narrow(), 10 * P.V) diff --git a/test/library/test_basic.py b/test/library/test_instance_library_modules.py similarity index 98% rename from test/library/test_basic.py rename to test/library/test_instance_library_modules.py index fb071af1..dc5415ed 100644 --- a/test/library/test_basic.py +++ b/test/library/test_instance_library_modules.py @@ -50,7 +50,7 @@ def test_symbol_types(name: str, module): ) ], ) -@pytest.mark.timeout(60) # TODO lower +# @pytest.mark.timeout(60) # TODO lower def test_init_args(name: str, module): """Make sure we can instantiate all classes without error""" diff --git a/test/libs/geometry/test_basic.py b/test/libs/geometry/test_basic_transformations.py similarity index 100% rename from test/libs/geometry/test_basic.py rename to test/libs/geometry/test_basic_transformations.py