diff --git a/pyproject.toml b/pyproject.toml index 42ebf2ce4..d6d6d4c7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,7 @@ dependencies = [ "distinctipy>=1.2.2", "plotly>=5.15.0", "networkx==2.5", + "walkerlayout==1.0.2", "ipywidgets>=7.6.5", ] dynamic = ["version"] @@ -164,7 +165,7 @@ explicit_package_bases = true pretty = true [[tool.mypy.overrides]] -module = ["qiskit.*", "matplotlib.*"] +module = ["qiskit.*", "matplotlib.*", "networkx.*", "plotly.*", "_plotly_utils.*", "distinctipy.*", "ipywidgets.*", "walkerlayout.*"] ignore_missing_imports = true diff --git a/src/heuristic/HeuristicMapper.cpp b/src/heuristic/HeuristicMapper.cpp index b56caccc7..d58552fb1 100644 --- a/src/heuristic/HeuristicMapper.cpp +++ b/src/heuristic/HeuristicMapper.cpp @@ -1009,7 +1009,7 @@ void HeuristicMapper::Node::recalculateFixedCost( validMappedTwoQubitGates.emplace(q1, q2); } } - // 2-qubit-gates not yet mapped next to eachother are handled in the + // 2-qubit-gates not yet mapped next to each other are handled in the // heuristic } else { for (auto& swapNode : swaps) { diff --git a/src/mqt/qmap/visualization/treedraw.py b/src/mqt/qmap/visualization/treedraw.py deleted file mode 100644 index 1d6b02376..000000000 --- a/src/mqt/qmap/visualization/treedraw.py +++ /dev/null @@ -1,341 +0,0 @@ -"""treedraw.py -===========. - -Adapted from: -https://github.com/cvzi/py_treedraw - -This module implements a layout algorithm for a tree. -Each node in the tree has a layout property that contains an x and y -coordinate. -The layout is calculated by calling the method "walker(distance)". -The distance property indicated the distance between nodes on the same level. -If the tree is build up using the addChild/removeChild methods, the layout -will be calculated in linear time. -The algoithm is a python implemenation of this publication - "Improving -Walker's Algorithm to Run in Linear Time" by Christoph Buchheim, Michael -Junger, Sebastian Leipert" -""" -from __future__ import annotations - -__all__ = ["Tree", "Node"] - -__version__ = "1.4" - -import math - -try: - xrange(5) - myrange = xrange -except NameError: - myrange = range - - -class Tree: - def __init__(self, data) -> None: - # Start a tree with a root node - self.root = Node(data) - self.root.tree = self - self.root.leftSibling = None - self.nodes = [self.root] - - def addNode(self, node): - # add a child to the root - return self.root.addNode(node) - - def addChild(self, data): - # add a child to the root - return self.root.addChild(data) - - def removeChild(self, node): - # remove a child from the root - return self.root.removeChild(node) - - def walker(self, distance=1.0): - # Init layout algorithm - self._firstWalk(self.root, distance) - self._secondWalk(self.root, -self.root.layout.prelim) - - def _add(self, node): - if node not in self.nodes: - self.nodes.append(node) - return node - - def _firstWalk(self, v, distance): - if v.children: - defaultAncestor = v.children[0] - for w in v.children: - self._firstWalk(w, distance) - self._apportion(w, defaultAncestor, distance) - self._executeShifts(v) - midpoint = 0.5 * (v.children[0].layout.prelim + v.children[-1].layout.prelim) - w = self._leftSibling(v) - if w is not None: - v.layout.prelim = w.layout.prelim + distance - v.layout.mod = v.layout.prelim - midpoint - else: - v.layout.prelim = midpoint - else: - ls = self._leftSibling(v) - if ls: - v.layout.prelim = ls.layout.prelim + distance - - def _secondWalk(self, v, m): - v.layout.x(v.layout.prelim + m) - for w in v.children: - self._secondWalk(w, m + v.layout.mod) - - def _apportion(self, v, defaultAncestor, distance): - w = self._leftSibling(v) - if w is not None: - v_p_o = v - v_p_i = v - v_m_i = w - v_m_o = v_p_i.parent.children[0] - s_p_i = v_p_i.layout.mod - s_p_o = v_p_o.layout.mod - s_m_i = v_m_i.layout.mod - s_m_o = v_m_o.layout.mod - while v_m_i.nextRight() and v_p_i.nextLeft(): - v_m_i = v_m_i.nextRight() - v_p_i = v_p_i.nextLeft() - v_m_o = v_m_o.nextLeft() - v_p_o = v_p_o.nextRight() - v_p_o.layout.ancestor = v - shift = v_m_i.layout.prelim + s_m_i - (v_p_i.layout.prelim + s_p_i) + distance - if shift > 0: - self._moveSubtree(self._ancestor(v_m_i, v, defaultAncestor), v, shift) - s_p_i = s_p_i + shift - s_p_o = s_p_o + shift - s_m_i = s_m_i + v_m_i.layout.mod - s_p_i = s_p_i + v_p_i.layout.mod - s_m_o = s_m_o + v_m_o.layout.mod - s_p_o = s_p_o + v_p_o.layout.mod - if v_m_i.nextRight() and v_p_o.nextRight() is None: - v_p_o.layout.thread = v_m_i.nextRight() - v_p_o.layout.mod = v_p_o.layout.mod + s_m_i - s_p_o - if v_p_i.nextLeft() and v_m_o.nextLeft() is None: - v_m_o.layout.thread = v_p_i.nextLeft() - v_m_o.layout.mod = v_m_o.layout.mod + s_p_i - s_m_o - defaultAncestor = v - return defaultAncestor - - @staticmethod - def _leftSibling(v): - if v.leftSibling != -1: - return v.leftSibling - else: - if v.parent is None or not v.parent.children: - return None - last = None - for w in v.parent.children: - if w == v: - if last is not None: - return last - return None - last = w - return None - - @staticmethod - def _moveSubtree(w_m, w_p, shift): - subtrees = w_p.number() - w_m.number() - if subtrees == 0: - subtrees = 0.0000001 - w_p.layout.change = w_p.layout.change - shift / subtrees - w_p.layout.shift = w_p.layout.shift + shift - w_m.layout.change = w_m.layout.change + shift / subtrees - w_p.layout.prelim = w_p.layout.prelim + shift - w_p.layout.mod = w_p.layout.mod + shift - - @staticmethod - def _executeShifts(v): - shift = 0 - change = 0 - i = len(v.children) - for i in myrange(len(v.children) - 1, -1, -1): - w = v.children[i] - w.layout.prelim = w.layout.prelim + shift - w.layout.mod = w.layout.mod + shift - change = change + w.layout.change - shift = shift + w.layout.shift + change - - @staticmethod - def _ancestor(v_i, v, defaultAncestor): - if v_i.layout.ancestor.parent == v.parent: - return v_i.layout.ancestor - return defaultAncestor - - -class Node: - class Layout: - def __init__(self, v) -> None: - self.node = v - self.mod = 0 - self.thread = None - self.ancestor = v - self.prelim = 0 - self.shift = 0 - self.change = 0 - self.pos = [None, None] - self.number = -1 # undefined - - def x(self, value=None): - if value is not None: - self.pos[0] = value - return None - else: - return self.pos[0] - - def y(self, value=None): - if value is not None: - self.pos[1] = value - return None - else: - if self.pos[1] is None: - self.pos[1] = self.node.level() - return self.pos[1] - - def __init__(self, data) -> None: - self.tree = None - self.data = data - self.leftSibling = -1 # undefined, outdated - self.children = [] - self.parent = None - self.layout = self.Layout(self) - - def addNode(self, node): - # Add an existing tree/node as a child - - # Set left sibling - if self.children: - node.leftSibling = self.children[-1] - node.layout.number = node.leftSibling.layout.number + 1 - else: - node.leftSibling = None - node.layout.number = 0 - - # Append to node - node.parent = self - self.children.append(node) - - # Add to tree - root = self - i = 0 - while root.parent is not None: - root = root.parent - i += 1 - root.tree._add(node) - node.tree = root.tree - self.layout.pos[1] = i # Level - return node - - def addChild(self, data): - # Create a new node and add it as a child - return self.addNode(Node(data)) - - def removeChild(self, v): - j = -1 - for i in myrange(len(self.children)): - if self.children[i] == v: - del self.children[i] - j = i - break - - for i in myrange(len(self.tree.nodes)): - if self.tree.nodes[i] == v: - del self.tree.nodes[i] - break - - # Update left sibling - if j == 0: - self.children[0].leftSibling = None - elif j > 0: - self.children[j].leftSibling = self.children[j - 1] - else: # j == -1 - return - - # Update numbers - for i in myrange(j, len(self.children)): - self.children[i].layout.number = i - - # Remove children of the deleted node - i = 0 - while i < len(self.tree.nodes): - if self.tree.nodes[i] in v.children: - del self.tree.nodes[i] - else: - i += 1 - - v.children = [] - - def nextLeft(self): - if self.children: - return self.children[0] - else: - return self.layout.thread - - def nextRight(self): - if self.children: - return self.children[-1] - else: - return self.layout.thread - - def level(self): - if self.layout.pos[1] is not None: - return self.layout.pos[1] - n = self.parent - i = 0 - while n is not None: - n = n.parent - i += 1 - return i - - def position(self, origin=(0, 0), scalex=1.0, scaley=None): - """Return position as integer - Examples: - position(origin) - position(origin, 10) - position(origin, 10, 15) - position(origin, (10, 15)). - """ - if scaley is None: - if hasattr(scalex, "__getitem__"): - scaley = scalex[1] - scalex = scalex[0] - else: - scaley = scalex - return ( - origin[0] + int(math.ceil(self.layout.x() * scalex)), - origin[1] + int(math.ceil(self.layout.y() * scaley)), - ) - - def positionf(self, origin=(0.0, 0.0), scalex=1.0, scaley=None): - """Return position as floating point - Examples: - position(origin) - position(origin, 10) - position(origin, 10, 15) - position(origin, (10, 15)). - """ - if scaley is None: - if hasattr(scalex, "__getitem__"): - scaley = scalex[1] - scalex = scalex[0] - else: - scaley = scalex - return (origin[0] + (self.layout.x() * scalex), origin[1] + (self.layout.y() * scaley)) - - def number(self): - if self.layout.number != -1: - return self.layout.number - else: - if self.parent is None: - return 0 - else: - i = 0 - for node in self.parent.children: - if node == self: - return i - i += 1 - msg = "Error in number(self)!" - raise Exception(msg) diff --git a/src/mqt/qmap/visualization/visualize_search_graph.py b/src/mqt/qmap/visualization/visualize_search_graph.py index ff6f7e24e..2153f515d 100644 --- a/src/mqt/qmap/visualization/visualize_search_graph.py +++ b/src/mqt/qmap/visualization/visualize_search_graph.py @@ -15,8 +15,7 @@ from ipywidgets import HBox, IntSlider, Layout, Play, VBox, Widget, interactive, jslink from networkx.drawing.nx_pydot import graphviz_layout from plotly.subplots import make_subplots - -from mqt.qmap.visualization.treedraw import Tree +from walkerlayout import WalkerLayouting @dataclass @@ -133,19 +132,10 @@ def _layout_search_graph( search_graph: nx.Graph, root: int, method: Literal["walker", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork"], - walker_factor: float, # 0-1 tapered_layer_heights: bool, ) -> dict[int, Position]: if method == "walker": - tree = Tree(root) - nodes = {root: tree.root} - for node in search_graph.nodes: - if node != root: - nodes[node] = nodes[search_graph.nodes[node]["data"].parent].addChild(node) - tree.walker(walker_factor) - pos = {} - for node in tree.nodes: - pos[node.data] = node.position(origin=(0, 0), scalex=100, scaley=-1) + pos = WalkerLayouting.layout_networkx(search_graph, root, origin=(0, 0), scalex=60, scaley=-1) else: pos = graphviz_layout(search_graph, prog=method, root=search_graph.nodes[0]) @@ -481,7 +471,7 @@ def _parse_arch_graph(file_path: str) -> nx.Graph: # TODO: this is the cost of 1 swap for the non-noise-aware heuristic # mapper; depending on directionality this might be different; once # more dynamic swap cost system in Architecture.cpp is implemented - # replace wiht dynamic cost lookup + # replace with dynamic cost lookup edges.add(edge) nqbits = max(nqbits, q0, q1) nqbits += 1 @@ -731,7 +721,6 @@ def _load_layer_data( data_logging_path: str, layer: int, layout_method: Literal["walker", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork"], - layout_walker_factor: float, # 0-1 tapered_layer_heights: bool, number_of_node_traces: int, use3d: bool, @@ -790,7 +779,7 @@ def _load_layer_data( graph, graph_root = _parse_search_graph(f"{data_logging_path}nodes_layer_{layer}.csv", final_node_id) - pos = _layout_search_graph(graph, graph_root, layout_method, layout_walker_factor, tapered_layer_heights) + pos = _layout_search_graph(graph, graph_root, layout_method, tapered_layer_heights) ( edge_x, @@ -971,7 +960,6 @@ def _visualize_search_graph_check_parameters( architecture_node_positions: dict[int, Position] | None, architecture_layout_method: Literal["dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork"], search_node_layout_method: Literal["walker", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork"], - search_node_layout_walker_factor: float, search_graph_border: float, architecture_border: float, swap_arrow_spacing: float, @@ -1067,12 +1055,6 @@ def _visualize_search_graph_check_parameters( msg = 'search_node_layout_method must be one of "walker", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork"' raise ValueError(msg) - if search_node_layout_method == "walker" and ( - not _is_number(search_node_layout_walker_factor) or search_node_layout_walker_factor < 0 - ): - msg = "search_node_layout_walker_factor must be a non-negative float" - raise ValueError(msg) - if not _is_number(search_graph_border) or search_graph_border < 0: msg = "search_graph_border must be a non-negative float" raise ValueError(msg) @@ -1417,7 +1399,7 @@ def _visualize_search_graph_check_parameters( if isinstance(c, str): l = _cost_string_to_lambda(c) if l is None: - msg = f"Unkown cost function preset search_node_height[{i}]: {c}" + msg = f"Unknown cost function preset search_node_height[{i}]: {c}" raise ValueError(msg) elif l in lambdas: msg = f"search_node_height must not contain the same cost function multiple times: {c}" @@ -1435,7 +1417,7 @@ def _visualize_search_graph_check_parameters( if l is not None: search_node_height = [l] else: - msg = f"Unkown cost function preset search_node_height: {search_node_height}" + msg = f"Unknown cost function preset search_node_height: {search_node_height}" raise ValueError(msg) else: msg = "search_node_height must be a list of cost functions or a single cost function." @@ -1466,7 +1448,6 @@ def visualize_search_graph( search_node_layout_method: Literal[ "walker", "dot", "neato", "fdp", "sfdp", "circo", "twopi", "osage", "patchwork" ] = "walker", - search_node_layout_walker_factor: float = 0.6, search_graph_border: float = 0.05, architecture_border: float = 0.05, swap_arrow_spacing: float = 0.05, @@ -1563,7 +1544,6 @@ def visualize_search_graph( architecture_node_positions, architecture_layout_method, search_node_layout_method, - search_node_layout_walker_factor, search_graph_border, architecture_border, swap_arrow_spacing, @@ -1901,7 +1881,6 @@ def update_layer(new_layer: int): data_logging_path, current_layer, search_node_layout_method, - search_node_layout_walker_factor, tapered_search_layer_heights, number_of_node_traces, use3d,