From 4c7a75add1d401a37b18f6d931f441f3703e87bd Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 12 Mar 2025 18:10:53 -0700 Subject: [PATCH 001/127] Initial commit --- minorminer/utils/zephyr/__init__.py | 17 + minorminer/utils/zephyr/coordinate_systems.py | 72 ++ minorminer/utils/zephyr/node_edge.py | 671 ++++++++++++++++++ minorminer/utils/zephyr/plane_shift.py | 163 +++++ minorminer/utils/zephyr/qfloor.py | 432 +++++++++++ minorminer/utils/zephyr/survey.py | 315 ++++++++ minorminer/utils/{ => zephyr}/zephyr.py | 0 tests/utils/zephyr/test_coordinate_systems.py | 51 ++ tests/utils/zephyr/test_node_edge.py | 361 ++++++++++ tests/utils/zephyr/test_plane_shift.py | 75 ++ tests/utils/zephyr/test_qfloor.py | 229 ++++++ tests/utils/{ => zephyr}/test_zephyr.py | 0 tests/utils/zephyr/test_zephyr_base.py | 87 +++ tests/utils/zephyr/test_zephyr_survey.py | 87 +++ 14 files changed, 2560 insertions(+) create mode 100644 minorminer/utils/zephyr/__init__.py create mode 100644 minorminer/utils/zephyr/coordinate_systems.py create mode 100644 minorminer/utils/zephyr/node_edge.py create mode 100644 minorminer/utils/zephyr/plane_shift.py create mode 100644 minorminer/utils/zephyr/qfloor.py create mode 100644 minorminer/utils/zephyr/survey.py rename minorminer/utils/{ => zephyr}/zephyr.py (100%) create mode 100644 tests/utils/zephyr/test_coordinate_systems.py create mode 100644 tests/utils/zephyr/test_node_edge.py create mode 100644 tests/utils/zephyr/test_plane_shift.py create mode 100644 tests/utils/zephyr/test_qfloor.py rename tests/utils/{ => zephyr}/test_zephyr.py (100%) create mode 100644 tests/utils/zephyr/test_zephyr_base.py create mode 100644 tests/utils/zephyr/test_zephyr_survey.py diff --git a/minorminer/utils/zephyr/__init__.py b/minorminer/utils/zephyr/__init__.py new file mode 100644 index 00000000..5c73648c --- /dev/null +++ b/minorminer/utils/zephyr/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + +from minorminer.utils.zephyr.zephyr import * diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py new file mode 100644 index 00000000..c7862c6a --- /dev/null +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -0,0 +1,72 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from __future__ import annotations +from collections import namedtuple + + + +zephyr_fields = ["u", "w", "k", "j", "z"] +ZephyrCoord = namedtuple("ZephyrCoord", zephyr_fields, defaults=(None,)*len(zephyr_fields)) +cartesian_fields = ["x", "y", "k"] +CartesianCoord = namedtuple("CartesianCoord", cartesian_fields, defaults=(None,)*len(cartesian_fields)) + + + +def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: + """Converts a CartesianCoord to its corresponding ZephyrCoord. + Note: It assumes the given CartesianCoord is valid. + + Args: + ccoord (CartesianCoord): The coodinate in Cartesian system to be converted. + + Returns: + ZephyrCoord: The coordinate of the ccoord in Zephyr system. + """ + x, y, k = ccoord + if x % 2 == 0: + u: int = 0 + w: int = x //2 + j: int = ((y-1)%4) // 2 + z: int = y // 4 + else: + u: int = 1 + w: int = y // 2 + j: int = ((x-1)%4) // 2 + z: int = x // 4 + return ZephyrCoord(u=u, w=w, k=k, j=j, z=z) + + +def zephyr_to_cartesian(zcoord: ZephyrCoord) -> CartesianCoord: + """Converts a ZephyrCoord to its corresponding CartesianCoord. + Note: It assumes the given ZephyrCoord is valid. + + Args: + zcoord (ZephyrCoord): The coodinate in Zephyr system to be converted. + + Returns: + CartesianCoord: The coordinate of the ccoord in Cartesian system. + """ + u, w, k, j, z = zcoord + if u == 0: + x = 2*w + y = 4*z + 2*j + 1 + else: + x = 4*z + 2*j + 1 + y = 2 * w + return CartesianCoord(x=x, y=y, k=k) \ No newline at end of file diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py new file mode 100644 index 00000000..0dd2bf20 --- /dev/null +++ b/minorminer/utils/zephyr/node_edge.py @@ -0,0 +1,671 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + + +from __future__ import annotations +from typing import Generator, Iterable, Callable +from itertools import product +from collections import namedtuple +from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord, zephyr_to_cartesian, cartesian_to_zephyr +from minorminer.utils.zephyr.plane_shift import PlaneShift + +ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) + +class Edge: + """Initializes an Edge with 'int' nodes x, y. + + Args: + x (int): One endpoint of edge. + y (int): Another endpoint of edge. + + Raises: + TypeError: If either of x or y is not 'int'. + """ + def __init__( + self, + x: int, + y: int, + ) -> None: + + if not all(isinstance(var, int) for var in (x, y)): + raise TypeError( + f"Expected x, y to be 'int', got {type(x), type(y)}" + ) + if x < y: + self._edge = (x, y) + else: + self._edge = (y, x) + + def __hash__(self): + return hash(self._edge) + + def __getitem__(self, index: int) -> int: + return self._edge[index] + + def __eq__(self, other: Edge): + return self._edge == other._edge + + def __str__(self) -> str: + return f"{type(self).__name__}{self._edge}" + + def __repr__(self) -> str: + return f"{type(self).__name__}{self._edge}" + + + +class ZEdge(Edge): + """Initializes a ZEdge with 'ZNode' nodes x, y + + Args: + x (ZNode): One endpoint of edge. + y (ZNode): Another endpoint of edge. + + Raises: + TypeError: If either of x or y is not 'ZNode'. + ValueError: If x, y do not have the same shape. + ValueError: If x, y are not neighbours in a perfect yield (quotient) + Zephyr graph. + + Example 1: + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge + >>> e = ZEdge(ZNode((3, 2)), ZNode((7, 2))) + >>> print(e) + ZEdge(ZNode(CartesianCoord(x=3, y=2, k=None)), ZNode(CartesianCoord(x=7, y=2, k=None))) + Example 2: + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge + >>> ZEdge(ZNode((2, 3)), ZNode((6, 3))) # raises error, since the two are not neighbors + """ + def __init__( + self, + x: ZNode, + y: ZNode, + ) -> None: + if not isinstance(x, ZNode) or not isinstance(y, ZNode): + raise TypeError( + f"Expected x, y to be ZNode, got {type(x), type(y)}" + ) + if x.shape != y.shape: + raise ValueError( + f"Expected x, y to have the same shape, got {x.shape, y.shape}" + ) + self._kind = None + for kind in ("internal", "external", "odd"): + if x.is_neighbor(y, nbr_kind=kind): + self._kind = kind + break + if self._kind is None: + raise ValueError(f"Expected x, y to be neighbours, got {x, y}") + if x < y: + self._edge = (x, y) + else: + self._edge = (y, x) + + + + +class ZNode: + """Initializes 'ZNode' with coord and optional shape. + + Args: + coord (CartesianCoord | ZephyrCoord | tuple[int]): coordinate in (quotient) Zephyr or (quotient) Cartesian + shape (ZShape | tuple[int | None] | None, optional): shape of Zephyr graph containing ZNode. + m: grid size, t: tile size + Defaults to None. + convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. + Defaults to None. + + Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), + shape = None raises ValueError. In this case the tile size of Zephyr, t, + must be provided. + + Example: + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape + >>> zn1 = ZNode((5, 2), ZShape(m=5)) + >>> zn1.neighbors() + [ZNode(CartesianCoord(x=4, y=1, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=4, y=3, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=6, y=1, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=6, y=3, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=1, y=2, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=9, y=2, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape + >>> zn1 = ZNode((5, 2), ZShape(m=5)) + >>> zn1.neighbors(nbr_kind="odd") + [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), + ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] + """ + def __init__( + self, + coord: CartesianCoord | ZephyrCoord | tuple[int], + shape: ZShape | tuple[int | None] | None=None, + convert_to_z: bool | None=None, + ) -> None: + self.shape = shape + + if convert_to_z is None: + self.convert_to_z = len(coord) in (4, 5) + else: + self.convert_to_z = convert_to_z + + # convert coord to CartesianCoord or ZephyrCoord + if not isinstance(coord, (CartesianCoord, ZephyrCoord)): + coord = self.get_coord(coord) + + # convert coord to CartesianCoord + if isinstance(coord, ZephyrCoord): + coord = zephyr_to_cartesian(coord) + + self.ccoord = coord + + @property + def shape(self) -> ZShape | None: + """Returns the shape of the Zephyr graph ZNode belongs to.""" + return self._shape + + @shape.setter + def shape(self, new_shape) -> None: + """Sets a new value for shape""" + if new_shape is None: + new_shape = ZShape() + elif isinstance(new_shape, tuple): + new_shape = ZShape(*new_shape) + if not isinstance(new_shape, ZShape): + raise TypeError( + f"Expected shape to be tuple[int | None] or ZShape or None, got {type(new_shape)}" + ) + if hasattr(self, "_ccoord"): + if (self._ccoord.k is None) != (new_shape.t is None): + raise ValueError( + f"ccoord, shape must be both quotient or non-quotient, got {self._ccoord, new_shape}" + ) + for var, val in {"m": new_shape.m, "t": new_shape.t}.items(): + if (val is not None) and (not isinstance(val, int)): + raise TypeError( + f"Expected {var} to be None or 'int', got {type(val)}" + ) + self._shape = new_shape + + @property + def ccoord(self) -> CartesianCoord: + """Returns the CartesianCoord of self, ccoord""" + return self._ccoord + + @ccoord.setter + def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): + """Sets a new value for ccoord""" + if isinstance(new_ccoord, tuple): + new_ccoord = CartesianCoord(*new_ccoord) + if not isinstance(new_ccoord, CartesianCoord): + raise TypeError( + f"Expected ccoord to be CartesianCoord or tuple[int], got {type(new_ccoord)}" + ) + for c in (new_ccoord.x, new_ccoord.y): + if not isinstance(c, int): + raise TypeError( + f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}" + ) + if c < 0: + raise ValueError( + f"Expected ccoord.x and ccoord.y to be non-negative, got {c}" + ) + if new_ccoord.x % 2 == new_ccoord.y % 2: + raise ValueError( + f"Expected ccoord.x and ccoord.y to differ in parity, got {new_ccoord.x, new_ccoord.y}" + ) + # check k value of CartesianCoord is consistent with t + if hasattr(self, "_shape"): + if (self._shape.t is None) != (new_ccoord.k is None): + raise ValueError( + f"shape and ccoord must be both quotient or non-quotient, got {self._shape}, {new_ccoord}" + ) + if (self._shape.t is not None) and (new_ccoord.k not in range(self._shape.t)): + raise ValueError( + f"Expected k to be in {range(self._shape.t)}, got {new_ccoord.k}" + ) + + # check x, y value of CartesianCoord is consistent with m + if self._shape.m is not None: + if (all(val in range(4*self._shape.m+1) for val in (new_ccoord.x, new_ccoord.y))): + self._ccoord = new_ccoord + else: + raise ValueError( + f"Expected ccoord.x and ccoord.y to be in {range(4*self._shape.m+1)}, got {new_ccoord.x, new_ccoord.y}" + ) + self._ccoord = new_ccoord + + @staticmethod + def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: + """Takes a tuple[int] and returns the corresponding CartesianCoord + or ZephyrCoord""" + if (not isinstance(coord, tuple)) or ( + not all(isinstance(c, int) for c in coord)): + raise TypeError( + f"Expected {coord} to be a tuple[int], got {coord}" + ) + if any(c < 0 for c in coord): + raise ValueError(f"Expected elements of coord to be non-negative, got {coord}") + len_coord = len(coord) + if len_coord in (2, 3): + x, y, *k = coord + if x % 2 == y % 2: + raise ValueError( + f"Expected x, y to differ in parity, got {x, y}" + ) + return CartesianCoord(x=x, y=y) if len(k) == 0 else CartesianCoord(x=x, y=y, k=k[0]) + if len_coord in (4, 5): + u, w, *k, j, z = coord + for var, val in {"u": u, "j": j}.items(): + if not val in [0, 1]: + raise ValueError( + f"Expected {var} to be in [0, 1], got {val}" + ) + return ZephyrCoord(u=u, w=w, j=j, z=z) if len(k) == 0 else ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) + raise ValueError( + f"coord can have length 2, 3, 4 or 5, got {len_coord}" + ) + + @property + def zcoord(self) -> ZephyrCoord: + """Returns ZephyrCoordinate corresponding to ccoord""" + return cartesian_to_zephyr(self._ccoord) + + def is_quo(self) -> bool: + """Decides if the ZNode object is quotient""" + return (self._ccoord.k is None) and (self._shape.t is None) + + def to_quo(self) -> ZNode: + """Returns the quotient ZNode corresponding to self""" + qshape = ZShape(m=self._shape.m) + qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) + return ZNode( + coord=qccoord, + shape=qshape, + convert_to_z=self.convert_to_z + ) + + @property + def direction(self) -> int: + """Returns direction, 0 or 1""" + return self._ccoord.x % 2 + + def is_vertical(self) -> bool: + """Decides whether self is a vertical qubit""" + return self.direction == 0 + + def is_horizontal(self) -> bool: + """Decides whether self is a horizontal qubit""" + return self.direction == 1 + + def node_kind(self) -> str: + """Returns the node kind, vertical or horizontal""" + return "vertical" if self.is_vertical() else "horizontal" + + def neighbor_kind( + self, + other: ZNode, + ) -> str | None: + """Returns the kind of coupler between self and other, + 'internal', 'external', 'odd', or None.""" + if not isinstance(other, ZNode): + other = ZNode(other) + if self._shape != other._shape: + return + coord1 = self._ccoord + coord2 = other._ccoord + x1, y1 = coord1.x, coord1.y + x2, y2 = coord2.x, coord2.y + if abs(x1-x2) == abs(y1-y2) == 1: + return "internal" + if x1 % 2 != x2 % 2: + return + if coord1.k != coord2.k: # odd, external neighbors only on the same k + return + if self.is_vertical(): # self vertical + if x1 != x2: # odd, external neighbors only on the same vertical lines + return + diff_y = abs(y1-y2) + return "odd" if diff_y == 2 else "external" if diff_y == 4 else None + else: + if y1 != y2: # odd, external neighbors only on the same horizontal lines + return + diff_x = abs(x1-x2) + return "odd" if diff_x == 2 else "external" if diff_x == 4 else None + + def internal_neighbors_generator( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> Generator[ZNode]: + """Generator of internal neighbors""" + x, y, _ = self._ccoord + convert = self.convert_to_z + k_vals = [None] if self._shape.t is None else range(self._shape.t) + for i, j, k in product((-1, 1), (-1, 1), k_vals): + ccoord = CartesianCoord(x=x+i, y=y+j, k=k) + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) + if not where(coord): + continue + try: + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) + except GeneratorExit: + raise + except Exception: + pass + + def external_neighbors_generator( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> Generator[ZNode]: + """Generator of external neighbors""" + x, y, k = self._ccoord + convert = self.convert_to_z + changing_index = 1 if x%2 == 0 else 0 + for s in [-4, 4]: + new_x = x + s if changing_index == 0 else x + new_y = y + s if changing_index == 1 else y + ccoord = CartesianCoord(x=new_x, y=new_y, k=k) + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) + if not where(coord): + continue + try: + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) + except GeneratorExit: + raise + except Exception: + pass + + def odd_neighbors_generator( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> Generator[ZNode]: + """Generator of odd neighbors""" + x, y, k = self._ccoord + convert = self.convert_to_z + changing_index = 1 if x%2 == 0 else 0 + for s in [-2, 2]: + new_x = x + s if changing_index == 0 else x + new_y = y + s if changing_index == 1 else y + ccoord = CartesianCoord(x=new_x, y=new_y, k=k) + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) + if not where(coord): + continue + try: + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) + except GeneratorExit: + raise + except Exception: + pass + + def neighbors_generator( + self, + nbr_kind: str | Iterable[str] | None=None, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> Generator[ZNode]: + if nbr_kind is None: + kinds = {"internal", "external", "odd"} + else: + if isinstance(nbr_kind, str): + kinds = {nbr_kind} + elif isinstance(nbr_kind, Iterable): + kinds = set(nbr_kind) + else: + raise TypeError( + f"Expected 'str' or Iterable[str] or None for nbr_kind" + ) + kinds = kinds.intersection({"internal", "external", "odd"}) + if "internal" in kinds: + for cc in self.internal_neighbors_generator(where=where): + yield cc + if "external" in kinds: + for cc in self.external_neighbors_generator(where=where): + yield cc + if "odd" in kinds: + for cc in self.odd_neighbors_generator(where=where): + yield cc + + def neighbors(self, + nbr_kind: str | Iterable[str] | None=None, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> set[ZNode]: + """Returns neighbors when restricted to nbr_kind and where""" + return set(self.neighbors_generator(nbr_kind=nbr_kind, where=where)) + + def internal_neighbors( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> set[ZNode]: + """Returns internal neighbors when restricted to where""" + return set(self.neighbors_generator(nbr_kind="internal", where=where)) + + def external_neighbors( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> set[ZNode]: + """Returns external neighbors when restricted to where""" + return set(self.neighbors_generator(nbr_kind="external", where=where)) + + def odd_neighbors( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> set[ZNode]: + """Returns odd neighbors when restricted to where""" + return set(self.neighbors_generator(nbr_kind="odd", where=where)) + + def is_internal_neighbor( + self, other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> bool: + """Tells if another ZNode is an internal neighbor when restricted to where""" + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be ZNode, got {type(other)}" + ) + for nbr in self.internal_neighbors_generator(where=where): + if other == nbr: + return True + return False + + def is_external_neighbor( + self, other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> bool: + """Tells if another ZNode is an external neighbor when restricted to where""" + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be ZNode, got {type(other)}" + ) + for nbr in self.external_neighbors_generator(where=where): + if other == nbr: + return True + return False + + def is_odd_neighbor( + self, other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> bool: + """Tells if another ZNode is an odd neighbor when restricted to where""" + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be ZNode, got {type(other)}" + ) + for nbr in self.odd_neighbors_generator(where=where): + if other == nbr: + return True + return False + + def is_neighbor( + self, other: ZNode, + nbr_kind: str | Iterable[str] | None=None, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> bool: + """Tells if another ZNode is a neighbor when restricted to nbr_kind and where""" + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be ZNode, got {type(other)}" + ) + for nbr in self.neighbors_generator(nbr_kind=nbr_kind, where=where): + if other == nbr: + return True + return False + + def incident_edges( + self, + nbr_kind: str | Iterable[str] | None=None, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> list[ZEdge]: + """Returns incident edges when restricted to nbr_kind and where""" + return [ + ZEdge(self, v) + for v in self.neighbors(nbr_kind=nbr_kind, where=where) + ] + + def degree( + self, + nbr_kind: str | Iterable[str] | None=None, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + ) -> int: + """Returns degree when restricted to nbr_kind and where""" + return len(self.neighbors(nbr_kind=nbr_kind, where=where)) + + def __eq__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord == other._ccoord + + def __ne__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord != other._ccoord + + def __gt__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord > other._ccoord + + def __ge__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord >= other._ccoord + + def __lt__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord < other._ccoord + + def __le__(self, other: ZNode) -> bool: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + return self._ccoord <= other._ccoord + + def __add__( + self, + shift: PlaneShift | tuple[int], + ) -> ZNode: + if not isinstance(shift, PlaneShift): + shift = PlaneShift(*shift) + x, y, k = self._ccoord + new_x = x + shift[0] + new_y = y + shift[1] + + return ZNode(coord=CartesianCoord(x=new_x, y=new_y, k=k), shape=self._shape) + + def __sub__( + self, + other: ZNode, + ) -> PlaneShift: + if not isinstance(other, ZNode): + raise TypeError( + f"Expected {other} to be {type(self).__name__}, got {type(other)}" + ) + if self._shape != other._shape: + raise ValueError( + f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" + ) + x_shift: int = self._ccoord.x - other._ccoord.x + y_shift: int = self._ccoord.y - other._ccoord.y + try: + return PlaneShift(x_shift=x_shift, y_shift=y_shift) + except: + raise ValueError( + f"{other} cannot be subtracted from {self}" + ) + + def __hash__(self) -> int: + return (self._ccoord, self._shape).__hash__() + + def __repr__(self) -> str: + if self.convert_to_z: + coord = self.zcoord + coord_str = f"{coord.u, coord.w, coord.k, coord.j, coord.z}" + else: + coord = self._ccoord + coord_str = f"{coord.x, coord.y, coord.k}" + if self._shape == ZShape(): + shape_str = "" + else: + shape_str = f", shape={self._shape.m, self._shape.t!r}" + return f"{type(self).__name__}("+coord_str+shape_str+")" \ No newline at end of file diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py new file mode 100644 index 00000000..54e910ae --- /dev/null +++ b/minorminer/utils/zephyr/plane_shift.py @@ -0,0 +1,163 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from __future__ import annotations +from typing import Iterator +from collections import namedtuple + +Shift = namedtuple("Shift", ["x", "y"]) + + + +class PlaneShift: + """ Initializes PlaneShift with an x_shift, y_shift. + + Args: + x_shift (int): The displacement in the x-direction of a CartesianCoord. + y_shift (int): The displacement in the y-direction of a CartesianCoord. + + Raises: + TypeError: If x_shift or y_shift is not 'int'. + ValueError: If x_shift and y_shift have different parity. + + Example: + >>> from minorminer.utils.zephyr.plane_shift import PlaneShift + >>> ps1 = PlaneShift(1, 3) + >>> ps2 = PlaneShift(2, -4) + >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") + """ + def __init__( + self, + x_shift: int, + y_shift: int, + ) -> None: + for shift in [x_shift, y_shift]: + if not isinstance(shift, int): + raise TypeError(f"Expected {shift} to be 'int', got {type(shift)}") + if x_shift%2 != y_shift%2: + raise ValueError( + f"Expected x_shift, y_shift to have the same parity, got {x_shift, y_shift}" + ) + self._shift = Shift(x_shift, y_shift) + + @property + def x(self) -> int: + """Returns the shift in x direction""" + return self._shift.x + + @property + def y(self) -> int: + """Returns the shift in y direction""" + return self._shift.y + + def __mul__(self, scale: int | float) -> PlaneShift: + """Multiplies the self from left by the number value``scale``. + + Args: + scale (int | float): The scale for left-multiplying self with. + + Raises: + TypeError: If scale is not 'int' or 'float'. + ValueError: If the resulting PlaneShift has non-whole values. + + Returns: + PlaneShift: The result of left-multiplying self by scale. + """ + if not isinstance(scale, (int, float)): + raise TypeError(f"Expected scale to be int or float, got {type(scale)}") + new_shift_x = scale*self._shift.x + new_shift_y = scale*self._shift.y + if int(new_shift_x) != new_shift_x or int(new_shift_y) != new_shift_y: + raise ValueError(f"{scale} cannot be multiplied by {self}") + return PlaneShift(int(new_shift_x), int(new_shift_y)) + + def __rmul__(self, scale: int | float) -> PlaneShift: + """Multiplies the self from right by the number value``scale``. + + Args: + scale (int | float): The scale for right-multiplying self with. + + Raises: + TypeError: If scale is not 'int' or 'float'. + ValueError: If the resulting PlaneShift has non-whole values. + + Returns: + PlaneShift: The result of right-multiplying self by scale. + """ + return self * scale + + def __add__(self, other: PlaneShift) -> PlaneShift: + """ + Adds another PlaneShift object to self. + Args: + other (PlaneShift): The object to add self by. + + Raises: + TypeError: If other is not PlaneShift + + Returns: + PlaneShift: The displacement in CartesianCoord by self followed by other. + """ + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return PlaneShift(self._shift.x+other._shift.x, self._shift.y+other._shift.y) + + def __iter__(self) -> Iterator[int]: + return self._shift.__iter__() + + def __len__(self) -> int: + return self._shift.__len__() + + def __hash__(self) -> int: + return self._shift.__hash__() + + def __getitem__(self, key) -> int: + return self._shift.__getitem__(key) + + def __eq__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return self._shift == other._shift + + def __ne__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return not self._shift == other._shift + + def __lt__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return self._shift < other._shift + + def __le__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return (self == other) or (self < other) + + def __gt__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return self._shift > other._shift + + def __ge__(self, other: PlaneShift) -> bool: + if not isinstance(other, PlaneShift): + raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") + return (self == other) or (self > other) + + def __repr__(self) -> str: + return f"{type(self).__name__}{self._shift.x, self._shift.y}" \ No newline at end of file diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py new file mode 100644 index 00000000..3457b306 --- /dev/null +++ b/minorminer/utils/zephyr/qfloor.py @@ -0,0 +1,432 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from __future__ import annotations +from typing import Iterable, Callable, Iterator +from collections import defaultdict, namedtuple +from itertools import product +from minorminer.utils.zephyr.coordinate_systems import * +from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, ZShape + + + +Dim = namedtuple("Dim", ["Lx", "Ly"]) +UWJ = namedtuple("UWJ", ["u", "w", "j"]) +ZSE = namedtuple("ZSE", ["z_start", "z_end"]) + + + +class QuoTile: + """Initializes a 'QuoTile' object with zns. + + Args: + zns (Iterable[ZNode]): The ZNodes the tile contains + + Example: + .. code-block:: python + >>> from minorminer.utils.zephyr.node_edge import ZNode + >>> from minorminer.utils.zephyr.qfloor import QuoTile + >>> ccoords = [(k, 1+k) for k in range(4)] + [(1+k, k) for k in range(4)] + >>> zns = [ZNode(coord=ccoord) for ccoord in ccoords] + >>> tile = QuoTile(zns) + >>> print(f"{tile.edges() = }\n{tile.seed = }\n{tile.shifts = }") + """ + def __init__( + self, + zns: Iterable[ZNode], + ) -> None: + self.zns = zns + + @property + def zns(self) -> list[ZNode]: + """Returns the sorted list of ZNodes the tile contains""" + return self._zns + + @zns.setter + def zns(self, new_zns: Iterable[ZNode]) -> None: + """Sets the zns""" + if not isinstance(new_zns, Iterable): + raise TypeError( + f"Expected {new_zns} to be Iterable[ZNode], got {type(new_zns)}" + ) + for zn in new_zns: + if not isinstance(zn, ZNode): + raise TypeError( + f"Expected elements of {new_zns} to be ZNode, got {type(zn)}" + ) + if not zn.is_quo(): + raise ValueError(f"Expected elements of {new_zns} to be quotient, got {zn}") + new_zns_shape = {zn.shape for zn in new_zns} + if len(new_zns_shape) != 1: + raise ValueError( + f"Expected all elements of zns to have the same shape, got {new_zns_shape}" + ) + temp_zns = sorted(list(set(new_zns))) + if len(temp_zns) == 0: + return temp_zns + seed_convert = temp_zns[0].convert_to_z + result = [] + for zn in temp_zns: + zn.convert_to_z = seed_convert + result.append(zn) + self._zns = result + + @property + def seed(self) -> ZNode: + """Returns the smallest ZNode in Zns""" + return self._zns[0] + + @property + def shifts(self) -> list[PlaneShift]: + """Returns the list of shift of each ZNode in zns from seed""" + seed = self.seed + return [zn - seed for zn in self._zns] + + @property + def shape(self) -> ZShape: + """Returns the shape of the Zephyr graph the tile belongs to.""" + return self.seed.shape + + @property + def convert_to_z(self) -> bool: + """Returns the convert_to_z attribute of the ZNodes of the tile""" + return self.seed.convert_to_z + + @property + def ver_zns(self) -> list[ZNode]: + """Returns the list of vertical ZNodes of the tile""" + return [zn for zn in self._zns if zn.is_vertical()] + + @property + def hor_zns(self) -> list[ZNode]: + """Returns the list of horizontal ZNodes of the tile""" + return [zn for zn in self._zns if zn.is_horizontal()] + + def edges( + self, + where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, + nbr_kind: str | Iterable[str] | None=None, + ) -> list[ZEdge]: + """Returns the list of edges of the graph induced on the tile, + when resticted to nbr_kind and where.""" + zns = self._zns + tile_coords = [zn.zcoord for zn in zns] if self.convert_to_z else [zn.ccoord for zn in zns] + where_tile = lambda coord: where(coord) and coord in tile_coords + _edges = { + edge + for zn in zns + for edge in zn.incident_edges(nbr_kind=nbr_kind, where=where_tile) + } + return list(_edges) + + def __len__(self) -> int: + return len(self._zns) + + def __iter__(self) -> Iterator[ZNode]: + for zn in self._zns: + yield zn + + def __getitem__(self, key) -> ZNode: + return self._zns[key] + + def __hash__(self) -> int: + return hash(self._zns) + + def __eq__(self, other: QuoTile) -> bool: + return self._zns == other._zns + + def __add__(self, shift: PlaneShift) -> QuoTile: + return QuoTile(zns=[zn+shift for zn in self._zns]) + + def __repr__(self) -> str: + return f"{type(self).__name__}{self._zns!r}" + + def __str__(self) -> str: + return f"{type(self).__name__}{self._zns}" + + + + +class QuoFloor: + """Initializes QuoFloor object with a corner tile and dimension and + optional tile connector. + + Args: + corner_qtile (QuoTile): The tile at the top left corner of desired + subgrid of Quotient Zephyr graph. + dim (Dim | tuple[int]): The dimension of floor, i.e. + the number of columns of floor, the number of rows of floor. + tile_connector (dict[tuple[int], PlaneShift], optional): + Determines how to get the tiles (x+1, y) and (x, y+1) from tile (x, y). + Defaults to tile_connector0. + Example: + .. code-block:: python + >>> from minorminer.utils.zephyr.node_edge import ZNode + >>> from minorminer.utils.zephyr.qfloor import QuoFloor + >>> coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + >>> zns = [ZNode(coord=c) for c in coords] + >>> floor = QuoFloor(corner_qtile=zns, dim=(3, 5)) + >>> print(f"{floor.qtile_xy(2, 3) = }") + floor.qtile_xy(2, 3) = QuoTile[ZNode(CartesianCoord(x=8, y=13, k=None)), ZNode(CartesianCoord(x=9, y=12, k=None)), ZNode(CartesianCoord(x=9, y=14, k=None)), ZNode(CartesianCoord(x=10, y=13, k=None)), ZNode(CartesianCoord(x=10, y=15, k=None)), ZNode(CartesianCoord(x=11, y=14, k=None)), ZNode(CartesianCoord(x=11, y=16, k=None)), ZNode(CartesianCoord(x=12, y=15, k=None))] + + """ + implemented_connectors: tuple[dict[tuple[int], PlaneShift]] = ( + {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 4)}, + ) + tile_connector0 = implemented_connectors[0] + + def __init__( + self, + corner_qtile: QuoTile | Iterable[ZNode], + dim: Dim | tuple[int], + tile_connector: dict[tuple[int], PlaneShift] = tile_connector0, + ) -> None: + self.tile_connector = tile_connector + self.dim = dim + self.corner_qtile = corner_qtile + + @property + def tile_connector(self) -> dict[tuple[int], PlaneShift] | None: + """Returns the tile connector""" + return self.tile_connector + + @tile_connector.setter + def tile_connector( + self, + new_connector: dict[tuple[int], PlaneShift] + ) -> None: + """Sets the tile connector""" + if not isinstance(new_connector, dict): + raise TypeError(f"Expected tile_connector to be dict, got {type(new_connector)}") + if not new_connector in self.implemented_connectors: + raise NotImplementedError( + f"{new_connector} not implemented. " + f"Availabale options are {self.implemented_connectors}" + ) + if any(dir_con not in new_connector.keys() for dir_con in ((1, 0), (0, 1))): + raise ValueError( + f"Expected tile_connector to have (1, 0), (0, 1) as keys, got {new_connector}" + ) + self._tile_connector = new_connector + + def check_dim_corner_qtile_compatibility( + self, + dim: Dim, + corner_qtile: QuoTile, + ) -> None: + """Checks whether dimension and corner tile are compatible, i.e. + whether given the tile connector, the floor can be + constructed with the provided corner tile and dimensions. + """ + if any(par is None for par in (dim, corner_qtile, self._tile_connector)): + return + ver_step = self._tile_connector[(0, 1)] + hor_step = self._tile_connector[(1, 0)] + try: + [h+hor_step*(dim.Lx-1) for h in corner_qtile.hor_zns] + [v+ver_step*(dim.Ly-1) for v in corner_qtile.ver_zns] + except (ValueError, TypeError): + raise ValueError( + f"{dim, corner_qtile} are not compatible" + ) + + @property + def dim(self) -> Dim: + """Returns dimension of the floor""" + return self._dim + + @dim.setter + def dim(self, new_dim: Dim | tuple[int]) -> None: + """Sets dimension of the floor""" + if isinstance(new_dim, tuple): + new_dim = Dim(*new_dim) + if not isinstance(new_dim, Dim): + raise TypeError( + f"Expected dim to be Dim or tuple[int], got {type(new_dim)}" + ) + if not all(isinstance(x, int) for x in new_dim): + raise TypeError( + f"Expected Dim elements to be int, got {new_dim}" + ) + if any(x <= 0 for x in new_dim): + raise ValueError( + f"Expected elements of Dim to be positive integers, got {Dim}" + ) + if hasattr(self, "_corner_qtile"): + self.check_dim_corner_qtile_compatibility( + dim=new_dim, corner_qtile=self._corner_qtile) + self._dim = new_dim + + @property + def corner_qtile(self) -> QuoTile: + """Returns the corner tile of floor""" + return self._corner_qtile + + @corner_qtile.setter + def corner_qtile(self, new_qtile: QuoTile) -> None: + """Sets corner tile of the floor""" + if isinstance(new_qtile, Iterable): + new_qtile = QuoTile(zns=new_qtile) + if not isinstance(new_qtile, QuoTile): + raise TypeError( + f"Expected corner_qtile to be QuoTile, got {type(new_qtile)}" + ) + ccoords = [zn.ccoord for zn in new_qtile.zns] + x_values = defaultdict(list) + y_values = defaultdict(list) + for x, y, *_ in ccoords: + x_values[x].append(y) + y_values[y].append(x) + x_diff = {x: max(x_list) - min(x_list) + for x, x_list in x_values.items()} + y_diff = {y: max(y_list) - min(y_list ) + for y, y_list in y_values.items()} + x_jump = self._tile_connector[(1, 0)].x + y_jump = self._tile_connector[(0, 1)].y + + for y, xdiff in y_diff.items(): + if abs(xdiff) >= y_jump: + raise ValueError( + f"This tile may overlap other tiles on {y = }" + ) + for x, ydiff in x_diff.items(): + if abs(ydiff) >= x_jump: + raise ValueError( + f"This tile may overlap other tiles on {x = }" + ) + if hasattr(self, "_dim"): + self.check_dim_corner_qtile_compatibility( + dim=self._dim, corner_qtile=new_qtile) + self._corner_qtile = new_qtile + + def qtile_xy(self, x: int, y: int) -> QuoTile: + """Returns the tile at the position x, y of floor, i.e. column x, row y.""" + if (x, y) not in product(range(self._dim.Lx), range(self._dim.Ly)): + raise ValueError( + f"Expected x to be in {range(self._dim.Lx)} and y to be in {range(self._dim.Ly)}. " + f"Got {x, y}." + ) + xy_shift = x*self._tile_connector[(1, 0)] + y*self._tile_connector[(0, 1)] + return self._corner_qtile + xy_shift + + @property + def qtiles(self) -> dict[tuple[int], QuoTile]: + """Returns the dictionary where the keys are positions of floor, + and the values are the tiles corresponding to the position. + """ + if any(par is None + for par in (self._dim, self._corner_qtile, self._tile_connector) + ): + raise AttributeError( + f"Cannot access 'qtiles' because either 'dim', 'corner_qtile', or 'tile_connector' is None." + ) + return { + (x, y): self.qtile_xy(x=x, y=y) + for (x, y) in product(range(self._dim.Lx), range(self._dim.Ly)) + } + + @property + def zns(self) -> dict[tuple[int], list[ZNode]]: + """Returns the dictionary where the keys are positions of floor, + and the values are the ZNodes the tile corresponding to the position + contains.""" + return { + xy: xy_tile.zns + for xy, xy_tile in self.qtiles.items() + } + + @property + def ver_zns(self) -> dict[tuple[int], list[ZNode]]: + """Returns the dictionary where the keys are positions of floor, + and the values are the vertical ZNodes the tile corresponding to + the position contains.""" + return { + xy: xy_tile.ver_zns + for xy, xy_tile in self.qtiles.items() + } + + @property + def hor_zns(self) -> dict[tuple[int], list[ZNode]]: + """Returns the dictionary where the keys are positions of floor, + and the values are the horizontal ZNodes the tile corresponding to + the position contains.""" + return { + xy: xy_tile.hor_zns + for xy, xy_tile in self.qtiles.items() + } + + @property + def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: + """ + Returns {"col": {col_num: (UWJ, SE)}, "row": {hor_num: (UWJ, SE)}} + of the quo-external-paths + necessary to be covered from z_start to z_end. + """ + if any(par is None + for par in (self._dim, self._corner_qtile, self._tile_connector) + ): + raise AttributeError( + f"Cannot access 'quo_ext_paths' because either 'dim', 'corner_qtile', or 'tile_connector' is None." + ) + result = {"col": defaultdict(list), "row": defaultdict(list)} + hor_con = self._tile_connector[(1, 0)] + ver_con = self._tile_connector[(0, 1)] + if hor_con == PlaneShift(4, 0): + for row_num in range(self._dim.Ly): + for hzn in self.qtile_xy(0, row_num).hor_zns: + hzn_z = hzn.zcoord + result["row"][row_num].append( + ( + UWJ(u=hzn_z.u, w=hzn_z.w, j=hzn_z.j), + ZSE(z_start=hzn_z.z, z_end=hzn_z.z+self._dim.Lx-1) + ) + ) + elif hor_con == PlaneShift(2, 0): + result["row"] = dict() + else: + raise NotImplementedError + if ver_con == PlaneShift(0, 4): + for col_num in range(self._dim.Lx): + for vzn in self.qtile_xy(col_num, 0).ver_zns: + vzn_z = vzn.zcoord + result["col"][col_num].append( + ( + UWJ(u=vzn_z.u, w=vzn_z.w, j=vzn_z.j), + ZSE(z_start=vzn_z.z, z_end=vzn_z.z+self._dim.Ly-1) + ) + ) + elif ver_con == PlaneShift(0, 2): + result["col"] = dict() + else: + raise NotImplementedError + return {direction: dict(dir_dict) for direction, dir_dict in result.items()} + + def __repr__(self) -> str: + if self._tile_connector == self.tile_connector0: + tile_connector_str = ")" + else: + tile_connector_str = ", {self._tile_connector!r})" + return f"{type(self).__name__}({self._corner_qtile!r}, {self._dim!r}"+tile_connector_str + + def __str__(self) -> str: + if self._tile_connector == self.tile_connector0: + tile_connector_str = ")" + else: + tile_connector_str = ", {self._tile_connector})" + return f"{type(self).__name__}({self._corner_qtile}, {self._dim})"+tile_connector_str diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py new file mode 100644 index 00000000..3366576a --- /dev/null +++ b/minorminer/utils/zephyr/survey.py @@ -0,0 +1,315 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + + +from __future__ import annotations +from typing import Callable +from collections import namedtuple +import networkx as nx +import dwave_networkx as dnx +from functools import cached_property +from dwave.system import DWaveSampler +from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord +from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, ZShape + + +UWKJ = namedtuple("UWKJ", ["u", "w", "k", "j"]) +ZSE = namedtuple("ZSE", ["z_start", "z_end"]) + + + +class ZSurvey: + """Takes a Zephyr graph or DWaveSampler with Zephyr topology and + initializes a survey of the existing/missing nodes, edges. Also, gives a survey of + external paths. + + Args: + G (nx.Graph | DWaveSampler): A graph or DWaveSampler with Zephyr topology + Example: + >>> from dwave.system import DWaveSampler + >>> from minorminer.utils.zephyr.zephyr_survey import Survey + >>> sampler = DWaveSampler(solver="Advantage2_prototype2.6", profile='defaults') + >>> survey = Survey(sampler) + >>> print(f"Number of missing nodes is {survey.num_missing_nodes}") + Number of missing nodes is 33 + >>> print(f"Number of missing edges with both endpoints present is {survey.num_extra_missing_edges}") + Number of missing edges with both endpoints present is 18 + """ + def __init__( + self, + G: nx.Graph | DWaveSampler, + ) -> None: + + self._shape, self._input_coord_type = self.get_shape_coord(G) + if isinstance(G, nx.Graph): + G_nodes = list(G.nodes()) + G_edges = list(G.edges()) + elif isinstance(G, DWaveSampler): + G_nodes = G.nodelist + G_edges = G.edgelist + self._nodes: set[ZNode] = { + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape) + for v in G_nodes + } + self._edges: set[ZEdge] = { + ZEdge( + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape), + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape) + ) + for (u, v) in G_edges + } + + @staticmethod + def get_shape_coord(G: nx.Graph | DWaveSampler) -> dict[str, ZShape | str]: + """Returns the shape, coordinates of G, which must be a zephyr graph or + DWaveSampler with zephyr topology. + """ + def graph_shape_coord(G: nx.Graph) -> dict[str, int | str]: + G_info = G.graph + G_top = G_info.get("family") + if G_top != "zephyr": + raise ValueError( + f"Expected a graph with zephyr topology, got {G_top}" + ) + m, t, coord = G_info.get("rows"), G_info.get("tile"), G_info.get("labels") + return ZShape(m=m, t=t), coord + + def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: + sampler_top: dict[str, str | int] = sampler.properties.get("topology") + if sampler_top.get("type") != "zephyr": + raise ValueError( + f"Expected a sampler with zephyr topology, got {sampler_top}" + ) + nodes: list[int] = sampler.nodelist + edges: list[tuple[int]] = sampler.edgelist + for v in nodes: + if not isinstance(v, int): + raise NotImplementedError( + f"This is implemented only for nodelist containing 'int' elements , got {v}" + ) + for e in edges: + if not isinstance(e, tuple): + raise NotImplementedError( + f"This is implemented only for edgelist containing 'tuple' elements, got {e}" + ) + if len(e) != 2: + raise ValueError( + f"Expected tuple of length 2 in edgelist, got {e}" + ) + if not isinstance(e[0], int) or not isinstance(e[1], int): + raise NotImplementedError( + f"This is implemented only for 'tuple[int]' edgelist, got {e}" + ) + coord: str = "int" + return ZShape(*sampler_top.get("shape")), coord + + if isinstance(G, nx.Graph): + return graph_shape_coord(G) + if isinstance(G, DWaveSampler): + return sampler_shape_coord(G) + else: + raise TypeError( + f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}" + ) + + @cached_property + def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: + """Returns a function that converts the linear or zephyr coordinates to + the corresponding zephyr coordinates""" + if self._input_coord_type == "int": + return dnx.zephyr_coordinates(m=self.shape.m, t=self.shape.t).linear_to_zephyr + elif self._input_coord_type == "coordinate": + return lambda v: v + else: + raise ValueError( + f"Expected 'int' or 'coordinate' for self.coord, got {self._input_coord_type}" + ) + + @property + def shape(self) -> ZShape: + """Returns the ZShape of G""" + return self._shape + + @property + def nodes(self) -> set[ZNode]: + """Returns the ZNodes of the sampler/graph""" + return self._nodes + + @cached_property + def missing_nodes(self) -> set[ZNode]: + """Returns the ZNodes of the sampler/graph which are missing compared to perfect yield + Zephyr graph on the same shape. + """ + parent_nodes = [ + ZNode(coord=ZephyrCoord(*v), shape=self._shape) + for v in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).nodes() + ] + return {v for v in parent_nodes if not v in self._nodes} + + @property + def edges(self) -> set[ZEdge]: + """Returns the ZEdges of the sampler/graph""" + return self._edges + + @cached_property + def missing_edges(self) -> set[ZEdge]: + """Returns the ZEdges of the sampler/graph which are missing compared to + perfect yield Zephyr graph on the same shape.""" + parent_edges = [ + ZEdge( + ZNode(coord=u, shape=self.shape), + ZNode(coord=v, shape=self.shape) + ) + for (u, v) in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).edges() + ] + return {e for e in parent_edges if not e in self._edges} + + @property + def extra_missing_edges(self) -> set[ZEdge]: + """Returns the ZEdges of the sampler/graph which are missing compared to + perfect yield Zephyr graph on the same shape and are not incident with + a missing node.""" + return { + e + for e in self.missing_edges + if e[0] in self._nodes + and e[1] in self._nodes + } + + @property + def num_nodes(self) -> int: + """Returns the number of nodes""" + return len(self._nodes) + + @property + def num_missing_nodes(self) -> int: + """Returns the number of missing nodes""" + return len(self.missing_nodes) + + @property + def num_edges(self) -> int: + """Returns the number of edges""" + return len(self._edges) + + @property + def num_missing_edges(self) -> int: + """Returns the number of missing edges""" + return len(self.missing_edges) + + @property + def num_extra_missing_edges(self) -> int: + """Returns the number of missing edges that are not incident + with a missing node""" + return len(self.extra_missing_edges) + + def neighbors( + self, + v: ZNode, + nbr_kind: str | None=None, + ) -> set[ZNode]: + """Returns the neighbours of v when restricted to nbr_kind""" + if not isinstance(v, ZNode): + raise TypeError( + f"Expected v to be ZNode, got {v}" + ) + if v in self.missing_nodes: + return {} + return { + v_nbr + for v_nbr in v.neighbors(nbr_kind=nbr_kind) + if ZEdge(v, v_nbr) in self._edges + } + + def incident_edges( + self, + v: ZNode, + nbr_kind: str | None=None, + ) -> set[ZEdge]: + """Returns the edges incident with v when restricted to nbr_kind""" + nbrs = self.neighbors(v, nbr_kind=nbr_kind) + if len(nbrs) == 0: + return set() + return {ZEdge(v, v_nbr) for v_nbr in nbrs} + + def degree( + self, + v: ZNode, + nbr_kind: str | None=None, + ) -> int: + """Returns the degree of v when restricted to nbr_kind""" + return len(self.neighbors(v, nbr_kind=nbr_kind)) + + def _ext_path(self, uwkj: UWKJ) -> set[ZSE]: + """ + Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) + for each non-overlapping external path segment of uwkj. + """ + z_vals = list(range(self.shape.m)) # As in zephyr coordinates + + def _ext_seg(z_start: int) -> ZSE | None: + """ + If (u, w, k, j, z_start) does not exist, returns None. + Else, finds the external path segment starting at z_start going to right, and + returns the endpoints of the segment (z_start, z_end). + """ + cur: ZNode = ZNode(coord=ZephyrCoord(*uwkj, z_start), shape=self.shape) + if cur in self.missing_nodes: + return None + upper_z: int = z_vals[-1] + if z_start > upper_z: + return None + if z_start == upper_z: + return ZSE(z_start, z_start) + next_ext = ZNode(coord=ZephyrCoord(*uwkj, z_start+1), shape=self.shape) + ext_edge = ZEdge(cur, next_ext) + if ext_edge in self.missing_edges: + return ZSE(z_start, z_start) + is_extensible = _ext_seg(z_start+1) + if is_extensible is None: + return ZSE(z_start, z_start) + return ZSE(z_start, is_extensible.z_end) + + uwkj_sur: set[ZSE] = set() + while z_vals: + z_start = z_vals[0] + seg = _ext_seg(z_start=z_start) + if seg is None: + z_vals.remove(z_start) + else: + uwkj_sur.add(seg) + for i in range(seg.z_start, seg.z_end+1): + z_vals.remove(i) + + return uwkj_sur + + def calculate_external_paths_stretch(self) -> dict[UWKJ, set[ZSE]]: + """ + Returns {uwkj: set of connected segments (z_start, z_end)} + """ + uwkj_vals = [ + UWKJ(u=u, w=w, k=k, j=j) + for u in range(2) # As in zephyr coordinates + for w in range(2*self.shape.m+1) # As in zephyr coordinates + for k in range(self.shape.t) # As in zephyr coordinates + for j in range(2) # As in zephyr coordinates + ] + return { + uwkj: self._ext_path(uwkj) + for uwkj in uwkj_vals + } + diff --git a/minorminer/utils/zephyr.py b/minorminer/utils/zephyr/zephyr.py similarity index 100% rename from minorminer/utils/zephyr.py rename to minorminer/utils/zephyr/zephyr.py diff --git a/tests/utils/zephyr/test_coordinate_systems.py b/tests/utils/zephyr/test_coordinate_systems.py new file mode 100644 index 00000000..9ca91661 --- /dev/null +++ b/tests/utils/zephyr/test_coordinate_systems.py @@ -0,0 +1,51 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + +import unittest +from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord, cartesian_to_zephyr, zephyr_to_cartesian + +class TestCoordinateSystems(unittest.TestCase): + + def test_cartesian_to_zephyr_runs(self): + xyks = [(0, 1), (1, 0, None), (12, -3)] + ccoords = [CartesianCoord(*xyk) for xyk in xyks] + for ccoord in ccoords: + cartesian_to_zephyr(ccoord=ccoord) + + def test_cartesian_to_zephyr(self): + self.assertEqual(ZephyrCoord(0, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(0, 1, None))) + self.assertEqual(ZephyrCoord(1, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(1, 0, None))) + self.assertEqual(CartesianCoord(5, 12, 3), zephyr_to_cartesian(ZephyrCoord(1, 6, 3, 0, 1))) + + def test_zephyr_to_cartesian_runs(self): + uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] + zcoords = [ZephyrCoord(*uwkjz) for uwkjz in uwkjzs] + for zcoord in zcoords: + zephyr_to_cartesian(zcoord=zcoord) + + def test_coordinate_systems_match(self): + valid_uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] + zcoords = [ZephyrCoord(*uwkjz) for uwkjz in valid_uwkjzs] + for zcoord in zcoords: + ccoord = zephyr_to_cartesian(zcoord=zcoord) + self.assertEqual(zcoord, cartesian_to_zephyr(ccoord=ccoord)) + + valid_xyks = [(0, 1), (1, 0, None), (12, -3)] + ccoords = [CartesianCoord(*xyk) for xyk in valid_xyks] + for ccoord in ccoords: + zcoord = cartesian_to_zephyr(ccoord=ccoord) + self.assertEqual(ccoord, zephyr_to_cartesian(zcoord=zcoord)) \ No newline at end of file diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py new file mode 100644 index 00000000..eb3ab271 --- /dev/null +++ b/tests/utils/zephyr/test_node_edge.py @@ -0,0 +1,361 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + +import unittest +from itertools import product +from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, Edge, ZShape +from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord + + + +class TestEdge(unittest.TestCase): + def test_valid_input_runs(self) -> None: + Edge(2, 4) + Edge(5, -1) + + def test_invalid_input_raises_error(self) -> None: + with self.assertRaises(TypeError): + Edge(2, 'string') + Edge(2, ZNode(ZephyrCoord(0, 10, 3, 1, 3), ZShape(t=4))) + + def test_equal(self) -> None: + self.assertEqual(Edge(2, 4), Edge(4, 2)) + + + +class TestZEdge(unittest.TestCase): + def test_valid_input_runs(self) -> None: + valid_edges = [ + ( + ZNode(coord=ZephyrCoord(0, 10, 3, 1, 3), shape=ZShape(6, 4)), + ZNode(coord=ZephyrCoord(0, 10, 3, 1, 2), shape=ZShape(6, 4)) + ), + ( + ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), + ZNode(coord=CartesianCoord(4, 1, 2), shape=(6, 3)) + ), + (ZNode(coord=(1, 6)), ZNode(coord=(5, 6))), + ] + for x, y in valid_edges: + ZEdge(x, y) + + def test_invalid_input_raises_error(self): + with self.assertRaises((TypeError, ValueError)): + ZEdge(2, 4) + ZEdge(ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3))) + + + + +class TestZNode(unittest.TestCase): + def setUp(self): + self.u_vals = [0, 1] + self.invalid_u_vals = [2, -1, None, 3.5] + self.invalid_w_vals = [-1] + self.j_vals = [0, 1] + self.invalid_j_vals = [2, -1, None, 3.5] + self.m_vals = [6, 1, 20] + self.invalid_m_vals = [0, 2.5, -3] + self.t_vals = [6, 1, 20] + self.invalid_t_vals = [0, 2.5, -2] + self.invalid_z_vals = [-1, None] + xym_vals = [ + ((0, 3), 1), ((5, 2), 6), ((16, 1), 4), + ((1, 12), 3), ((3, 0), 4), ((5, 4), 6), + ((0, 3), 6), ((6, 3), 5), + ] + self.xyms = xym_vals + [(xy, None) for xy, _ in xym_vals] + self.left_up_xyms = [((0, 3), 6), ((0, 3), None), ((11, 0), None), ((0, 5), 8)] + self.right_down_xyms = [((1, 12), 3), ((16, 1), 4)] + self.midgrid_xyms = [((5, 2), 6), ((5, 4), 6), ((6, 7), 5)] + + def test_zephyr_node_runs(self) -> None: + for xy, m in self.xyms: + ZNode(xy, ZShape(m=m)) + ZNode(xy, ZShape(m=m), convert_to_z=True) + + for u in self.u_vals: + for j in self.j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + z_vals = range(m) + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + def test_zephyr_node_invalid_args_raises_error(self) -> None: + invalid_xyms = [ + ((-1, 2), 6), + ((6, -1), 4), + ((1, 3), -5), + ((1, 3), 6), + ((2, 4), 6), + ((17, 0), 4), + ((0, 17), 4), + ((0, 0, 0), 1), + ((-1, 2), None), + ] + for xy, m in invalid_xyms: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=xy, shape=ZShape(m=m)) + # All good except u_vals + for u in self.invalid_u_vals: + for j in self.j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + z_vals = range(m) + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + # All good except w_vals + for u in self.u_vals: + for j in self.j_vals: + for m in self.m_vals: + z_vals = range(m) + for w in self.invalid_w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + # All good except k_vals + for u in self.u_vals: + for j in self.j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + z_vals = range(m) + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + invalid_k_vals = [-1, t, 2.5, None] + for k in invalid_k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + # All good except j_vals + for u in self.u_vals: + for j in self.invalid_j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + z_vals = range(m) + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + # All good except z_vals + for u in self.u_vals: + for j in self.j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + invalid_z_vals = [None, -1, m, 1.5] + for w in w_vals: + for z in invalid_z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + # All good except m_vals + for u in self.u_vals: + for j in self.j_vals: + for m in self.invalid_m_vals: + w_vals = [0] + z_vals = [0] + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + # All good except t_vals + for u in self.u_vals: + for j in self.j_vals: + for m in self.invalid_m_vals: + w_vals = [0] + z_vals = [0] + for w in w_vals: + for z in z_vals: + for t in self.invalid_t_vals: + k_vals = [0, 1, 3] + for k in k_vals: + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=(u, w, k, j, z), shape=(m, t)) + + def test_add_sub_runs(self) -> None: + left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] + lu_qps = PlaneShift(-1, -1) + right_down_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.right_down_xyms] + rd_qps = PlaneShift(1, 1) + midgrid_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.midgrid_xyms] + for pc in midgrid_pcs: + for s1, s2 in product((-2, 2), (-2, 2)): + pc + PlaneShift(s1, s2) + for pc in left_up_pcs: + with self.assertRaises(ValueError): + pc + lu_qps + for pc in right_down_pcs: + with self.assertRaises(ValueError): + pc + rd_qps + + def test_add_sub(self) -> None: + midgrid_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.midgrid_xyms] + right_down_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.right_down_xyms] + left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] + + for pc in midgrid_pcs + right_down_pcs + left_up_pcs: + self.assertEqual(pc+PlaneShift(0, 0), pc) + + def test_neighbors_generator_runs(self) -> None: + for _ in ZNode((1, 12, 4), ZShape(t=6)).neighbors_generator(): + _ + + def test_zcoord(self) -> None: + ZNode((11, 12, 4), ZShape(t=6)).zcoord == ZephyrCoord(1, 0, 4, 0, 2) + ZNode((1, 0)).zcoord == ZephyrCoord(1, 0, None, 0, 0) + ZNode((0, 1)).zcoord == ZephyrCoord(0, 0, None, 0, 0) + + def test_direction_node_kind(self) -> None: + for u in self.u_vals: + for j in self.j_vals: + for m in self.m_vals: + w_vals = range(2*m+1) + z_vals = range(m) + for w in w_vals: + for z in z_vals: + for t in self.t_vals: + k_vals = range(t) + for k in k_vals: + zn = ZNode(coord=(u, w, k, j, z), shape=(m, t)) + self.assertEqual(zn.direction, u) + if u == 0: + self.assertTrue(zn.is_vertical()) + self.assertEqual(zn.node_kind(), "vertical") + else: + self.assertTrue(zn.is_horizontal()) + self.assertEqual(zn.node_kind(), "horizontal") + + def test_neighbor_kind(self) -> None: + zn = ZNode((0, 1)) + self.assertTrue(zn.neighbor_kind(ZNode((1, 0))) == "internal") + self.assertTrue(zn.neighbor_kind(ZNode((1, 2))) == "internal") + self.assertTrue(zn.neighbor_kind(ZNode((0, 3))) == "odd") + self.assertTrue(zn.neighbor_kind(ZNode((0, 5))) == "external") + self.assertTrue(zn.neighbor_kind(ZNode((0, 7)))is None) + self.assertTrue(zn.neighbor_kind(ZNode((1, 6)))is None) + + def test_internal_generator(self) -> None: + zn1 = ZNode((0, 1)) + set_internal1 = {x for x in zn1.internal_neighbors_generator()} + expected1 = {ZNode((1, 0)), ZNode((1, 2))} + self.assertEqual(set_internal1, expected1) + + zn2 = ZNode((0, 1, 0), ZShape(t=4)) + set_internal2 = {x for x in zn2.internal_neighbors_generator()} + expected2 = {ZNode((1, 0, k), ZShape(t=4)) for k in range(4)} | {ZNode((1, 2, k), ZShape(t=4)) for k in range(4)} + self.assertEqual(set_internal2, expected2) + + def test_external_generator(self) -> None: + zn = ZNode((0, 1)) + set_external = {x for x in zn.external_neighbors_generator()} + expected = {ZNode((0, 5))} + self.assertEqual(set_external, expected) + + zn2 = ZNode((0, 1, 2), ZShape(t=4)) + set_external2 = {x for x in zn2.external_neighbors_generator()} + expected2 = {ZNode((0, 5, 2), ZShape(t=4))} + self.assertEqual(set_external2, expected2) + + def test_odd_generator(self) -> None: + zn = ZNode((0, 1)) + set_odd = {x for x in zn.odd_neighbors_generator()} + expected = {ZNode((0, 3))} + self.assertEqual(set_odd, expected) + + zn2 = ZNode((0, 1, 2), ZShape(t=4)) + set_odd2 = {x for x in zn2.odd_neighbors_generator()} + expected2 = {ZNode((0, 3, 2), ZShape(t=4))} + self.assertEqual(set_odd2, expected2) + + def test_degree(self) -> None: + for (x, y), m in self.midgrid_xyms: + qzn1 = ZNode(coord=(x, y), shape=ZShape(m=m)) + self.assertEqual(len(qzn1.neighbors()), 8) + self.assertEqual(qzn1.degree(), 8) + self.assertEqual(qzn1.degree(nbr_kind="internal"), 4) + self.assertEqual(qzn1.degree(nbr_kind="external"), 2) + self.assertEqual(qzn1.degree(nbr_kind="odd"), 2) + for t in [1, 4, 6]: + zn1 = ZNode(coord=(x, y, 0), shape=ZShape(m=m, t=t)) + self.assertEqual(zn1.degree(), 4*t+4) + self.assertEqual(zn1.degree(nbr_kind="internal"), 4*t) + self.assertEqual(zn1.degree(nbr_kind="external"), 2) + self.assertEqual(zn1.degree(nbr_kind="odd"), 2) + + qzn2 = ZNode(coord=(0, 1)) + self.assertEqual(qzn2.degree(), 4) + self.assertEqual(qzn2.degree(nbr_kind="internal"), 2) + self.assertEqual(qzn2.degree(nbr_kind="external"), 1) + self.assertEqual(qzn2.degree(nbr_kind="odd"), 1) + for t in [1, 4, 6]: + zn2 = ZNode(coord=(0, 1, 0), shape=ZShape(t=t)) + self.assertEqual(zn2.degree(), 2*t+2) + self.assertEqual(zn2.degree(nbr_kind="internal"), 2*t) + self.assertEqual(zn2.degree(nbr_kind="external"), 1) + self.assertEqual(zn2.degree(nbr_kind="odd"), 1) + + qzn3 = ZNode((24, 5), ZShape(m=6)) + self.assertEqual(qzn3.degree(), 6) + self.assertEqual(qzn3.degree(nbr_kind="internal"), 2) + self.assertEqual(qzn3.degree(nbr_kind="external"), 2) + self.assertEqual(qzn3.degree(nbr_kind="odd"), 2) + for t in [1, 5, 6]: + zn3 = ZNode((24, 5, 0), ZShape(m=6, t=t)) + self.assertEqual(zn3.degree(), 2*t+4) + self.assertEqual(zn3.degree(nbr_kind="internal"), 2*t) + self.assertEqual(zn3.degree(nbr_kind="external"), 2) + self.assertEqual(zn3.degree(nbr_kind="odd"), 2) + + qzn4 = ZNode((24, 5)) + self.assertEqual(qzn4.degree(), 8) + self.assertEqual(qzn4.degree(nbr_kind="internal"), 4) + self.assertEqual(qzn4.degree(nbr_kind="external"), 2) + self.assertEqual(qzn4.degree(nbr_kind="odd"), 2) + for t in [1, 5, 6]: + zn4 = ZNode((24, 5, 0), ZShape(t=t)) + self.assertEqual(zn4.degree(), 4*t+4) + self.assertEqual(zn4.degree(nbr_kind="internal"), 4*t) + self.assertEqual(zn4.degree(nbr_kind="external"), 2) + self.assertEqual(zn4.degree(nbr_kind="odd"), 2) + + diff --git a/tests/utils/zephyr/test_plane_shift.py b/tests/utils/zephyr/test_plane_shift.py new file mode 100644 index 00000000..0daee3aa --- /dev/null +++ b/tests/utils/zephyr/test_plane_shift.py @@ -0,0 +1,75 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from itertools import combinations +import unittest +from minorminer.utils.zephyr.plane_shift import PlaneShift + + + + +class TestPlaneShift(unittest.TestCase): + def setUp(self) -> None: + shifts = [ + (0, 2), (-3, -1), (-2, 0), (1, 1), + (1, -3), (-4, 6), (10, 4), (0, 0), + ] + self.shifts = shifts + + + def test_valid_input_runs(self) -> None: + for shift in self.shifts: + PlaneShift(*shift) + + def test_invalid_input_gives_error(self) -> None: + invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.)] + with self.assertRaises(TypeError): + for invalid_type_ in invalid_input_types: + PlaneShift(*invalid_type_) + + invalid_input_vals = [(4, 1), (0, 1)] + with self.assertRaises(ValueError): + for invalid_val_ in invalid_input_vals: + PlaneShift(*invalid_val_) + + def test_multiply(self) -> None: + for shift in self.shifts: + for scale in [0, 1, 2, 5, 10, -3]: + self.assertEqual( + PlaneShift(shift[0]*scale, shift[1]*scale), + PlaneShift(*shift)*scale + ) + self.assertEqual( + PlaneShift(shift[0]*scale, shift[1]*scale), + scale*PlaneShift(*shift) + ) + + def test_add(self) -> None: + for s0, s1 in combinations(self.shifts, 2): + self.assertEqual( + PlaneShift(*s0)+PlaneShift(*s1), + PlaneShift(s0[0]+s1[0], s0[1]+s1[1]) + ) + + def test_mul(self) -> None: + self.assertEqual(1.5* PlaneShift(0, -4), PlaneShift(0, -6)) + with self.assertRaises(ValueError): + 0.5*PlaneShift(0, -6) + PlaneShift(0, -4)*0.75 + with self.assertRaises(TypeError): + "hello"*PlaneShift(0, -4) diff --git a/tests/utils/zephyr/test_qfloor.py b/tests/utils/zephyr/test_qfloor.py new file mode 100644 index 00000000..8553cbe6 --- /dev/null +++ b/tests/utils/zephyr/test_qfloor.py @@ -0,0 +1,229 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from itertools import product +import unittest +from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.node_edge import ZNode, ZShape, ZEdge +from minorminer.utils.zephyr.qfloor import QuoTile, QuoFloor + + + + +class TestQuoTile(unittest.TestCase): + def test_quo_tile_runs(self) -> None: + zn = ZNode((0, 1), ZShape(m=6)) + shifts0 = [(1, -1), (0, 2), (1, 1), (1, 3)] + zns = [zn + PlaneShift(*x) for x in shifts0] + QuoTile(zns=zns) + + def test_zns_define(self) -> None: + seed0 = ZNode((0, 1), ZShape(m=6)) + shifts0 = [PlaneShift(1, 1), PlaneShift(0, 2)] + zns0 = [seed0+shift for shift in shifts0] + seed1 = seed0+PlaneShift(0, 2) + shifts1 = [PlaneShift(1, -1), PlaneShift(0, 0)] + zns1 = [seed1+shift for shift in shifts1] + zns2 = [ZNode((1, 2), ZShape(m=6)), ZNode((0, 0, 1, 0), ZShape(m=6))] + qtile0 = QuoTile(zns=zns0) + qtile1 = QuoTile(zns=zns1) + qtile2 = QuoTile(zns=zns2) + self.assertEqual(qtile0, qtile1) + self.assertEqual(qtile0, qtile2) + self.assertEqual(qtile2, qtile1) + shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] + shifts_repeat = 2*shifts + for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: + qtile0 = QuoTile(zns=[zn+shift for shift in shifts]) + qtile1 = QuoTile(zns=[zn+shift for shift in shifts_repeat]) + self.assertEqual(qtile0, qtile1) + + def test_edges(self) -> None: + shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] + for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: + edges_ = QuoTile([zn+shift for shift in shifts]).edges() + for xy in edges_: + self.assertTrue(isinstance(xy, ZEdge)) + self.assertTrue(isinstance(xy[0], ZNode)) + self.assertTrue(isinstance(xy[1], ZNode)) + self.assertTrue(xy[0].is_neighbor(xy[1])) + + + + +class TestQuoFloor(unittest.TestCase): + valid_connector = QuoFloor.tile_connector0 + + def test_init_runs_as_expected(self) -> None: + corner_qtile = [ZNode((0, 1)), ZNode((1, 0)), ZNode((1, 2)), ZNode((2, 3))] + bad_corner_qtile = [ZNode((0, 1)), ((1, 0))] + not_imp_connector = {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 2)} + QuoFloor(corner_qtile=corner_qtile, dim = (100, 100)) + for m in [6, 8]: + corner_qtile2 = [ZNode((0, 1), ZShape(m=m)), ZNode((1, 0), ZShape(m=m))] + for dim in [(1, 1), (m, m), (m, 1), (1, m), (m//2, m//2)]: + qfl = QuoFloor(corner_qtile=corner_qtile2, dim=dim) + self.assertEqual(qfl.dim, dim) + self.assertEqual(set(qfl.corner_qtile.zns), set(corner_qtile2)) + with self.assertRaises(TypeError): + qfl.corner_qtile = bad_corner_qtile + with self.assertRaises(ValueError): + qfl.dim = (1, m+1) + with self.assertRaises(ValueError): + qfl.dim = (1, 0) + with self.assertRaises(ValueError): + qfl.dim = (0, 1) + with self.assertRaises(NotImplementedError): + qfl.tile_connector = not_imp_connector + with self.assertRaises(ValueError): + QuoFloor(corner_qtile=corner_qtile2, dim = (m+1, m+1)) + corner_qtile3 = [ZNode((0, 1)), ZNode((1, 0), ZShape(m=m))] + with self.assertRaises(ValueError): + QuoFloor(corner_qtile=corner_qtile3, dim = (1, 1)) + with self.assertRaises(TypeError): + QuoFloor(corner_qtile=bad_corner_qtile, dim = (1, 1)) + bad_coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + [(6, 3)] + bad_corner_qtile2 = [ZNode(coord=coord) for coord in bad_coords] + with self.assertRaises(ValueError): + QuoFloor(corner_qtile=bad_corner_qtile2, dim=(1, 1)) + + def test_qtile_xy(self) -> None: + coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) + qfl = QuoFloor(corner_qtile=corner_qtile, dim=(10, 7)) + # Check the ZNodes in each tile are as expected + for x, y in product([1, 6, 8], [1, 2, 3, 6]): + xy_tile = qfl.qtile_xy(x, y) + coords_xy = sorted([(a+4*x, b+4*y) for (a, b) in coords]) + xy_tile_expected_zns = [ZNode(coord = c) for c in coords_xy] + self.assertEqual(xy_tile.zns, xy_tile_expected_zns) + + def test_qtiles(self) -> None: + coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) + for a, b in product([1, 6, 8], [1, 3, 6]): + qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) + tiles = qfl.qtiles + # Check it has m*n values in it + self.assertEqual(len(tiles), a*b) + # Check each tile contains the same number of znodes, none is a repeat + for xy, xy_tile in tiles.items(): + self.assertEqual(len(set(xy_tile)), len(set(coords))) + self.assertEqual(len(set(xy_tile)), len(xy_tile)) + self.assertTrue(xy in product(range(a), range(b))) + for m in [4, 6, 7]: + corner_qtile = QuoTile([ZNode(coord=coord, shape=ZShape(m=m)) for coord in coords]) + for a, b in product([1, 6, 8], [1, 3, 6]): + if a <= m and b <= m: + qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) + tiles = qfl.qtiles + # Check it has m*n values in it + self.assertEqual(len(tiles), a*b) + # Check each tile contains the same number of znodes, none is a repeat + for xy, xy_tile in tiles.items(): + self.assertEqual(len(set(xy_tile)), len(set(coords))) + self.assertEqual(len(set(xy_tile)), len(xy_tile)) + self.assertTrue(xy in product(range(a), range(b))) + else: + with self.assertRaises(ValueError): + QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) + + def test_zns(self) -> None: + coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(3)] + corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) + for m, n in product([1, 6, 8], [1, 2, 3, 6]): + qfl = QuoFloor(corner_qtile=corner_qtile, dim=(m, n)) + tiles_zns = qfl.zns + # Check it has m*n values in it + self.assertEqual(len(tiles_zns), m*n) + # Check each tile contains the same number of znodes, none is a repeat + for xy, xy_zns in tiles_zns.items(): + self.assertEqual(len(set(xy_zns)), len(set(coords))) + self.assertEqual(len(set(xy_zns)), len(xy_zns)) + self.assertTrue(xy in product(range(m), range(n))) + + def test_ver_zns(self) -> None: + coords = [(0, 1), (1, 0)] + nodes = [ZNode(coord=c) for c in coords] + nodes += [n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + for dim in [(1, 1), (3, 5), (6, 6)]: + qfl = QuoFloor(corner_qtile=nodes, dim=dim) + qfl_ver = qfl.ver_zns + for xy , xy_ver in qfl_ver.items(): + self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) + for v in xy_ver: + self.assertTrue(v.is_vertical()) + self.assertEqual(len(qfl_ver), dim[0]*dim[1]) + + def test_hor_zns(self) -> None: + coords = [(0, 1), (1, 2)] + nodes = [ZNode(coord=c) for c in coords] + nodes += [n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + for dim in [(4, 7), (1, 1)]: + qfl = QuoFloor(corner_qtile=nodes, dim=dim) + qfl_hor = qfl.hor_zns + for xy , xy_hor in qfl_hor.items(): + self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) + for v in xy_hor: + self.assertTrue(v.is_horizontal()) + self.assertEqual(len(qfl_hor), dim[0]*dim[1]) + for m in [4, 5]: + nodes2 = [ZNode(coord=c, shape=ZShape(m=m)) for c in coords] + nodes2 += [n + p for n in nodes2 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + for dim in [(4, 7), (1, 1), (4, 4), (2, 3)]: + if dim[0] <= m and dim[1] <= m: + qfl = QuoFloor(corner_qtile=nodes2, dim=dim) + qfl_hor = qfl.hor_zns + for xy , xy_hor in qfl_hor.items(): + self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) + for v in xy_hor: + self.assertTrue(v.is_horizontal()) + self.assertEqual(len(qfl_hor), dim[0]*dim[1]) + else: + with self.assertRaises(ValueError): + QuoFloor(corner_qtile=nodes2, dim=dim) + + + def test_quo_ext_paths(self) -> None: + coords1 = [(2, 1), (1, 2)] + nodes1 = [ZNode(coord=c) for c in coords1] + nodes1 += [n + p for n in nodes1 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + coords2 = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(3)] + nodes2 = [ZNode(coord=coord) for coord in coords2] + for dim in [(1, 1), (5, 5), (3, 6)]: + floors = [QuoFloor(corner_qtile=nodes1, dim=dim), QuoFloor(corner_qtile=nodes2, dim=dim)] + for qfl in floors: + ext_paths = qfl.quo_ext_paths + self.assertTrue("col" in ext_paths) + self.assertTrue("row" in ext_paths) + col_ext_paths = ext_paths["col"] + row_ext_paths = ext_paths["row"] + self.assertEqual(len(col_ext_paths), dim[0]) + self.assertEqual(len(row_ext_paths), dim[1]) + for list_c in col_ext_paths.values(): + # Check no repeat + uwjs_c = [x[0] for x in list_c] + self.assertEqual(len(uwjs_c), len(set(uwjs_c))) + for _, zse in list_c: + self.assertEqual(zse.z_end-zse.z_start+1, dim[1]) + for list_r in row_ext_paths.values(): + # Check no repeat + uwjs_r = [x[0] for x in list_r] + self.assertEqual(len(uwjs_r), len(set(uwjs_r))) + for _, zse in list_r: + self.assertEqual(zse.z_end-zse.z_start+1, dim[0]) \ No newline at end of file diff --git a/tests/utils/test_zephyr.py b/tests/utils/zephyr/test_zephyr.py similarity index 100% rename from tests/utils/test_zephyr.py rename to tests/utils/zephyr/test_zephyr.py diff --git a/tests/utils/zephyr/test_zephyr_base.py b/tests/utils/zephyr/test_zephyr_base.py new file mode 100644 index 00000000..687d7f3e --- /dev/null +++ b/tests/utils/zephyr/test_zephyr_base.py @@ -0,0 +1,87 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + +import unittest +from itertools import product +import numpy as np +from dwave.cloud import Client +from dwave.system import DWaveSampler +import dwave_networkx as dnx + +class ZephyrBaseTest(unittest.TestCase): + def setUp(self): + self.initialize_samplers() + self.initialize_rngs() + self.initialize_node_del_percents() + self.initialize_edge_del_percents() + self.initialize_zeph_ms() + self.initialize_graphs() + + def initialize_samplers(self): + with Client.from_config(client="qpu") as client: + zeph_solvers = client.get_solvers( + topology__type__eq="zephyr", + ) + self.samplers = [DWaveSampler(solver=z_solver.id) for z_solver in zeph_solvers] + + def initialize_rngs(self): + self.rngs = [ + np.random.default_rng(seed=1), + np.random.default_rng(seed=10), + ] + + def initialize_zeph_ms(self): + self.zeph_ms = [3, 6] + + def initialize_node_del_percents(self): + self.node_del_percents = [0, 0.03] + + + def initialize_edge_del_percents(self): + self.edge_del_percents = [0, 0.02] + + + def initialize_graphs(self): + self.graphs = list() + for rng in self.rngs: + for m in self.zeph_ms: + for node_del_per, edge_del_per in product( + self.node_del_percents, self.edge_del_percents + ): + G = dnx.zephyr_graph(m=m, coordinates=True) + num_nodes_to_remove = int(node_del_per * G.number_of_nodes()) + nodes_to_remove = [ + tuple(v) + for v in rng.choice(G.nodes(), num_nodes_to_remove) + ] + G.remove_nodes_from(nodes_to_remove) + num_edges_to_remove = int(edge_del_per * G.number_of_edges()) + edges_to_remove = [ + (tuple(u), tuple(v)) + for (u, v) in rng.choice(G.edges(), num_edges_to_remove) + ] + G.remove_edges_from(edges_to_remove) + G_dict = { + "G": G, + "m": m, + "num_nodes": G.number_of_nodes(), + "num_edges": G.number_of_edges(), + "num_missing_nodes": num_nodes_to_remove, + "num_missing_edges": dnx.zephyr_graph(m=m).number_of_edges()-G.number_of_edges(), + "num_extra_missing_edges": num_edges_to_remove, + } + self.graphs.append(G_dict) \ No newline at end of file diff --git a/tests/utils/zephyr/test_zephyr_survey.py b/tests/utils/zephyr/test_zephyr_survey.py new file mode 100644 index 00000000..d22cee23 --- /dev/null +++ b/tests/utils/zephyr/test_zephyr_survey.py @@ -0,0 +1,87 @@ +# Copyright 2025 D-Wave Systems Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# ================================================================================================ + + + +from tests.utils.zephyr.test_zephyr_base import ZephyrBaseTest +from minorminer.utils.zephyr.survey import ZSurvey + + +class TestZephyrSurvey(ZephyrBaseTest): + def setUp(self) -> None: + super().setUp() + + def test_get_zephyr_shape_coord_sampler(self) -> None: + for z_sampler in self.samplers: + (m, t), coord = ZSurvey(z_sampler).get_shape_coord(z_sampler) + sampler_top = z_sampler.properties["topology"] + self.assertTrue(m, sampler_top.get("shape")[0]) + self.assertTrue(t, sampler_top.get("shape")[1]) + if coord == "int": + for v in z_sampler.nodelist: + self.assertTrue(isinstance(v, int)) + elif coord == "coordinates": + for v in z_sampler.nodelist: + self.assertTrue(isinstance(v, tuple)) + + + def test_get_zephyr_shape_coord_graph(self) -> None: + for G_dict in self.graphs: + z_graph = G_dict["G"] + (m, t), coord = ZSurvey(z_graph).get_shape_coord(z_graph) + G_info = z_graph.graph + self.assertTrue(m, G_info.get("rows")) + self.assertTrue(t, G_info.get("tile")) + self.assertTrue(coord, G_info.get("labels")) + + + def test_zephyr_survey_runs_sampler(self) -> None: + for z_sampler in self.samplers: + try: + ZSurvey(z_sampler) + except Exception as e: + self.fail(f"ZephyrSurvey raised an exception {e} when running with sampler = {z_sampler}") + + def test_zephyr_survey_runs_graph(self) -> None: + for G_dict in self.graphs: + z_graph = G_dict["G"] + try: + ZSurvey(z_graph) + except Exception as e: + self.fail(f"ZephyrSurvey raised an exception {e} when running with graph = {z_graph}") + + def test_num_nodes(self) -> None: + for z_sampler in self.samplers: + num_nodes = len(z_sampler.nodelist) + self.assertEqual(len(ZSurvey(z_sampler).nodes), num_nodes) + for G_dict in self.graphs: + z_graph = G_dict["G"] + num_nodes = G_dict["num_nodes"] + self.assertEqual(len(ZSurvey(z_graph).nodes), num_nodes) + + def test_num_edges(self) -> None: + for z_sampler in self.samplers: + num_edges = len(z_sampler.edgelist) + self.assertEqual(len(ZSurvey(z_sampler).edges), num_edges) + for G_dict in self.graphs: + z_graph = G_dict["G"] + num_edges = G_dict["num_edges"] + self.assertEqual(len(ZSurvey(z_graph).edges), num_edges) + + def test_external_paths(self) -> None: + for z_sampler in self.samplers: + sur = ZSurvey(z_sampler) + sur.calculate_external_paths_stretch() \ No newline at end of file From 26280617dae4420be3cf305991b00b58ff41aa96 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Thu, 13 Mar 2025 11:15:05 -0700 Subject: [PATCH 002/127] Reformat --- tests/utils/zephyr/test_coordinate_systems.py | 21 ++-- tests/utils/zephyr/test_node_edge.py | 87 ++++++++------- tests/utils/zephyr/test_plane_shift.py | 44 ++++---- tests/utils/zephyr/test_qfloor.py | 100 ++++++++++-------- tests/utils/zephyr/test_zephyr.py | 17 ++- tests/utils/zephyr/test_zephyr_base.py | 26 +++-- tests/utils/zephyr/test_zephyr_survey.py | 17 +-- 7 files changed, 165 insertions(+), 147 deletions(-) diff --git a/tests/utils/zephyr/test_coordinate_systems.py b/tests/utils/zephyr/test_coordinate_systems.py index 9ca91661..57f3f331 100644 --- a/tests/utils/zephyr/test_coordinate_systems.py +++ b/tests/utils/zephyr/test_coordinate_systems.py @@ -16,7 +16,10 @@ import unittest -from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord, cartesian_to_zephyr, zephyr_to_cartesian + +from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, + cartesian_to_zephyr, zephyr_to_cartesian) + class TestCoordinateSystems(unittest.TestCase): @@ -27,25 +30,29 @@ def test_cartesian_to_zephyr_runs(self): cartesian_to_zephyr(ccoord=ccoord) def test_cartesian_to_zephyr(self): - self.assertEqual(ZephyrCoord(0, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(0, 1, None))) - self.assertEqual(ZephyrCoord(1, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(1, 0, None))) + self.assertEqual( + ZephyrCoord(0, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(0, 1, None)) + ) + self.assertEqual( + ZephyrCoord(1, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(1, 0, None)) + ) self.assertEqual(CartesianCoord(5, 12, 3), zephyr_to_cartesian(ZephyrCoord(1, 6, 3, 0, 1))) def test_zephyr_to_cartesian_runs(self): - uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] + uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] zcoords = [ZephyrCoord(*uwkjz) for uwkjz in uwkjzs] for zcoord in zcoords: zephyr_to_cartesian(zcoord=zcoord) def test_coordinate_systems_match(self): - valid_uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] + valid_uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] zcoords = [ZephyrCoord(*uwkjz) for uwkjz in valid_uwkjzs] for zcoord in zcoords: ccoord = zephyr_to_cartesian(zcoord=zcoord) self.assertEqual(zcoord, cartesian_to_zephyr(ccoord=ccoord)) - + valid_xyks = [(0, 1), (1, 0, None), (12, -3)] ccoords = [CartesianCoord(*xyk) for xyk in valid_xyks] for ccoord in ccoords: zcoord = cartesian_to_zephyr(ccoord=ccoord) - self.assertEqual(ccoord, zephyr_to_cartesian(zcoord=zcoord)) \ No newline at end of file + self.assertEqual(ccoord, zephyr_to_cartesian(zcoord=zcoord)) diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py index eb3ab271..7716de6d 100644 --- a/tests/utils/zephyr/test_node_edge.py +++ b/tests/utils/zephyr/test_node_edge.py @@ -17,10 +17,10 @@ import unittest from itertools import product -from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, Edge, ZShape -from minorminer.utils.zephyr.plane_shift import PlaneShift -from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord +from minorminer.utils.zephyr.coordinate_systems import CartesianCoord, ZephyrCoord +from minorminer.utils.zephyr.node_edge import Edge, ZEdge, ZNode, ZShape +from minorminer.utils.zephyr.plane_shift import PlaneShift class TestEdge(unittest.TestCase): @@ -30,36 +30,36 @@ def test_valid_input_runs(self) -> None: def test_invalid_input_raises_error(self) -> None: with self.assertRaises(TypeError): - Edge(2, 'string') + Edge(2, "string") Edge(2, ZNode(ZephyrCoord(0, 10, 3, 1, 3), ZShape(t=4))) def test_equal(self) -> None: self.assertEqual(Edge(2, 4), Edge(4, 2)) - class TestZEdge(unittest.TestCase): def test_valid_input_runs(self) -> None: valid_edges = [ ( ZNode(coord=ZephyrCoord(0, 10, 3, 1, 3), shape=ZShape(6, 4)), - ZNode(coord=ZephyrCoord(0, 10, 3, 1, 2), shape=ZShape(6, 4)) - ), + ZNode(coord=ZephyrCoord(0, 10, 3, 1, 2), shape=ZShape(6, 4)), + ), ( ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), - ZNode(coord=CartesianCoord(4, 1, 2), shape=(6, 3)) - ), + ZNode(coord=CartesianCoord(4, 1, 2), shape=(6, 3)), + ), (ZNode(coord=(1, 6)), ZNode(coord=(5, 6))), - ] + ] for x, y in valid_edges: ZEdge(x, y) def test_invalid_input_raises_error(self): with self.assertRaises((TypeError, ValueError)): ZEdge(2, 4) - ZEdge(ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3))) - - + ZEdge( + ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), + ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), + ) class TestZNode(unittest.TestCase): @@ -75,10 +75,15 @@ def setUp(self): self.invalid_t_vals = [0, 2.5, -2] self.invalid_z_vals = [-1, None] xym_vals = [ - ((0, 3), 1), ((5, 2), 6), ((16, 1), 4), - ((1, 12), 3), ((3, 0), 4), ((5, 4), 6), - ((0, 3), 6), ((6, 3), 5), - ] + ((0, 3), 1), + ((5, 2), 6), + ((16, 1), 4), + ((1, 12), 3), + ((3, 0), 4), + ((5, 4), 6), + ((0, 3), 6), + ((6, 3), 5), + ] self.xyms = xym_vals + [(xy, None) for xy, _ in xym_vals] self.left_up_xyms = [((0, 3), 6), ((0, 3), None), ((11, 0), None), ((0, 5), 8)] self.right_down_xyms = [((1, 12), 3), ((16, 1), 4)] @@ -92,7 +97,7 @@ def test_zephyr_node_runs(self) -> None: for u in self.u_vals: for j in self.j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) z_vals = range(m) for w in w_vals: for z in z_vals: @@ -112,7 +117,7 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: ((0, 17), 4), ((0, 0, 0), 1), ((-1, 2), None), - ] + ] for xy, m in invalid_xyms: with self.assertRaises((ValueError, TypeError)): ZNode(coord=xy, shape=ZShape(m=m)) @@ -120,7 +125,7 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: for u in self.invalid_u_vals: for j in self.j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) z_vals = range(m) for w in w_vals: for z in z_vals: @@ -147,21 +152,21 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: for u in self.u_vals: for j in self.j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) z_vals = range(m) for w in w_vals: for z in z_vals: for t in self.t_vals: invalid_k_vals = [-1, t, 2.5, None] for k in invalid_k_vals: - with self.assertRaises((ValueError, TypeError)): + with self.assertRaises((ValueError, TypeError)): ZNode(coord=(u, w, k, j, z), shape=(m, t)) # All good except j_vals for u in self.u_vals: for j in self.invalid_j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) z_vals = range(m) for w in w_vals: for z in z_vals: @@ -174,7 +179,7 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: for u in self.u_vals: for j in self.j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) invalid_z_vals = [None, -1, m, 1.5] for w in w_vals: for z in invalid_z_vals: @@ -234,7 +239,7 @@ def test_add_sub(self) -> None: left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] for pc in midgrid_pcs + right_down_pcs + left_up_pcs: - self.assertEqual(pc+PlaneShift(0, 0), pc) + self.assertEqual(pc + PlaneShift(0, 0), pc) def test_neighbors_generator_runs(self) -> None: for _ in ZNode((1, 12, 4), ZShape(t=6)).neighbors_generator(): @@ -249,7 +254,7 @@ def test_direction_node_kind(self) -> None: for u in self.u_vals: for j in self.j_vals: for m in self.m_vals: - w_vals = range(2*m+1) + w_vals = range(2 * m + 1) z_vals = range(m) for w in w_vals: for z in z_vals: @@ -271,8 +276,8 @@ def test_neighbor_kind(self) -> None: self.assertTrue(zn.neighbor_kind(ZNode((1, 2))) == "internal") self.assertTrue(zn.neighbor_kind(ZNode((0, 3))) == "odd") self.assertTrue(zn.neighbor_kind(ZNode((0, 5))) == "external") - self.assertTrue(zn.neighbor_kind(ZNode((0, 7)))is None) - self.assertTrue(zn.neighbor_kind(ZNode((1, 6)))is None) + self.assertTrue(zn.neighbor_kind(ZNode((0, 7))) is None) + self.assertTrue(zn.neighbor_kind(ZNode((1, 6))) is None) def test_internal_generator(self) -> None: zn1 = ZNode((0, 1)) @@ -282,7 +287,9 @@ def test_internal_generator(self) -> None: zn2 = ZNode((0, 1, 0), ZShape(t=4)) set_internal2 = {x for x in zn2.internal_neighbors_generator()} - expected2 = {ZNode((1, 0, k), ZShape(t=4)) for k in range(4)} | {ZNode((1, 2, k), ZShape(t=4)) for k in range(4)} + expected2 = {ZNode((1, 0, k), ZShape(t=4)) for k in range(4)} | { + ZNode((1, 2, k), ZShape(t=4)) for k in range(4) + } self.assertEqual(set_internal2, expected2) def test_external_generator(self) -> None: @@ -290,7 +297,7 @@ def test_external_generator(self) -> None: set_external = {x for x in zn.external_neighbors_generator()} expected = {ZNode((0, 5))} self.assertEqual(set_external, expected) - + zn2 = ZNode((0, 1, 2), ZShape(t=4)) set_external2 = {x for x in zn2.external_neighbors_generator()} expected2 = {ZNode((0, 5, 2), ZShape(t=4))} @@ -317,8 +324,8 @@ def test_degree(self) -> None: self.assertEqual(qzn1.degree(nbr_kind="odd"), 2) for t in [1, 4, 6]: zn1 = ZNode(coord=(x, y, 0), shape=ZShape(m=m, t=t)) - self.assertEqual(zn1.degree(), 4*t+4) - self.assertEqual(zn1.degree(nbr_kind="internal"), 4*t) + self.assertEqual(zn1.degree(), 4 * t + 4) + self.assertEqual(zn1.degree(nbr_kind="internal"), 4 * t) self.assertEqual(zn1.degree(nbr_kind="external"), 2) self.assertEqual(zn1.degree(nbr_kind="odd"), 2) @@ -328,9 +335,9 @@ def test_degree(self) -> None: self.assertEqual(qzn2.degree(nbr_kind="external"), 1) self.assertEqual(qzn2.degree(nbr_kind="odd"), 1) for t in [1, 4, 6]: - zn2 = ZNode(coord=(0, 1, 0), shape=ZShape(t=t)) - self.assertEqual(zn2.degree(), 2*t+2) - self.assertEqual(zn2.degree(nbr_kind="internal"), 2*t) + zn2 = ZNode(coord=(0, 1, 0), shape=ZShape(t=t)) + self.assertEqual(zn2.degree(), 2 * t + 2) + self.assertEqual(zn2.degree(nbr_kind="internal"), 2 * t) self.assertEqual(zn2.degree(nbr_kind="external"), 1) self.assertEqual(zn2.degree(nbr_kind="odd"), 1) @@ -341,8 +348,8 @@ def test_degree(self) -> None: self.assertEqual(qzn3.degree(nbr_kind="odd"), 2) for t in [1, 5, 6]: zn3 = ZNode((24, 5, 0), ZShape(m=6, t=t)) - self.assertEqual(zn3.degree(), 2*t+4) - self.assertEqual(zn3.degree(nbr_kind="internal"), 2*t) + self.assertEqual(zn3.degree(), 2 * t + 4) + self.assertEqual(zn3.degree(nbr_kind="internal"), 2 * t) self.assertEqual(zn3.degree(nbr_kind="external"), 2) self.assertEqual(zn3.degree(nbr_kind="odd"), 2) @@ -353,9 +360,7 @@ def test_degree(self) -> None: self.assertEqual(qzn4.degree(nbr_kind="odd"), 2) for t in [1, 5, 6]: zn4 = ZNode((24, 5, 0), ZShape(t=t)) - self.assertEqual(zn4.degree(), 4*t+4) - self.assertEqual(zn4.degree(nbr_kind="internal"), 4*t) + self.assertEqual(zn4.degree(), 4 * t + 4) + self.assertEqual(zn4.degree(nbr_kind="internal"), 4 * t) self.assertEqual(zn4.degree(nbr_kind="external"), 2) self.assertEqual(zn4.degree(nbr_kind="odd"), 2) - - diff --git a/tests/utils/zephyr/test_plane_shift.py b/tests/utils/zephyr/test_plane_shift.py index 0daee3aa..9180215a 100644 --- a/tests/utils/zephyr/test_plane_shift.py +++ b/tests/utils/zephyr/test_plane_shift.py @@ -15,29 +15,32 @@ # ================================================================================================ - -from itertools import combinations import unittest -from minorminer.utils.zephyr.plane_shift import PlaneShift - +from itertools import combinations +from minorminer.utils.zephyr.plane_shift import PlaneShift class TestPlaneShift(unittest.TestCase): def setUp(self) -> None: shifts = [ - (0, 2), (-3, -1), (-2, 0), (1, 1), - (1, -3), (-4, 6), (10, 4), (0, 0), - ] + (0, 2), + (-3, -1), + (-2, 0), + (1, 1), + (1, -3), + (-4, 6), + (10, 4), + (0, 0), + ] self.shifts = shifts - def test_valid_input_runs(self) -> None: for shift in self.shifts: PlaneShift(*shift) def test_invalid_input_gives_error(self) -> None: - invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.)] + invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.0)] with self.assertRaises(TypeError): for invalid_type_ in invalid_input_types: PlaneShift(*invalid_type_) @@ -51,25 +54,22 @@ def test_multiply(self) -> None: for shift in self.shifts: for scale in [0, 1, 2, 5, 10, -3]: self.assertEqual( - PlaneShift(shift[0]*scale, shift[1]*scale), - PlaneShift(*shift)*scale - ) + PlaneShift(shift[0] * scale, shift[1] * scale), PlaneShift(*shift) * scale + ) self.assertEqual( - PlaneShift(shift[0]*scale, shift[1]*scale), - scale*PlaneShift(*shift) - ) + PlaneShift(shift[0] * scale, shift[1] * scale), scale * PlaneShift(*shift) + ) def test_add(self) -> None: for s0, s1 in combinations(self.shifts, 2): self.assertEqual( - PlaneShift(*s0)+PlaneShift(*s1), - PlaneShift(s0[0]+s1[0], s0[1]+s1[1]) - ) + PlaneShift(*s0) + PlaneShift(*s1), PlaneShift(s0[0] + s1[0], s0[1] + s1[1]) + ) def test_mul(self) -> None: - self.assertEqual(1.5* PlaneShift(0, -4), PlaneShift(0, -6)) + self.assertEqual(1.5 * PlaneShift(0, -4), PlaneShift(0, -6)) with self.assertRaises(ValueError): - 0.5*PlaneShift(0, -6) - PlaneShift(0, -4)*0.75 + 0.5 * PlaneShift(0, -6) + PlaneShift(0, -4) * 0.75 with self.assertRaises(TypeError): - "hello"*PlaneShift(0, -4) + "hello" * PlaneShift(0, -4) diff --git a/tests/utils/zephyr/test_qfloor.py b/tests/utils/zephyr/test_qfloor.py index 8553cbe6..a4f549d6 100644 --- a/tests/utils/zephyr/test_qfloor.py +++ b/tests/utils/zephyr/test_qfloor.py @@ -15,14 +15,12 @@ # ================================================================================================ - -from itertools import product import unittest -from minorminer.utils.zephyr.plane_shift import PlaneShift -from minorminer.utils.zephyr.node_edge import ZNode, ZShape, ZEdge -from minorminer.utils.zephyr.qfloor import QuoTile, QuoFloor - +from itertools import product +from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape +from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.qfloor import QuoFloor, QuoTile class TestQuoTile(unittest.TestCase): @@ -35,10 +33,10 @@ def test_quo_tile_runs(self) -> None: def test_zns_define(self) -> None: seed0 = ZNode((0, 1), ZShape(m=6)) shifts0 = [PlaneShift(1, 1), PlaneShift(0, 2)] - zns0 = [seed0+shift for shift in shifts0] - seed1 = seed0+PlaneShift(0, 2) + zns0 = [seed0 + shift for shift in shifts0] + seed1 = seed0 + PlaneShift(0, 2) shifts1 = [PlaneShift(1, -1), PlaneShift(0, 0)] - zns1 = [seed1+shift for shift in shifts1] + zns1 = [seed1 + shift for shift in shifts1] zns2 = [ZNode((1, 2), ZShape(m=6)), ZNode((0, 0, 1, 0), ZShape(m=6))] qtile0 = QuoTile(zns=zns0) qtile1 = QuoTile(zns=zns1) @@ -47,16 +45,16 @@ def test_zns_define(self) -> None: self.assertEqual(qtile0, qtile2) self.assertEqual(qtile2, qtile1) shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] - shifts_repeat = 2*shifts + shifts_repeat = 2 * shifts for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: - qtile0 = QuoTile(zns=[zn+shift for shift in shifts]) - qtile1 = QuoTile(zns=[zn+shift for shift in shifts_repeat]) + qtile0 = QuoTile(zns=[zn + shift for shift in shifts]) + qtile1 = QuoTile(zns=[zn + shift for shift in shifts_repeat]) self.assertEqual(qtile0, qtile1) def test_edges(self) -> None: shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: - edges_ = QuoTile([zn+shift for shift in shifts]).edges() + edges_ = QuoTile([zn + shift for shift in shifts]).edges() for xy in edges_: self.assertTrue(isinstance(xy, ZEdge)) self.assertTrue(isinstance(xy[0], ZNode)) @@ -64,8 +62,6 @@ def test_edges(self) -> None: self.assertTrue(xy[0].is_neighbor(xy[1])) - - class TestQuoFloor(unittest.TestCase): valid_connector = QuoFloor.tile_connector0 @@ -73,17 +69,17 @@ def test_init_runs_as_expected(self) -> None: corner_qtile = [ZNode((0, 1)), ZNode((1, 0)), ZNode((1, 2)), ZNode((2, 3))] bad_corner_qtile = [ZNode((0, 1)), ((1, 0))] not_imp_connector = {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 2)} - QuoFloor(corner_qtile=corner_qtile, dim = (100, 100)) + QuoFloor(corner_qtile=corner_qtile, dim=(100, 100)) for m in [6, 8]: corner_qtile2 = [ZNode((0, 1), ZShape(m=m)), ZNode((1, 0), ZShape(m=m))] - for dim in [(1, 1), (m, m), (m, 1), (1, m), (m//2, m//2)]: + for dim in [(1, 1), (m, m), (m, 1), (1, m), (m // 2, m // 2)]: qfl = QuoFloor(corner_qtile=corner_qtile2, dim=dim) self.assertEqual(qfl.dim, dim) self.assertEqual(set(qfl.corner_qtile.zns), set(corner_qtile2)) with self.assertRaises(TypeError): qfl.corner_qtile = bad_corner_qtile with self.assertRaises(ValueError): - qfl.dim = (1, m+1) + qfl.dim = (1, m + 1) with self.assertRaises(ValueError): qfl.dim = (1, 0) with self.assertRaises(ValueError): @@ -91,36 +87,36 @@ def test_init_runs_as_expected(self) -> None: with self.assertRaises(NotImplementedError): qfl.tile_connector = not_imp_connector with self.assertRaises(ValueError): - QuoFloor(corner_qtile=corner_qtile2, dim = (m+1, m+1)) + QuoFloor(corner_qtile=corner_qtile2, dim=(m + 1, m + 1)) corner_qtile3 = [ZNode((0, 1)), ZNode((1, 0), ZShape(m=m))] with self.assertRaises(ValueError): - QuoFloor(corner_qtile=corner_qtile3, dim = (1, 1)) + QuoFloor(corner_qtile=corner_qtile3, dim=(1, 1)) with self.assertRaises(TypeError): - QuoFloor(corner_qtile=bad_corner_qtile, dim = (1, 1)) - bad_coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + [(6, 3)] + QuoFloor(corner_qtile=bad_corner_qtile, dim=(1, 1)) + bad_coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] + [(6, 3)] bad_corner_qtile2 = [ZNode(coord=coord) for coord in bad_coords] with self.assertRaises(ValueError): QuoFloor(corner_qtile=bad_corner_qtile2, dim=(1, 1)) def test_qtile_xy(self) -> None: - coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) qfl = QuoFloor(corner_qtile=corner_qtile, dim=(10, 7)) # Check the ZNodes in each tile are as expected for x, y in product([1, 6, 8], [1, 2, 3, 6]): xy_tile = qfl.qtile_xy(x, y) - coords_xy = sorted([(a+4*x, b+4*y) for (a, b) in coords]) - xy_tile_expected_zns = [ZNode(coord = c) for c in coords_xy] + coords_xy = sorted([(a + 4 * x, b + 4 * y) for (a, b) in coords]) + xy_tile_expected_zns = [ZNode(coord=c) for c in coords_xy] self.assertEqual(xy_tile.zns, xy_tile_expected_zns) def test_qtiles(self) -> None: - coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) for a, b in product([1, 6, 8], [1, 3, 6]): qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) tiles = qfl.qtiles # Check it has m*n values in it - self.assertEqual(len(tiles), a*b) + self.assertEqual(len(tiles), a * b) # Check each tile contains the same number of znodes, none is a repeat for xy, xy_tile in tiles.items(): self.assertEqual(len(set(xy_tile)), len(set(coords))) @@ -133,7 +129,7 @@ def test_qtiles(self) -> None: qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) tiles = qfl.qtiles # Check it has m*n values in it - self.assertEqual(len(tiles), a*b) + self.assertEqual(len(tiles), a * b) # Check each tile contains the same number of znodes, none is a repeat for xy, xy_tile in tiles.items(): self.assertEqual(len(set(xy_tile)), len(set(coords))) @@ -144,13 +140,13 @@ def test_qtiles(self) -> None: QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) def test_zns(self) -> None: - coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(3)] + coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(3)] corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) for m, n in product([1, 6, 8], [1, 2, 3, 6]): qfl = QuoFloor(corner_qtile=corner_qtile, dim=(m, n)) tiles_zns = qfl.zns # Check it has m*n values in it - self.assertEqual(len(tiles_zns), m*n) + self.assertEqual(len(tiles_zns), m * n) # Check each tile contains the same number of znodes, none is a repeat for xy, xy_zns in tiles_zns.items(): self.assertEqual(len(set(xy_zns)), len(set(coords))) @@ -160,53 +156,65 @@ def test_zns(self) -> None: def test_ver_zns(self) -> None: coords = [(0, 1), (1, 0)] nodes = [ZNode(coord=c) for c in coords] - nodes += [n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + nodes += [ + n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] + ] for dim in [(1, 1), (3, 5), (6, 6)]: qfl = QuoFloor(corner_qtile=nodes, dim=dim) qfl_ver = qfl.ver_zns - for xy , xy_ver in qfl_ver.items(): + for xy, xy_ver in qfl_ver.items(): self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) for v in xy_ver: self.assertTrue(v.is_vertical()) - self.assertEqual(len(qfl_ver), dim[0]*dim[1]) + self.assertEqual(len(qfl_ver), dim[0] * dim[1]) def test_hor_zns(self) -> None: coords = [(0, 1), (1, 2)] nodes = [ZNode(coord=c) for c in coords] - nodes += [n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + nodes += [ + n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] + ] for dim in [(4, 7), (1, 1)]: qfl = QuoFloor(corner_qtile=nodes, dim=dim) qfl_hor = qfl.hor_zns - for xy , xy_hor in qfl_hor.items(): + for xy, xy_hor in qfl_hor.items(): self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) for v in xy_hor: self.assertTrue(v.is_horizontal()) - self.assertEqual(len(qfl_hor), dim[0]*dim[1]) + self.assertEqual(len(qfl_hor), dim[0] * dim[1]) for m in [4, 5]: nodes2 = [ZNode(coord=c, shape=ZShape(m=m)) for c in coords] - nodes2 += [n + p for n in nodes2 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] + nodes2 += [ + n + p + for n in nodes2 + for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] + ] for dim in [(4, 7), (1, 1), (4, 4), (2, 3)]: - if dim[0] <= m and dim[1] <= m: + if dim[0] <= m and dim[1] <= m: qfl = QuoFloor(corner_qtile=nodes2, dim=dim) qfl_hor = qfl.hor_zns - for xy , xy_hor in qfl_hor.items(): + for xy, xy_hor in qfl_hor.items(): self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) for v in xy_hor: self.assertTrue(v.is_horizontal()) - self.assertEqual(len(qfl_hor), dim[0]*dim[1]) + self.assertEqual(len(qfl_hor), dim[0] * dim[1]) else: with self.assertRaises(ValueError): QuoFloor(corner_qtile=nodes2, dim=dim) - def test_quo_ext_paths(self) -> None: coords1 = [(2, 1), (1, 2)] nodes1 = [ZNode(coord=c) for c in coords1] - nodes1 += [n + p for n in nodes1 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)]] - coords2 = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(3)] + nodes1 += [ + n + p for n in nodes1 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] + ] + coords2 = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(3)] nodes2 = [ZNode(coord=coord) for coord in coords2] for dim in [(1, 1), (5, 5), (3, 6)]: - floors = [QuoFloor(corner_qtile=nodes1, dim=dim), QuoFloor(corner_qtile=nodes2, dim=dim)] + floors = [ + QuoFloor(corner_qtile=nodes1, dim=dim), + QuoFloor(corner_qtile=nodes2, dim=dim), + ] for qfl in floors: ext_paths = qfl.quo_ext_paths self.assertTrue("col" in ext_paths) @@ -220,10 +228,10 @@ def test_quo_ext_paths(self) -> None: uwjs_c = [x[0] for x in list_c] self.assertEqual(len(uwjs_c), len(set(uwjs_c))) for _, zse in list_c: - self.assertEqual(zse.z_end-zse.z_start+1, dim[1]) + self.assertEqual(zse.z_end - zse.z_start + 1, dim[1]) for list_r in row_ext_paths.values(): # Check no repeat uwjs_r = [x[0] for x in list_r] self.assertEqual(len(uwjs_r), len(set(uwjs_r))) for _, zse in list_r: - self.assertEqual(zse.z_end-zse.z_start+1, dim[0]) \ No newline at end of file + self.assertEqual(zse.z_end - zse.z_start + 1, dim[0]) diff --git a/tests/utils/zephyr/test_zephyr.py b/tests/utils/zephyr/test_zephyr.py index 1f82a0e5..1486045d 100644 --- a/tests/utils/zephyr/test_zephyr.py +++ b/tests/utils/zephyr/test_zephyr.py @@ -17,12 +17,11 @@ import unittest from random import shuffle -from dwave_networkx.generators.zephyr import zephyr_graph import networkx as nx -from parameterized import parameterized - +from dwave_networkx.generators.zephyr import zephyr_graph from minorminer.utils.diagnostic import is_valid_embedding -from minorminer.utils.zephyr import find_clique_embedding, find_biclique_embedding +from minorminer.utils.zephyr import find_biclique_embedding, find_clique_embedding +from parameterized import parameterized class Test_find_clique_embedding(unittest.TestCase): @@ -37,7 +36,7 @@ def test_k_parameter_int(self): self.assertTrue(is_valid_embedding(embedding, nx.complete_graph(k_int), ze)) def test_k_parameter_list(self): - k_nodes = ['one', 'two', 'three'] + k_nodes = ["one", "two", "three"] m = 4 # Find embedding @@ -148,9 +147,9 @@ def test_clique_missing_edges(self): class Test_find_biclique_embedding(unittest.TestCase): - ABC_DE = (['a', 'b', 'c'], ['d', 'e'], 2) - ABC_P = (['a', 'b', 'c'], 7, 2) - N_DE = (4, ['d', 'e'], 2) + ABC_DE = (["a", "b", "c"], ["d", "e"], 2) + ABC_P = (["a", "b", "c"], 7, 2) + N_DE = (4, ["d", "e"], 2) @parameterized.expand(((6, 6, 2), (16, 16, 3), (4, 5, 1), ABC_DE, ABC_P, N_DE)) def test_success(self, a, b, m): @@ -171,7 +170,7 @@ def test_success(self, a, b, m): biclique = nx.complete_bipartite_graph(left, right) self.assertTrue(is_valid_embedding({**left, **right}, biclique, ze)) - @parameterized.expand((((1, 2), (2, 3)), ((1, 2), 2), (3, (2, 3)), (('a', 'b'), ('b', 'c')))) + @parameterized.expand((((1, 2), (2, 3)), ((1, 2), 2), (3, (2, 3)), (("a", "b"), ("b", "c")))) def test_overlapping_labels(self, a, b): m = 2 diff --git a/tests/utils/zephyr/test_zephyr_base.py b/tests/utils/zephyr/test_zephyr_base.py index 687d7f3e..bb57d604 100644 --- a/tests/utils/zephyr/test_zephyr_base.py +++ b/tests/utils/zephyr/test_zephyr_base.py @@ -17,10 +17,12 @@ import unittest from itertools import product + +import dwave_networkx as dnx import numpy as np from dwave.cloud import Client from dwave.system import DWaveSampler -import dwave_networkx as dnx + class ZephyrBaseTest(unittest.TestCase): def setUp(self): @@ -35,7 +37,7 @@ def initialize_samplers(self): with Client.from_config(client="qpu") as client: zeph_solvers = client.get_solvers( topology__type__eq="zephyr", - ) + ) self.samplers = [DWaveSampler(solver=z_solver.id) for z_solver in zeph_solvers] def initialize_rngs(self): @@ -43,37 +45,32 @@ def initialize_rngs(self): np.random.default_rng(seed=1), np.random.default_rng(seed=10), ] - + def initialize_zeph_ms(self): self.zeph_ms = [3, 6] def initialize_node_del_percents(self): self.node_del_percents = [0, 0.03] - def initialize_edge_del_percents(self): self.edge_del_percents = [0, 0.02] - def initialize_graphs(self): self.graphs = list() for rng in self.rngs: for m in self.zeph_ms: for node_del_per, edge_del_per in product( self.node_del_percents, self.edge_del_percents - ): + ): G = dnx.zephyr_graph(m=m, coordinates=True) num_nodes_to_remove = int(node_del_per * G.number_of_nodes()) - nodes_to_remove = [ - tuple(v) - for v in rng.choice(G.nodes(), num_nodes_to_remove) - ] + nodes_to_remove = [tuple(v) for v in rng.choice(G.nodes(), num_nodes_to_remove)] G.remove_nodes_from(nodes_to_remove) num_edges_to_remove = int(edge_del_per * G.number_of_edges()) edges_to_remove = [ (tuple(u), tuple(v)) for (u, v) in rng.choice(G.edges(), num_edges_to_remove) - ] + ] G.remove_edges_from(edges_to_remove) G_dict = { "G": G, @@ -81,7 +78,8 @@ def initialize_graphs(self): "num_nodes": G.number_of_nodes(), "num_edges": G.number_of_edges(), "num_missing_nodes": num_nodes_to_remove, - "num_missing_edges": dnx.zephyr_graph(m=m).number_of_edges()-G.number_of_edges(), + "num_missing_edges": dnx.zephyr_graph(m=m).number_of_edges() + - G.number_of_edges(), "num_extra_missing_edges": num_edges_to_remove, - } - self.graphs.append(G_dict) \ No newline at end of file + } + self.graphs.append(G_dict) diff --git a/tests/utils/zephyr/test_zephyr_survey.py b/tests/utils/zephyr/test_zephyr_survey.py index d22cee23..d8cecb39 100644 --- a/tests/utils/zephyr/test_zephyr_survey.py +++ b/tests/utils/zephyr/test_zephyr_survey.py @@ -15,9 +15,8 @@ # ================================================================================================ - -from tests.utils.zephyr.test_zephyr_base import ZephyrBaseTest from minorminer.utils.zephyr.survey import ZSurvey +from tests.utils.zephyr.test_zephyr_base import ZephyrBaseTest class TestZephyrSurvey(ZephyrBaseTest): @@ -37,7 +36,6 @@ def test_get_zephyr_shape_coord_sampler(self) -> None: for v in z_sampler.nodelist: self.assertTrue(isinstance(v, tuple)) - def test_get_zephyr_shape_coord_graph(self) -> None: for G_dict in self.graphs: z_graph = G_dict["G"] @@ -47,13 +45,14 @@ def test_get_zephyr_shape_coord_graph(self) -> None: self.assertTrue(t, G_info.get("tile")) self.assertTrue(coord, G_info.get("labels")) - def test_zephyr_survey_runs_sampler(self) -> None: for z_sampler in self.samplers: try: ZSurvey(z_sampler) except Exception as e: - self.fail(f"ZephyrSurvey raised an exception {e} when running with sampler = {z_sampler}") + self.fail( + f"ZephyrSurvey raised an exception {e} when running with sampler = {z_sampler}" + ) def test_zephyr_survey_runs_graph(self) -> None: for G_dict in self.graphs: @@ -61,8 +60,10 @@ def test_zephyr_survey_runs_graph(self) -> None: try: ZSurvey(z_graph) except Exception as e: - self.fail(f"ZephyrSurvey raised an exception {e} when running with graph = {z_graph}") - + self.fail( + f"ZephyrSurvey raised an exception {e} when running with graph = {z_graph}" + ) + def test_num_nodes(self) -> None: for z_sampler in self.samplers: num_nodes = len(z_sampler.nodelist) @@ -84,4 +85,4 @@ def test_num_edges(self) -> None: def test_external_paths(self) -> None: for z_sampler in self.samplers: sur = ZSurvey(z_sampler) - sur.calculate_external_paths_stretch() \ No newline at end of file + sur.calculate_external_paths_stretch() From ef9f800227ed503b9db0dd2aab73fce2c5787464 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Thu, 13 Mar 2025 11:22:00 -0700 Subject: [PATCH 003/127] Reformat --- minorminer/utils/zephyr/coordinate_systems.py | 27 +- minorminer/utils/zephyr/node_edge.py | 291 ++++++++---------- minorminer/utils/zephyr/plane_shift.py | 22 +- minorminer/utils/zephyr/qfloor.py | 152 ++++----- minorminer/utils/zephyr/survey.py | 122 +++----- minorminer/utils/zephyr/zephyr.py | 11 +- 6 files changed, 257 insertions(+), 368 deletions(-) diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index c7862c6a..4baf5fc1 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -15,17 +15,16 @@ # ================================================================================================ - from __future__ import annotations -from collections import namedtuple - +from collections import namedtuple -zephyr_fields = ["u", "w", "k", "j", "z"] -ZephyrCoord = namedtuple("ZephyrCoord", zephyr_fields, defaults=(None,)*len(zephyr_fields)) +zephyr_fields = ["u", "w", "k", "j", "z"] +ZephyrCoord = namedtuple("ZephyrCoord", zephyr_fields, defaults=(None,) * len(zephyr_fields)) cartesian_fields = ["x", "y", "k"] -CartesianCoord = namedtuple("CartesianCoord", cartesian_fields, defaults=(None,)*len(cartesian_fields)) - +CartesianCoord = namedtuple( + "CartesianCoord", cartesian_fields, defaults=(None,) * len(cartesian_fields) +) def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: @@ -41,13 +40,13 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: x, y, k = ccoord if x % 2 == 0: u: int = 0 - w: int = x //2 - j: int = ((y-1)%4) // 2 + w: int = x // 2 + j: int = ((y - 1) % 4) // 2 z: int = y // 4 else: u: int = 1 w: int = y // 2 - j: int = ((x-1)%4) // 2 + j: int = ((x - 1) % 4) // 2 z: int = x // 4 return ZephyrCoord(u=u, w=w, k=k, j=j, z=z) @@ -64,9 +63,9 @@ def zephyr_to_cartesian(zcoord: ZephyrCoord) -> CartesianCoord: """ u, w, k, j, z = zcoord if u == 0: - x = 2*w - y = 4*z + 2*j + 1 + x = 2 * w + y = 4 * z + 2 * j + 1 else: - x = 4*z + 2*j + 1 + x = 4 * z + 2 * j + 1 y = 2 * w - return CartesianCoord(x=x, y=y, k=k) \ No newline at end of file + return CartesianCoord(x=x, y=y, k=k) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 0dd2bf20..bee01fc1 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -15,17 +15,19 @@ # ================================================================================================ - - from __future__ import annotations -from typing import Generator, Iterable, Callable -from itertools import product + from collections import namedtuple -from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord, CartesianCoord, zephyr_to_cartesian, cartesian_to_zephyr +from itertools import product +from typing import Callable, Generator, Iterable + +from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, + cartesian_to_zephyr, zephyr_to_cartesian) from minorminer.utils.zephyr.plane_shift import PlaneShift ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) + class Edge: """Initializes an Edge with 'int' nodes x, y. @@ -36,16 +38,15 @@ class Edge: Raises: TypeError: If either of x or y is not 'int'. """ + def __init__( self, x: int, y: int, - ) -> None: + ) -> None: if not all(isinstance(var, int) for var in (x, y)): - raise TypeError( - f"Expected x, y to be 'int', got {type(x), type(y)}" - ) + raise TypeError(f"Expected x, y to be 'int', got {type(x), type(y)}") if x < y: self._edge = (x, y) else: @@ -67,7 +68,6 @@ def __repr__(self) -> str: return f"{type(self).__name__}{self._edge}" - class ZEdge(Edge): """Initializes a ZEdge with 'ZNode' nodes x, y @@ -80,7 +80,7 @@ class ZEdge(Edge): ValueError: If x, y do not have the same shape. ValueError: If x, y are not neighbours in a perfect yield (quotient) Zephyr graph. - + Example 1: >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge >>> e = ZEdge(ZNode((3, 2)), ZNode((7, 2))) @@ -90,19 +90,16 @@ class ZEdge(Edge): >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge >>> ZEdge(ZNode((2, 3)), ZNode((6, 3))) # raises error, since the two are not neighbors """ + def __init__( self, x: ZNode, y: ZNode, - ) -> None: + ) -> None: if not isinstance(x, ZNode) or not isinstance(y, ZNode): - raise TypeError( - f"Expected x, y to be ZNode, got {type(x), type(y)}" - ) + raise TypeError(f"Expected x, y to be ZNode, got {type(x), type(y)}") if x.shape != y.shape: - raise ValueError( - f"Expected x, y to have the same shape, got {x.shape, y.shape}" - ) + raise ValueError(f"Expected x, y to have the same shape, got {x.shape, y.shape}") self._kind = None for kind in ("internal", "external", "odd"): if x.is_neighbor(y, nbr_kind=kind): @@ -116,8 +113,6 @@ def __init__( self._edge = (y, x) - - class ZNode: """Initializes 'ZNode' with coord and optional shape. @@ -128,11 +123,11 @@ class ZNode: Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. Defaults to None. - + Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), shape = None raises ValueError. In this case the tile size of Zephyr, t, must be provided. - + Example: >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) @@ -151,12 +146,13 @@ class ZNode: [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] """ + def __init__( self, coord: CartesianCoord | ZephyrCoord | tuple[int], - shape: ZShape | tuple[int | None] | None=None, - convert_to_z: bool | None=None, - ) -> None: + shape: ZShape | tuple[int | None] | None = None, + convert_to_z: bool | None = None, + ) -> None: self.shape = shape if convert_to_z is None: @@ -189,17 +185,15 @@ def shape(self, new_shape) -> None: if not isinstance(new_shape, ZShape): raise TypeError( f"Expected shape to be tuple[int | None] or ZShape or None, got {type(new_shape)}" - ) + ) if hasattr(self, "_ccoord"): if (self._ccoord.k is None) != (new_shape.t is None): raise ValueError( f"ccoord, shape must be both quotient or non-quotient, got {self._ccoord, new_shape}" - ) + ) for var, val in {"m": new_shape.m, "t": new_shape.t}.items(): if (val is not None) and (not isinstance(val, int)): - raise TypeError( - f"Expected {var} to be None or 'int', got {type(val)}" - ) + raise TypeError(f"Expected {var} to be None or 'int', got {type(val)}") self._shape = new_shape @property @@ -215,16 +209,12 @@ def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): if not isinstance(new_ccoord, CartesianCoord): raise TypeError( f"Expected ccoord to be CartesianCoord or tuple[int], got {type(new_ccoord)}" - ) + ) for c in (new_ccoord.x, new_ccoord.y): if not isinstance(c, int): - raise TypeError( - f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}" - ) + raise TypeError(f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}") if c < 0: - raise ValueError( - f"Expected ccoord.x and ccoord.y to be non-negative, got {c}" - ) + raise ValueError(f"Expected ccoord.x and ccoord.y to be non-negative, got {c}") if new_ccoord.x % 2 == new_ccoord.y % 2: raise ValueError( f"Expected ccoord.x and ccoord.y to differ in parity, got {new_ccoord.x, new_ccoord.y}" @@ -234,52 +224,45 @@ def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): if (self._shape.t is None) != (new_ccoord.k is None): raise ValueError( f"shape and ccoord must be both quotient or non-quotient, got {self._shape}, {new_ccoord}" - ) + ) if (self._shape.t is not None) and (new_ccoord.k not in range(self._shape.t)): - raise ValueError( - f"Expected k to be in {range(self._shape.t)}, got {new_ccoord.k}" - ) + raise ValueError(f"Expected k to be in {range(self._shape.t)}, got {new_ccoord.k}") # check x, y value of CartesianCoord is consistent with m if self._shape.m is not None: - if (all(val in range(4*self._shape.m+1) for val in (new_ccoord.x, new_ccoord.y))): + if all(val in range(4 * self._shape.m + 1) for val in (new_ccoord.x, new_ccoord.y)): self._ccoord = new_ccoord else: raise ValueError( f"Expected ccoord.x and ccoord.y to be in {range(4*self._shape.m+1)}, got {new_ccoord.x, new_ccoord.y}" - ) + ) self._ccoord = new_ccoord @staticmethod def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: """Takes a tuple[int] and returns the corresponding CartesianCoord or ZephyrCoord""" - if (not isinstance(coord, tuple)) or ( - not all(isinstance(c, int) for c in coord)): - raise TypeError( - f"Expected {coord} to be a tuple[int], got {coord}" - ) + if (not isinstance(coord, tuple)) or (not all(isinstance(c, int) for c in coord)): + raise TypeError(f"Expected {coord} to be a tuple[int], got {coord}") if any(c < 0 for c in coord): raise ValueError(f"Expected elements of coord to be non-negative, got {coord}") len_coord = len(coord) if len_coord in (2, 3): x, y, *k = coord if x % 2 == y % 2: - raise ValueError( - f"Expected x, y to differ in parity, got {x, y}" - ) + raise ValueError(f"Expected x, y to differ in parity, got {x, y}") return CartesianCoord(x=x, y=y) if len(k) == 0 else CartesianCoord(x=x, y=y, k=k[0]) if len_coord in (4, 5): u, w, *k, j, z = coord for var, val in {"u": u, "j": j}.items(): if not val in [0, 1]: - raise ValueError( - f"Expected {var} to be in [0, 1], got {val}" - ) - return ZephyrCoord(u=u, w=w, j=j, z=z) if len(k) == 0 else ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) - raise ValueError( - f"coord can have length 2, 3, 4 or 5, got {len_coord}" + raise ValueError(f"Expected {var} to be in [0, 1], got {val}") + return ( + ZephyrCoord(u=u, w=w, j=j, z=z) + if len(k) == 0 + else ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) ) + raise ValueError(f"coord can have length 2, 3, 4 or 5, got {len_coord}") @property def zcoord(self) -> ZephyrCoord: @@ -294,11 +277,7 @@ def to_quo(self) -> ZNode: """Returns the quotient ZNode corresponding to self""" qshape = ZShape(m=self._shape.m) qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) - return ZNode( - coord=qccoord, - shape=qshape, - convert_to_z=self.convert_to_z - ) + return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) @property def direction(self) -> int: @@ -320,7 +299,7 @@ def node_kind(self) -> str: def neighbor_kind( self, other: ZNode, - ) -> str | None: + ) -> str | None: """Returns the kind of coupler between self and other, 'internal', 'external', 'odd', or None.""" if not isinstance(other, ZNode): @@ -331,33 +310,33 @@ def neighbor_kind( coord2 = other._ccoord x1, y1 = coord1.x, coord1.y x2, y2 = coord2.x, coord2.y - if abs(x1-x2) == abs(y1-y2) == 1: + if abs(x1 - x2) == abs(y1 - y2) == 1: return "internal" - if x1 % 2 != x2 % 2: + if x1 % 2 != x2 % 2: return - if coord1.k != coord2.k: # odd, external neighbors only on the same k + if coord1.k != coord2.k: # odd, external neighbors only on the same k return - if self.is_vertical(): # self vertical - if x1 != x2: # odd, external neighbors only on the same vertical lines + if self.is_vertical(): # self vertical + if x1 != x2: # odd, external neighbors only on the same vertical lines return - diff_y = abs(y1-y2) + diff_y = abs(y1 - y2) return "odd" if diff_y == 2 else "external" if diff_y == 4 else None else: if y1 != y2: # odd, external neighbors only on the same horizontal lines return - diff_x = abs(x1-x2) + diff_x = abs(x1 - x2) return "odd" if diff_x == 2 else "external" if diff_x == 4 else None def internal_neighbors_generator( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> Generator[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> Generator[ZNode]: """Generator of internal neighbors""" x, y, _ = self._ccoord convert = self.convert_to_z k_vals = [None] if self._shape.t is None else range(self._shape.t) for i, j, k in product((-1, 1), (-1, 1), k_vals): - ccoord = CartesianCoord(x=x+i, y=y+j, k=k) + ccoord = CartesianCoord(x=x + i, y=y + j, k=k) coord = ccoord if not convert else cartesian_to_zephyr(ccoord) if not where(coord): continue @@ -366,7 +345,7 @@ def internal_neighbors_generator( coord=ccoord, shape=self._shape, convert_to_z=convert, - ) + ) except GeneratorExit: raise except Exception: @@ -374,12 +353,12 @@ def internal_neighbors_generator( def external_neighbors_generator( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> Generator[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> Generator[ZNode]: """Generator of external neighbors""" x, y, k = self._ccoord convert = self.convert_to_z - changing_index = 1 if x%2 == 0 else 0 + changing_index = 1 if x % 2 == 0 else 0 for s in [-4, 4]: new_x = x + s if changing_index == 0 else x new_y = y + s if changing_index == 1 else y @@ -392,7 +371,7 @@ def external_neighbors_generator( coord=ccoord, shape=self._shape, convert_to_z=convert, - ) + ) except GeneratorExit: raise except Exception: @@ -400,12 +379,12 @@ def external_neighbors_generator( def odd_neighbors_generator( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> Generator[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> Generator[ZNode]: """Generator of odd neighbors""" x, y, k = self._ccoord convert = self.convert_to_z - changing_index = 1 if x%2 == 0 else 0 + changing_index = 1 if x % 2 == 0 else 0 for s in [-2, 2]: new_x = x + s if changing_index == 0 else x new_y = y + s if changing_index == 1 else y @@ -418,7 +397,7 @@ def odd_neighbors_generator( coord=ccoord, shape=self._shape, convert_to_z=convert, - ) + ) except GeneratorExit: raise except Exception: @@ -426,9 +405,9 @@ def odd_neighbors_generator( def neighbors_generator( self, - nbr_kind: str | Iterable[str] | None=None, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> Generator[ZNode]: + nbr_kind: str | Iterable[str] | None = None, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> Generator[ZNode]: if nbr_kind is None: kinds = {"internal", "external", "odd"} else: @@ -437,9 +416,7 @@ def neighbors_generator( elif isinstance(nbr_kind, Iterable): kinds = set(nbr_kind) else: - raise TypeError( - f"Expected 'str' or Iterable[str] or None for nbr_kind" - ) + raise TypeError(f"Expected 'str' or Iterable[str] or None for nbr_kind") kinds = kinds.intersection({"internal", "external", "odd"}) if "internal" in kinds: for cc in self.internal_neighbors_generator(where=where): @@ -451,86 +428,83 @@ def neighbors_generator( for cc in self.odd_neighbors_generator(where=where): yield cc - def neighbors(self, - nbr_kind: str | Iterable[str] | None=None, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> set[ZNode]: + def neighbors( + self, + nbr_kind: str | Iterable[str] | None = None, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> set[ZNode]: """Returns neighbors when restricted to nbr_kind and where""" return set(self.neighbors_generator(nbr_kind=nbr_kind, where=where)) def internal_neighbors( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> set[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> set[ZNode]: """Returns internal neighbors when restricted to where""" return set(self.neighbors_generator(nbr_kind="internal", where=where)) def external_neighbors( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> set[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> set[ZNode]: """Returns external neighbors when restricted to where""" return set(self.neighbors_generator(nbr_kind="external", where=where)) def odd_neighbors( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> set[ZNode]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> set[ZNode]: """Returns odd neighbors when restricted to where""" return set(self.neighbors_generator(nbr_kind="odd", where=where)) def is_internal_neighbor( - self, other: ZNode, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> bool: + self, + other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> bool: """Tells if another ZNode is an internal neighbor when restricted to where""" if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be ZNode, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") for nbr in self.internal_neighbors_generator(where=where): if other == nbr: return True return False def is_external_neighbor( - self, other: ZNode, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> bool: + self, + other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> bool: """Tells if another ZNode is an external neighbor when restricted to where""" if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be ZNode, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") for nbr in self.external_neighbors_generator(where=where): if other == nbr: return True return False def is_odd_neighbor( - self, other: ZNode, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> bool: + self, + other: ZNode, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> bool: """Tells if another ZNode is an odd neighbor when restricted to where""" if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be ZNode, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") for nbr in self.odd_neighbors_generator(where=where): if other == nbr: return True return False def is_neighbor( - self, other: ZNode, - nbr_kind: str | Iterable[str] | None=None, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> bool: + self, + other: ZNode, + nbr_kind: str | Iterable[str] | None = None, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> bool: """Tells if another ZNode is a neighbor when restricted to nbr_kind and where""" if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be ZNode, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") for nbr in self.neighbors_generator(nbr_kind=nbr_kind, where=where): if other == nbr: return True @@ -538,121 +512,102 @@ def is_neighbor( def incident_edges( self, - nbr_kind: str | Iterable[str] | None=None, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> list[ZEdge]: + nbr_kind: str | Iterable[str] | None = None, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> list[ZEdge]: """Returns incident edges when restricted to nbr_kind and where""" - return [ - ZEdge(self, v) - for v in self.neighbors(nbr_kind=nbr_kind, where=where) - ] + return [ZEdge(self, v) for v in self.neighbors(nbr_kind=nbr_kind, where=where)] def degree( self, - nbr_kind: str | Iterable[str] | None=None, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - ) -> int: + nbr_kind: str | Iterable[str] | None = None, + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + ) -> int: """Returns degree when restricted to nbr_kind and where""" return len(self.neighbors(nbr_kind=nbr_kind, where=where)) def __eq__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord == other._ccoord def __ne__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord != other._ccoord def __gt__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord > other._ccoord def __ge__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord >= other._ccoord def __lt__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord < other._ccoord def __le__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) return self._ccoord <= other._ccoord def __add__( self, shift: PlaneShift | tuple[int], - ) -> ZNode: + ) -> ZNode: if not isinstance(shift, PlaneShift): shift = PlaneShift(*shift) x, y, k = self._ccoord new_x = x + shift[0] new_y = y + shift[1] - + return ZNode(coord=CartesianCoord(x=new_x, y=new_y, k=k), shape=self._shape) def __sub__( self, other: ZNode, - ) -> PlaneShift: + ) -> PlaneShift: if not isinstance(other, ZNode): - raise TypeError( - f"Expected {other} to be {type(self).__name__}, got {type(other)}" - ) + raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: raise ValueError( f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) + ) x_shift: int = self._ccoord.x - other._ccoord.x y_shift: int = self._ccoord.y - other._ccoord.y try: return PlaneShift(x_shift=x_shift, y_shift=y_shift) except: - raise ValueError( - f"{other} cannot be subtracted from {self}" - ) + raise ValueError(f"{other} cannot be subtracted from {self}") def __hash__(self) -> int: return (self._ccoord, self._shape).__hash__() @@ -668,4 +623,4 @@ def __repr__(self) -> str: shape_str = "" else: shape_str = f", shape={self._shape.m, self._shape.t!r}" - return f"{type(self).__name__}("+coord_str+shape_str+")" \ No newline at end of file + return f"{type(self).__name__}(" + coord_str + shape_str + ")" diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index 54e910ae..e7dee233 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -15,17 +15,16 @@ # ================================================================================================ - from __future__ import annotations -from typing import Iterator + from collections import namedtuple +from typing import Iterator Shift = namedtuple("Shift", ["x", "y"]) - class PlaneShift: - """ Initializes PlaneShift with an x_shift, y_shift. + """Initializes PlaneShift with an x_shift, y_shift. Args: x_shift (int): The displacement in the x-direction of a CartesianCoord. @@ -41,18 +40,19 @@ class PlaneShift: >>> ps2 = PlaneShift(2, -4) >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") """ + def __init__( self, x_shift: int, y_shift: int, - ) -> None: + ) -> None: for shift in [x_shift, y_shift]: if not isinstance(shift, int): raise TypeError(f"Expected {shift} to be 'int', got {type(shift)}") - if x_shift%2 != y_shift%2: + if x_shift % 2 != y_shift % 2: raise ValueError( f"Expected x_shift, y_shift to have the same parity, got {x_shift, y_shift}" - ) + ) self._shift = Shift(x_shift, y_shift) @property @@ -80,8 +80,8 @@ def __mul__(self, scale: int | float) -> PlaneShift: """ if not isinstance(scale, (int, float)): raise TypeError(f"Expected scale to be int or float, got {type(scale)}") - new_shift_x = scale*self._shift.x - new_shift_y = scale*self._shift.y + new_shift_x = scale * self._shift.x + new_shift_y = scale * self._shift.y if int(new_shift_x) != new_shift_x or int(new_shift_y) != new_shift_y: raise ValueError(f"{scale} cannot be multiplied by {self}") return PlaneShift(int(new_shift_x), int(new_shift_y)) @@ -115,7 +115,7 @@ def __add__(self, other: PlaneShift) -> PlaneShift: """ if not isinstance(other, PlaneShift): raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return PlaneShift(self._shift.x+other._shift.x, self._shift.y+other._shift.y) + return PlaneShift(self._shift.x + other._shift.x, self._shift.y + other._shift.y) def __iter__(self) -> Iterator[int]: return self._shift.__iter__() @@ -160,4 +160,4 @@ def __ge__(self, other: PlaneShift) -> bool: return (self == other) or (self > other) def __repr__(self) -> str: - return f"{type(self).__name__}{self._shift.x, self._shift.y}" \ No newline at end of file + return f"{type(self).__name__}{self._shift.x, self._shift.y}" diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 3457b306..28464933 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -15,29 +15,27 @@ # ================================================================================================ - from __future__ import annotations -from typing import Iterable, Callable, Iterator + from collections import defaultdict, namedtuple from itertools import product +from typing import Callable, Iterable, Iterator + from minorminer.utils.zephyr.coordinate_systems import * +from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape from minorminer.utils.zephyr.plane_shift import PlaneShift -from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, ZShape - - Dim = namedtuple("Dim", ["Lx", "Ly"]) UWJ = namedtuple("UWJ", ["u", "w", "j"]) ZSE = namedtuple("ZSE", ["z_start", "z_end"]) - class QuoTile: """Initializes a 'QuoTile' object with zns. Args: zns (Iterable[ZNode]): The ZNodes the tile contains - + Example: .. code-block:: python >>> from minorminer.utils.zephyr.node_edge import ZNode @@ -47,10 +45,11 @@ class QuoTile: >>> tile = QuoTile(zns) >>> print(f"{tile.edges() = }\n{tile.seed = }\n{tile.shifts = }") """ + def __init__( self, zns: Iterable[ZNode], - ) -> None: + ) -> None: self.zns = zns @property @@ -62,21 +61,17 @@ def zns(self) -> list[ZNode]: def zns(self, new_zns: Iterable[ZNode]) -> None: """Sets the zns""" if not isinstance(new_zns, Iterable): - raise TypeError( - f"Expected {new_zns} to be Iterable[ZNode], got {type(new_zns)}" - ) + raise TypeError(f"Expected {new_zns} to be Iterable[ZNode], got {type(new_zns)}") for zn in new_zns: if not isinstance(zn, ZNode): - raise TypeError( - f"Expected elements of {new_zns} to be ZNode, got {type(zn)}" - ) + raise TypeError(f"Expected elements of {new_zns} to be ZNode, got {type(zn)}") if not zn.is_quo(): raise ValueError(f"Expected elements of {new_zns} to be quotient, got {zn}") new_zns_shape = {zn.shape for zn in new_zns} if len(new_zns_shape) != 1: raise ValueError( f"Expected all elements of zns to have the same shape, got {new_zns_shape}" - ) + ) temp_zns = sorted(list(set(new_zns))) if len(temp_zns) == 0: return temp_zns @@ -120,19 +115,17 @@ def hor_zns(self) -> list[ZNode]: def edges( self, - where: Callable[[CartesianCoord | ZephyrCoord], bool]=lambda coord: True, - nbr_kind: str | Iterable[str] | None=None, - ) -> list[ZEdge]: + where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, + nbr_kind: str | Iterable[str] | None = None, + ) -> list[ZEdge]: """Returns the list of edges of the graph induced on the tile, when resticted to nbr_kind and where.""" zns = self._zns tile_coords = [zn.zcoord for zn in zns] if self.convert_to_z else [zn.ccoord for zn in zns] where_tile = lambda coord: where(coord) and coord in tile_coords _edges = { - edge - for zn in zns - for edge in zn.incident_edges(nbr_kind=nbr_kind, where=where_tile) - } + edge for zn in zns for edge in zn.incident_edges(nbr_kind=nbr_kind, where=where_tile) + } return list(_edges) def __len__(self) -> int: @@ -152,7 +145,7 @@ def __eq__(self, other: QuoTile) -> bool: return self._zns == other._zns def __add__(self, shift: PlaneShift) -> QuoTile: - return QuoTile(zns=[zn+shift for zn in self._zns]) + return QuoTile(zns=[zn + shift for zn in self._zns]) def __repr__(self) -> str: return f"{type(self).__name__}{self._zns!r}" @@ -161,8 +154,6 @@ def __str__(self) -> str: return f"{type(self).__name__}{self._zns}" - - class QuoFloor: """Initializes QuoFloor object with a corner tile and dimension and optional tile connector. @@ -171,7 +162,7 @@ class QuoFloor: corner_qtile (QuoTile): The tile at the top left corner of desired subgrid of Quotient Zephyr graph. dim (Dim | tuple[int]): The dimension of floor, i.e. - the number of columns of floor, the number of rows of floor. + the number of columns of floor, the number of rows of floor. tile_connector (dict[tuple[int], PlaneShift], optional): Determines how to get the tiles (x+1, y) and (x, y+1) from tile (x, y). Defaults to tile_connector0. @@ -184,8 +175,9 @@ class QuoFloor: >>> floor = QuoFloor(corner_qtile=zns, dim=(3, 5)) >>> print(f"{floor.qtile_xy(2, 3) = }") floor.qtile_xy(2, 3) = QuoTile[ZNode(CartesianCoord(x=8, y=13, k=None)), ZNode(CartesianCoord(x=9, y=12, k=None)), ZNode(CartesianCoord(x=9, y=14, k=None)), ZNode(CartesianCoord(x=10, y=13, k=None)), ZNode(CartesianCoord(x=10, y=15, k=None)), ZNode(CartesianCoord(x=11, y=14, k=None)), ZNode(CartesianCoord(x=11, y=16, k=None)), ZNode(CartesianCoord(x=12, y=15, k=None))] - + """ + implemented_connectors: tuple[dict[tuple[int], PlaneShift]] = ( {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 4)}, ) @@ -196,7 +188,7 @@ def __init__( corner_qtile: QuoTile | Iterable[ZNode], dim: Dim | tuple[int], tile_connector: dict[tuple[int], PlaneShift] = tile_connector0, - ) -> None: + ) -> None: self.tile_connector = tile_connector self.dim = dim self.corner_qtile = corner_qtile @@ -207,10 +199,7 @@ def tile_connector(self) -> dict[tuple[int], PlaneShift] | None: return self.tile_connector @tile_connector.setter - def tile_connector( - self, - new_connector: dict[tuple[int], PlaneShift] - ) -> None: + def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: """Sets the tile connector""" if not isinstance(new_connector, dict): raise TypeError(f"Expected tile_connector to be dict, got {type(new_connector)}") @@ -218,18 +207,18 @@ def tile_connector( raise NotImplementedError( f"{new_connector} not implemented. " f"Availabale options are {self.implemented_connectors}" - ) + ) if any(dir_con not in new_connector.keys() for dir_con in ((1, 0), (0, 1))): raise ValueError( f"Expected tile_connector to have (1, 0), (0, 1) as keys, got {new_connector}" - ) + ) self._tile_connector = new_connector def check_dim_corner_qtile_compatibility( self, dim: Dim, corner_qtile: QuoTile, - ) -> None: + ) -> None: """Checks whether dimension and corner tile are compatible, i.e. whether given the tile connector, the floor can be constructed with the provided corner tile and dimensions. @@ -239,12 +228,10 @@ def check_dim_corner_qtile_compatibility( ver_step = self._tile_connector[(0, 1)] hor_step = self._tile_connector[(1, 0)] try: - [h+hor_step*(dim.Lx-1) for h in corner_qtile.hor_zns] - [v+ver_step*(dim.Ly-1) for v in corner_qtile.ver_zns] + [h + hor_step * (dim.Lx - 1) for h in corner_qtile.hor_zns] + [v + ver_step * (dim.Ly - 1) for v in corner_qtile.ver_zns] except (ValueError, TypeError): - raise ValueError( - f"{dim, corner_qtile} are not compatible" - ) + raise ValueError(f"{dim, corner_qtile} are not compatible") @property def dim(self) -> Dim: @@ -257,20 +244,13 @@ def dim(self, new_dim: Dim | tuple[int]) -> None: if isinstance(new_dim, tuple): new_dim = Dim(*new_dim) if not isinstance(new_dim, Dim): - raise TypeError( - f"Expected dim to be Dim or tuple[int], got {type(new_dim)}" - ) + raise TypeError(f"Expected dim to be Dim or tuple[int], got {type(new_dim)}") if not all(isinstance(x, int) for x in new_dim): - raise TypeError( - f"Expected Dim elements to be int, got {new_dim}" - ) + raise TypeError(f"Expected Dim elements to be int, got {new_dim}") if any(x <= 0 for x in new_dim): - raise ValueError( - f"Expected elements of Dim to be positive integers, got {Dim}" - ) + raise ValueError(f"Expected elements of Dim to be positive integers, got {Dim}") if hasattr(self, "_corner_qtile"): - self.check_dim_corner_qtile_compatibility( - dim=new_dim, corner_qtile=self._corner_qtile) + self.check_dim_corner_qtile_compatibility(dim=new_dim, corner_qtile=self._corner_qtile) self._dim = new_dim @property @@ -284,35 +264,26 @@ def corner_qtile(self, new_qtile: QuoTile) -> None: if isinstance(new_qtile, Iterable): new_qtile = QuoTile(zns=new_qtile) if not isinstance(new_qtile, QuoTile): - raise TypeError( - f"Expected corner_qtile to be QuoTile, got {type(new_qtile)}" - ) + raise TypeError(f"Expected corner_qtile to be QuoTile, got {type(new_qtile)}") ccoords = [zn.ccoord for zn in new_qtile.zns] x_values = defaultdict(list) y_values = defaultdict(list) for x, y, *_ in ccoords: x_values[x].append(y) y_values[y].append(x) - x_diff = {x: max(x_list) - min(x_list) - for x, x_list in x_values.items()} - y_diff = {y: max(y_list) - min(y_list ) - for y, y_list in y_values.items()} + x_diff = {x: max(x_list) - min(x_list) for x, x_list in x_values.items()} + y_diff = {y: max(y_list) - min(y_list) for y, y_list in y_values.items()} x_jump = self._tile_connector[(1, 0)].x y_jump = self._tile_connector[(0, 1)].y - + for y, xdiff in y_diff.items(): if abs(xdiff) >= y_jump: - raise ValueError( - f"This tile may overlap other tiles on {y = }" - ) + raise ValueError(f"This tile may overlap other tiles on {y = }") for x, ydiff in x_diff.items(): if abs(ydiff) >= x_jump: - raise ValueError( - f"This tile may overlap other tiles on {x = }" - ) + raise ValueError(f"This tile may overlap other tiles on {x = }") if hasattr(self, "_dim"): - self.check_dim_corner_qtile_compatibility( - dim=self._dim, corner_qtile=new_qtile) + self.check_dim_corner_qtile_compatibility(dim=self._dim, corner_qtile=new_qtile) self._corner_qtile = new_qtile def qtile_xy(self, x: int, y: int) -> QuoTile: @@ -321,8 +292,8 @@ def qtile_xy(self, x: int, y: int) -> QuoTile: raise ValueError( f"Expected x to be in {range(self._dim.Lx)} and y to be in {range(self._dim.Ly)}. " f"Got {x, y}." - ) - xy_shift = x*self._tile_connector[(1, 0)] + y*self._tile_connector[(0, 1)] + ) + xy_shift = x * self._tile_connector[(1, 0)] + y * self._tile_connector[(0, 1)] return self._corner_qtile + xy_shift @property @@ -330,46 +301,35 @@ def qtiles(self) -> dict[tuple[int], QuoTile]: """Returns the dictionary where the keys are positions of floor, and the values are the tiles corresponding to the position. """ - if any(par is None - for par in (self._dim, self._corner_qtile, self._tile_connector) - ): + if any(par is None for par in (self._dim, self._corner_qtile, self._tile_connector)): raise AttributeError( f"Cannot access 'qtiles' because either 'dim', 'corner_qtile', or 'tile_connector' is None." - ) + ) return { (x, y): self.qtile_xy(x=x, y=y) for (x, y) in product(range(self._dim.Lx), range(self._dim.Ly)) - } + } @property def zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, and the values are the ZNodes the tile corresponding to the position contains.""" - return { - xy: xy_tile.zns - for xy, xy_tile in self.qtiles.items() - } + return {xy: xy_tile.zns for xy, xy_tile in self.qtiles.items()} @property def ver_zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, and the values are the vertical ZNodes the tile corresponding to the position contains.""" - return { - xy: xy_tile.ver_zns - for xy, xy_tile in self.qtiles.items() - } + return {xy: xy_tile.ver_zns for xy, xy_tile in self.qtiles.items()} @property def hor_zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, and the values are the horizontal ZNodes the tile corresponding to the position contains.""" - return { - xy: xy_tile.hor_zns - for xy, xy_tile in self.qtiles.items() - } + return {xy: xy_tile.hor_zns for xy, xy_tile in self.qtiles.items()} @property def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: @@ -378,12 +338,10 @@ def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: of the quo-external-paths necessary to be covered from z_start to z_end. """ - if any(par is None - for par in (self._dim, self._corner_qtile, self._tile_connector) - ): + if any(par is None for par in (self._dim, self._corner_qtile, self._tile_connector)): raise AttributeError( f"Cannot access 'quo_ext_paths' because either 'dim', 'corner_qtile', or 'tile_connector' is None." - ) + ) result = {"col": defaultdict(list), "row": defaultdict(list)} hor_con = self._tile_connector[(1, 0)] ver_con = self._tile_connector[(0, 1)] @@ -394,10 +352,10 @@ def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: result["row"][row_num].append( ( UWJ(u=hzn_z.u, w=hzn_z.w, j=hzn_z.j), - ZSE(z_start=hzn_z.z, z_end=hzn_z.z+self._dim.Lx-1) - ) + ZSE(z_start=hzn_z.z, z_end=hzn_z.z + self._dim.Lx - 1), ) - elif hor_con == PlaneShift(2, 0): + ) + elif hor_con == PlaneShift(2, 0): result["row"] = dict() else: raise NotImplementedError @@ -408,8 +366,8 @@ def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: result["col"][col_num].append( ( UWJ(u=vzn_z.u, w=vzn_z.w, j=vzn_z.j), - ZSE(z_start=vzn_z.z, z_end=vzn_z.z+self._dim.Ly-1) - ) + ZSE(z_start=vzn_z.z, z_end=vzn_z.z + self._dim.Ly - 1), + ) ) elif ver_con == PlaneShift(0, 2): result["col"] = dict() @@ -422,11 +380,11 @@ def __repr__(self) -> str: tile_connector_str = ")" else: tile_connector_str = ", {self._tile_connector!r})" - return f"{type(self).__name__}({self._corner_qtile!r}, {self._dim!r}"+tile_connector_str + return f"{type(self).__name__}({self._corner_qtile!r}, {self._dim!r}" + tile_connector_str def __str__(self) -> str: if self._tile_connector == self.tile_connector0: tile_connector_str = ")" else: tile_connector_str = ", {self._tile_connector})" - return f"{type(self).__name__}({self._corner_qtile}, {self._dim})"+tile_connector_str + return f"{type(self).__name__}({self._corner_qtile}, {self._dim})" + tile_connector_str diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 3366576a..fe01f231 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -15,27 +15,25 @@ # ================================================================================================ - - from __future__ import annotations -from typing import Callable + from collections import namedtuple -import networkx as nx -import dwave_networkx as dnx from functools import cached_property +from typing import Callable + +import dwave_networkx as dnx +import networkx as nx from dwave.system import DWaveSampler from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord -from minorminer.utils.zephyr.node_edge import ZNode, ZEdge, ZShape - +from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape UWKJ = namedtuple("UWKJ", ["u", "w", "k", "j"]) ZSE = namedtuple("ZSE", ["z_start", "z_end"]) - class ZSurvey: """Takes a Zephyr graph or DWaveSampler with Zephyr topology and - initializes a survey of the existing/missing nodes, edges. Also, gives a survey of + initializes a survey of the existing/missing nodes, edges. Also, gives a survey of external paths. Args: @@ -50,10 +48,11 @@ class ZSurvey: >>> print(f"Number of missing edges with both endpoints present is {survey.num_extra_missing_edges}") Number of missing edges with both endpoints present is 18 """ + def __init__( self, G: nx.Graph | DWaveSampler, - ) -> None: + ) -> None: self._shape, self._input_coord_type = self.get_shape_coord(G) if isinstance(G, nx.Graph): @@ -65,56 +64,51 @@ def __init__( self._nodes: set[ZNode] = { ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape) for v in G_nodes - } + } self._edges: set[ZEdge] = { ZEdge( ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape), - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape) - ) + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape), + ) for (u, v) in G_edges - } + } @staticmethod def get_shape_coord(G: nx.Graph | DWaveSampler) -> dict[str, ZShape | str]: """Returns the shape, coordinates of G, which must be a zephyr graph or DWaveSampler with zephyr topology. """ + def graph_shape_coord(G: nx.Graph) -> dict[str, int | str]: G_info = G.graph G_top = G_info.get("family") if G_top != "zephyr": - raise ValueError( - f"Expected a graph with zephyr topology, got {G_top}" - ) + raise ValueError(f"Expected a graph with zephyr topology, got {G_top}") m, t, coord = G_info.get("rows"), G_info.get("tile"), G_info.get("labels") return ZShape(m=m, t=t), coord - + def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: sampler_top: dict[str, str | int] = sampler.properties.get("topology") if sampler_top.get("type") != "zephyr": - raise ValueError( - f"Expected a sampler with zephyr topology, got {sampler_top}" - ) + raise ValueError(f"Expected a sampler with zephyr topology, got {sampler_top}") nodes: list[int] = sampler.nodelist edges: list[tuple[int]] = sampler.edgelist for v in nodes: if not isinstance(v, int): raise NotImplementedError( f"This is implemented only for nodelist containing 'int' elements , got {v}" - ) + ) for e in edges: if not isinstance(e, tuple): raise NotImplementedError( f"This is implemented only for edgelist containing 'tuple' elements, got {e}" - ) + ) if len(e) != 2: - raise ValueError( - f"Expected tuple of length 2 in edgelist, got {e}" - ) + raise ValueError(f"Expected tuple of length 2 in edgelist, got {e}") if not isinstance(e[0], int) or not isinstance(e[1], int): raise NotImplementedError( f"This is implemented only for 'tuple[int]' edgelist, got {e}" - ) + ) coord: str = "int" return ZShape(*sampler_top.get("shape")), coord @@ -123,9 +117,7 @@ def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: if isinstance(G, DWaveSampler): return sampler_shape_coord(G) else: - raise TypeError( - f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}" - ) + raise TypeError(f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}") @cached_property def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: @@ -138,7 +130,7 @@ def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: else: raise ValueError( f"Expected 'int' or 'coordinate' for self.coord, got {self._input_coord_type}" - ) + ) @property def shape(self) -> ZShape: @@ -158,7 +150,7 @@ def missing_nodes(self) -> set[ZNode]: parent_nodes = [ ZNode(coord=ZephyrCoord(*v), shape=self._shape) for v in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).nodes() - ] + ] return {v for v in parent_nodes if not v in self._nodes} @property @@ -171,12 +163,11 @@ def missing_edges(self) -> set[ZEdge]: """Returns the ZEdges of the sampler/graph which are missing compared to perfect yield Zephyr graph on the same shape.""" parent_edges = [ - ZEdge( - ZNode(coord=u, shape=self.shape), - ZNode(coord=v, shape=self.shape) - ) - for (u, v) in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).edges() - ] + ZEdge(ZNode(coord=u, shape=self.shape), ZNode(coord=v, shape=self.shape)) + for (u, v) in dnx.zephyr_graph( + m=self._shape.m, t=self._shape.t, coordinates=True + ).edges() + ] return {e for e in parent_edges if not e in self._edges} @property @@ -184,12 +175,7 @@ def extra_missing_edges(self) -> set[ZEdge]: """Returns the ZEdges of the sampler/graph which are missing compared to perfect yield Zephyr graph on the same shape and are not incident with a missing node.""" - return { - e - for e in self.missing_edges - if e[0] in self._nodes - and e[1] in self._nodes - } + return {e for e in self.missing_edges if e[0] in self._nodes and e[1] in self._nodes} @property def num_nodes(self) -> int: @@ -220,26 +206,20 @@ def num_extra_missing_edges(self) -> int: def neighbors( self, v: ZNode, - nbr_kind: str | None=None, - ) -> set[ZNode]: + nbr_kind: str | None = None, + ) -> set[ZNode]: """Returns the neighbours of v when restricted to nbr_kind""" if not isinstance(v, ZNode): - raise TypeError( - f"Expected v to be ZNode, got {v}" - ) + raise TypeError(f"Expected v to be ZNode, got {v}") if v in self.missing_nodes: return {} - return { - v_nbr - for v_nbr in v.neighbors(nbr_kind=nbr_kind) - if ZEdge(v, v_nbr) in self._edges - } + return {v_nbr for v_nbr in v.neighbors(nbr_kind=nbr_kind) if ZEdge(v, v_nbr) in self._edges} def incident_edges( self, v: ZNode, - nbr_kind: str | None=None, - ) -> set[ZEdge]: + nbr_kind: str | None = None, + ) -> set[ZEdge]: """Returns the edges incident with v when restricted to nbr_kind""" nbrs = self.neighbors(v, nbr_kind=nbr_kind) if len(nbrs) == 0: @@ -249,8 +229,8 @@ def incident_edges( def degree( self, v: ZNode, - nbr_kind: str | None=None, - ) -> int: + nbr_kind: str | None = None, + ) -> int: """Returns the degree of v when restricted to nbr_kind""" return len(self.neighbors(v, nbr_kind=nbr_kind)) @@ -259,7 +239,7 @@ def _ext_path(self, uwkj: UWKJ) -> set[ZSE]: Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) for each non-overlapping external path segment of uwkj. """ - z_vals = list(range(self.shape.m)) # As in zephyr coordinates + z_vals = list(range(self.shape.m)) # As in zephyr coordinates def _ext_seg(z_start: int) -> ZSE | None: """ @@ -275,16 +255,16 @@ def _ext_seg(z_start: int) -> ZSE | None: return None if z_start == upper_z: return ZSE(z_start, z_start) - next_ext = ZNode(coord=ZephyrCoord(*uwkj, z_start+1), shape=self.shape) + next_ext = ZNode(coord=ZephyrCoord(*uwkj, z_start + 1), shape=self.shape) ext_edge = ZEdge(cur, next_ext) if ext_edge in self.missing_edges: return ZSE(z_start, z_start) - is_extensible = _ext_seg(z_start+1) + is_extensible = _ext_seg(z_start + 1) if is_extensible is None: return ZSE(z_start, z_start) return ZSE(z_start, is_extensible.z_end) - uwkj_sur: set[ZSE] = set() + uwkj_sur: set[ZSE] = set() while z_vals: z_start = z_vals[0] seg = _ext_seg(z_start=z_start) @@ -292,7 +272,7 @@ def _ext_seg(z_start: int) -> ZSE | None: z_vals.remove(z_start) else: uwkj_sur.add(seg) - for i in range(seg.z_start, seg.z_end+1): + for i in range(seg.z_start, seg.z_end + 1): z_vals.remove(i) return uwkj_sur @@ -303,13 +283,9 @@ def calculate_external_paths_stretch(self) -> dict[UWKJ, set[ZSE]]: """ uwkj_vals = [ UWKJ(u=u, w=w, k=k, j=j) - for u in range(2) # As in zephyr coordinates - for w in range(2*self.shape.m+1) # As in zephyr coordinates - for k in range(self.shape.t) # As in zephyr coordinates - for j in range(2) # As in zephyr coordinates - ] - return { - uwkj: self._ext_path(uwkj) - for uwkj in uwkj_vals - } - + for u in range(2) # As in zephyr coordinates + for w in range(2 * self.shape.m + 1) # As in zephyr coordinates + for k in range(self.shape.t) # As in zephyr coordinates + for j in range(2) # As in zephyr coordinates + ] + return {uwkj: self._ext_path(uwkj) for uwkj in uwkj_vals} diff --git a/minorminer/utils/zephyr/zephyr.py b/minorminer/utils/zephyr/zephyr.py index d7a56164..00c2a8ed 100644 --- a/minorminer/utils/zephyr/zephyr.py +++ b/minorminer/utils/zephyr/zephyr.py @@ -14,12 +14,11 @@ # # ================================================================================================ -from dwave_networkx.generators.zephyr import zephyr_graph import networkx as nx - +from dwave_networkx.generators.zephyr import zephyr_graph from minorminer.busclique import busgraph_cache -__all__ = ['find_clique_embedding', 'find_biclique_embedding'] +__all__ = ["find_clique_embedding", "find_biclique_embedding"] def _get_target_graph(m=None, target_graph=None): @@ -130,5 +129,7 @@ def find_biclique_embedding(a, b, m=None, target_graph=None): if not embedding: raise ValueError("No biclique embedding found") - return ({x: embedding[anodes.index(x)] for x in anodes}, - {y: embedding[bnodes.index(y) + len(anodes)] for y in bnodes}) + return ( + {x: embedding[anodes.index(x)] for x in anodes}, + {y: embedding[bnodes.index(y) + len(anodes)] for y in bnodes}, + ) From a394ead548db9d11078b6882eda5497d284aadda Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 21 Mar 2025 21:55:32 -0700 Subject: [PATCH 004/127] Allow Edge input to be any type, not just int + Add check_edge_valid flag to ZEdge --- minorminer/utils/zephyr/node_edge.py | 79 ++++++++++++++++------------ 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index bee01fc1..87156f79 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -21,36 +21,37 @@ from itertools import product from typing import Callable, Generator, Iterable -from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, - cartesian_to_zephyr, zephyr_to_cartesian) +from minorminer.utils.zephyr.coordinate_systems import ( + CartesianCoord, + ZephyrCoord, + cartesian_to_zephyr, + zephyr_to_cartesian, +) from minorminer.utils.zephyr.plane_shift import PlaneShift ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) class Edge: - """Initializes an Edge with 'int' nodes x, y. + """Initializes an Edge with nodes x, y. Args: - x (int): One endpoint of edge. - y (int): Another endpoint of edge. - - Raises: - TypeError: If either of x or y is not 'int'. + x : One endpoint of edge. + y : Another endpoint of edge. """ def __init__( self, - x: int, - y: int, + x, + y, ) -> None: + self._edge = self._set_edge(x, y) - if not all(isinstance(var, int) for var in (x, y)): - raise TypeError(f"Expected x, y to be 'int', got {type(x), type(y)}") + def _set_edge(self, x, y): if x < y: - self._edge = (x, y) + return (x, y) else: - self._edge = (y, x) + return (y, x) def __hash__(self): return hash(self._edge) @@ -74,6 +75,8 @@ class ZEdge(Edge): Args: x (ZNode): One endpoint of edge. y (ZNode): Another endpoint of edge. + check_edge_valid (bool, optional): Flag to whether check the validity of values and types of x, y. + Defaults to True. Raises: TypeError: If either of x or y is not 'ZNode'. @@ -82,12 +85,12 @@ class ZEdge(Edge): Zephyr graph. Example 1: - >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge + >>> from zephyr_utils.node_edge import ZNode, ZEdge >>> e = ZEdge(ZNode((3, 2)), ZNode((7, 2))) >>> print(e) ZEdge(ZNode(CartesianCoord(x=3, y=2, k=None)), ZNode(CartesianCoord(x=7, y=2, k=None))) Example 2: - >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge + >>> from zephyr_utils.node_edge import ZNode, ZEdge >>> ZEdge(ZNode((2, 3)), ZNode((6, 3))) # raises error, since the two are not neighbors """ @@ -95,22 +98,22 @@ def __init__( self, x: ZNode, y: ZNode, + check_edge_valid: bool = True, ) -> None: - if not isinstance(x, ZNode) or not isinstance(y, ZNode): - raise TypeError(f"Expected x, y to be ZNode, got {type(x), type(y)}") - if x.shape != y.shape: - raise ValueError(f"Expected x, y to have the same shape, got {x.shape, y.shape}") - self._kind = None - for kind in ("internal", "external", "odd"): - if x.is_neighbor(y, nbr_kind=kind): - self._kind = kind - break - if self._kind is None: - raise ValueError(f"Expected x, y to be neighbours, got {x, y}") - if x < y: - self._edge = (x, y) - else: - self._edge = (y, x) + if check_edge_valid: + if not isinstance(x, ZNode) or not isinstance(y, ZNode): + raise TypeError(f"Expected x, y to be ZNode, got {type(x), type(y)}") + if x.shape != y.shape: + raise ValueError(f"Expected x, y to have the same shape, got {x.shape, y.shape}") + kind_found = False + for kind in ("internal", "external", "odd"): + if x.is_neighbor(y, nbr_kind=kind): + kind_found = True + break + if not kind_found: + raise ValueError(f"Expected x, y to be neighbours, got {x, y}") + + self._edge = self._set_edge(x, y) class ZNode: @@ -129,7 +132,7 @@ class ZNode: must be provided. Example: - >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape + >>> from zephyr_utils.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) >>> zn1.neighbors() [ZNode(CartesianCoord(x=4, y=1, k=None), shape=ZShape(m=5, t=None)), @@ -140,7 +143,7 @@ class ZNode: ZNode(CartesianCoord(x=9, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] - >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape + >>> from zephyr_utils.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) >>> zn1.neighbors(nbr_kind="odd") [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), @@ -615,10 +618,16 @@ def __hash__(self) -> int: def __repr__(self) -> str: if self.convert_to_z: coord = self.zcoord - coord_str = f"{coord.u, coord.w, coord.k, coord.j, coord.z}" + if coord.k is None: + coord_str = f"{coord.u, coord.w, coord.j, coord.z}" + else: + coord_str = f"{coord.u, coord.w, coord.k, coord.j, coord.z}" else: coord = self._ccoord - coord_str = f"{coord.x, coord.y, coord.k}" + if coord.k is None: + coord_str = f"{coord.x, coord.y}" + else: + coord_str = f"{coord.x, coord.y, coord.k}" if self._shape == ZShape(): shape_str = "" else: From cdeeff8e3fc9a63a5b706d475b629b0b8c7fd350 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 21 Mar 2025 21:56:47 -0700 Subject: [PATCH 005/127] Turn off check_edge_valid flag for _edges attribute in ZSurvey --- minorminer/utils/zephyr/survey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index fe01f231..7d69bc4b 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -69,6 +69,7 @@ def __init__( ZEdge( ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape), ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape), + check_edge_valid=False, ) for (u, v) in G_edges } From e6eca6d083140700b15a5ba3ceb36192564b1e4d Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 4 Apr 2025 11:07:45 -0700 Subject: [PATCH 006/127] Correct header --- minorminer/utils/zephyr/coordinate_systems.py | 2 +- minorminer/utils/zephyr/node_edge.py | 2 +- minorminer/utils/zephyr/plane_shift.py | 2 +- minorminer/utils/zephyr/qfloor.py | 2 +- minorminer/utils/zephyr/survey.py | 2 +- minorminer/utils/zephyr/zephyr.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index 4baf5fc1..fe4478e0 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 87156f79..8e56b1d9 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index e7dee233..fe12e4cf 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 28464933..12a7ec85 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 7d69bc4b..ac2d36e4 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/minorminer/utils/zephyr/zephyr.py b/minorminer/utils/zephyr/zephyr.py index 00c2a8ed..30f0523f 100644 --- a/minorminer/utils/zephyr/zephyr.py +++ b/minorminer/utils/zephyr/zephyr.py @@ -1,4 +1,4 @@ -# Copyright 2022 D-Wave Systems Inc. +# Copyright 2022 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 20b79d020fb5170cad6ba883d71791ce63231752 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Tue, 8 Apr 2025 17:50:31 -0700 Subject: [PATCH 007/127] Add NodeKind and EdgeKind and adjust; Add document --- minorminer/utils/zephyr/node_edge.py | 309 ++++++++++++++++++++------- tests/utils/zephyr/test_node_edge.py | 62 +++--- 2 files changed, 261 insertions(+), 110 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 8e56b1d9..4543c673 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -18,20 +18,29 @@ from __future__ import annotations from collections import namedtuple +from enum import Enum +from functools import cached_property from itertools import product from typing import Callable, Generator, Iterable -from minorminer.utils.zephyr.coordinate_systems import ( - CartesianCoord, - ZephyrCoord, - cartesian_to_zephyr, - zephyr_to_cartesian, -) +from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, + cartesian_to_zephyr, zephyr_to_cartesian) from minorminer.utils.zephyr.plane_shift import PlaneShift ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) +class EdgeKind(Enum): + INTERNAL = 1 + EXTERNAL = 2 + ODD = 3 + + +class NodeKind(Enum): + VERTICAL = 0 + HORIZONTAL = 1 + + class Edge: """Initializes an Edge with nodes x, y. @@ -48,6 +57,7 @@ def __init__( self._edge = self._set_edge(x, y) def _set_edge(self, x, y): + """Reutrns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) else: @@ -70,19 +80,19 @@ def __repr__(self) -> str: class ZEdge(Edge): - """Initializes a ZEdge with 'ZNode' nodes x, y + """Initializes a ZEdge with 'ZNode' nodes x, y. Args: x (ZNode): One endpoint of edge. y (ZNode): Another endpoint of edge. - check_edge_valid (bool, optional): Flag to whether check the validity of values and types of x, y. - Defaults to True. + check_edge_valid (bool, optional): Flag to whether check the validity of values and types of ``x``, ``y``. + Defaults to True. Raises: TypeError: If either of x or y is not 'ZNode'. ValueError: If x, y do not have the same shape. - ValueError: If x, y are not neighbours in a perfect yield (quotient) - Zephyr graph. + ValueError: If x, y are not neighbors in a perfect yield (quotient) + Zephyr graph. Example 1: >>> from zephyr_utils.node_edge import ZNode, ZEdge @@ -106,12 +116,12 @@ def __init__( if x.shape != y.shape: raise ValueError(f"Expected x, y to have the same shape, got {x.shape, y.shape}") kind_found = False - for kind in ("internal", "external", "odd"): + for kind in EdgeKind: if x.is_neighbor(y, nbr_kind=kind): kind_found = True break if not kind_found: - raise ValueError(f"Expected x, y to be neighbours, got {x, y}") + raise ValueError(f"Expected x, y to be neighbors, got {x, y}") self._edge = self._set_edge(x, y) @@ -145,7 +155,7 @@ class ZNode: ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] >>> from zephyr_utils.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) - >>> zn1.neighbors(nbr_kind="odd") + >>> zn1.neighbors(nbr_kind=EdgeKind.ODD) [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] """ @@ -272,6 +282,18 @@ def zcoord(self) -> ZephyrCoord: """Returns ZephyrCoordinate corresponding to ccoord""" return cartesian_to_zephyr(self._ccoord) + @cached_property + def node_kind(self) -> NodeKind: + """Returns the node kind of self""" + if self._ccoord.x % 2 == 0: + return NodeKind.VERTICAL + return NodeKind.HORIZONTAL + + @property + def direction(self) -> int: + """Returns direction, 0 or 1""" + return self.node_kind.value + def is_quo(self) -> bool: """Decides if the ZNode object is quotient""" return (self._ccoord.k is None) and (self._shape.t is None) @@ -282,29 +304,27 @@ def to_quo(self) -> ZNode: qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) - @property - def direction(self) -> int: - """Returns direction, 0 or 1""" - return self._ccoord.x % 2 - def is_vertical(self) -> bool: """Decides whether self is a vertical qubit""" - return self.direction == 0 + return self.node_kind is NodeKind.VERTICAL def is_horizontal(self) -> bool: """Decides whether self is a horizontal qubit""" - return self.direction == 1 - - def node_kind(self) -> str: - """Returns the node kind, vertical or horizontal""" - return "vertical" if self.is_vertical() else "horizontal" + return self.node_kind is NodeKind.HORIZONTAL def neighbor_kind( self, other: ZNode, - ) -> str | None: - """Returns the kind of coupler between self and other, - 'internal', 'external', 'odd', or None.""" + ) -> EdgeKind | None: + """Returns the kind of edge between self and other; + EdgeKind if there is an edge in perfect Zephyr between them or None. + + Args: + other (ZNode): a ZNode + + Returns: + EdgeKind | None: The edge kind between self and other in perfect Zephyr or None if there is no edge between in perfect Zephyr. + """ if not isinstance(other, ZNode): other = ZNode(other) if self._shape != other._shape: @@ -314,27 +334,37 @@ def neighbor_kind( x1, y1 = coord1.x, coord1.y x2, y2 = coord2.x, coord2.y if abs(x1 - x2) == abs(y1 - y2) == 1: - return "internal" + return EdgeKind.INTERNAL if x1 % 2 != x2 % 2: return if coord1.k != coord2.k: # odd, external neighbors only on the same k return - if self.is_vertical(): # self vertical + if self.node_kind is NodeKind.VERTICAL: # self vertical if x1 != x2: # odd, external neighbors only on the same vertical lines return diff_y = abs(y1 - y2) - return "odd" if diff_y == 2 else "external" if diff_y == 4 else None - else: - if y1 != y2: # odd, external neighbors only on the same horizontal lines - return - diff_x = abs(x1 - x2) - return "odd" if diff_x == 2 else "external" if diff_x == 4 else None + return EdgeKind.ODD if diff_y == 2 else EdgeKind.EXTERNAL if diff_y == 4 else None + + # else, self is horizontal + if y1 != y2: # odd, external neighbors only on the same horizontal lines + return + diff_x = abs(x1 - x2) + return EdgeKind.ODD if diff_x == 2 else EdgeKind.EXTERNAL if diff_x == 4 else None def internal_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of internal neighbors""" + """Generator of internal neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Yields: + ZNode: Internal neighbors of self when restricted by `where`. + """ x, y, _ = self._ccoord convert = self.convert_to_z k_vals = [None] if self._shape.t is None else range(self._shape.t) @@ -351,14 +381,23 @@ def internal_neighbors_generator( ) except GeneratorExit: raise - except Exception: + except (TypeError, ValueError): pass def external_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of external neighbors""" + """Generator of external neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Yields: + ZNode: External neighbors of self when restricted by `where`. + """ x, y, k = self._ccoord convert = self.convert_to_z changing_index = 1 if x % 2 == 0 else 0 @@ -377,14 +416,23 @@ def external_neighbors_generator( ) except GeneratorExit: raise - except Exception: + except (TypeError, ValueError): pass def odd_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of odd neighbors""" + """Generator of odd neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Yields: + ZNode: Odd neighbors of self when restricted by `where`. + """ x, y, k = self._ccoord convert = self.convert_to_z changing_index = 1 if x % 2 == 0 else 0 @@ -403,71 +451,123 @@ def odd_neighbors_generator( ) except GeneratorExit: raise - except Exception: + except (TypeError, ValueError): pass def neighbors_generator( self, - nbr_kind: str | Iterable[str] | None = None, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: + """Generator of neighbors of self when restricted by `nbr_kind` and `where`. + + Args: + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Yields: + ZNode: Neighbors of self when restricted by `nbr_kind` and `where`. + """ if nbr_kind is None: - kinds = {"internal", "external", "odd"} + kinds = {kind for kind in EdgeKind} + elif isinstance(nbr_kind, EdgeKind): + kinds = {nbr_kind} else: - if isinstance(nbr_kind, str): - kinds = {nbr_kind} - elif isinstance(nbr_kind, Iterable): - kinds = set(nbr_kind) - else: - raise TypeError(f"Expected 'str' or Iterable[str] or None for nbr_kind") - kinds = kinds.intersection({"internal", "external", "odd"}) - if "internal" in kinds: + kinds = set(nbr_kind) + if EdgeKind.INTERNAL in kinds: for cc in self.internal_neighbors_generator(where=where): yield cc - if "external" in kinds: + if EdgeKind.EXTERNAL in kinds: for cc in self.external_neighbors_generator(where=where): yield cc - if "odd" in kinds: + if EdgeKind.ODD in kinds: for cc in self.odd_neighbors_generator(where=where): yield cc def neighbors( self, - nbr_kind: str | Iterable[str] | None = None, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns neighbors when restricted to nbr_kind and where""" + """Returns set of neighbors of self when restricted by `nbr_kind` and `where`. + + Args: + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + Returns: + set[ZNode]: Set of neighbors of self when restricted by `nbr_kind` and `where`. + """ return set(self.neighbors_generator(nbr_kind=nbr_kind, where=where)) def internal_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns internal neighbors when restricted to where""" - return set(self.neighbors_generator(nbr_kind="internal", where=where)) + """Returns the set of internal neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + Returns: + set[ZNode]: Set of internal neighbors of self when restricted by `where`. + """ + return set(self.neighbors_generator(nbr_kind=EdgeKind.INTERNAL, where=where)) def external_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns external neighbors when restricted to where""" - return set(self.neighbors_generator(nbr_kind="external", where=where)) + """Returns the set of external neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + Returns: + set[ZNode]: Set of external neighbors of self when restricted by `where`. + """ + return set(self.neighbors_generator(nbr_kind=EdgeKind.EXTERNAL, where=where)) def odd_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns odd neighbors when restricted to where""" - return set(self.neighbors_generator(nbr_kind="odd", where=where)) + """Returns the set of odd neighbors of self when restricted by `where`. + + Args: + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + Returns: + set[ZNode]: Set of odd neighbors of self when restricted by `where`. + """ + return set(self.neighbors_generator(nbr_kind=EdgeKind.ODD, where=where)) def is_internal_neighbor( self, other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Tells if another ZNode is an internal neighbor when restricted to where""" - if not isinstance(other, ZNode): - raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") + """Returns whether other is an internal neighbor of self when restricted by `where`. + + Args: + other (ZNode): Another instance to compare with self. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + bool: Whether other is an internal neighbor of self when restricted by `where`. + """ for nbr in self.internal_neighbors_generator(where=where): if other == nbr: return True @@ -478,9 +578,17 @@ def is_external_neighbor( other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Tells if another ZNode is an external neighbor when restricted to where""" - if not isinstance(other, ZNode): - raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") + """Returns whether other is an external neighbor of self when restricted by `where`. + + Args: + other (ZNode): Another instance to compare with self. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + bool: Whether other is an external neighbor of self when restricted by `where`. + """ for nbr in self.external_neighbors_generator(where=where): if other == nbr: return True @@ -491,9 +599,17 @@ def is_odd_neighbor( other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Tells if another ZNode is an odd neighbor when restricted to where""" - if not isinstance(other, ZNode): - raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") + """Returns whether other is an odd neighbor of self when restricted by `where`. + + Args: + other (ZNode): Another instance to compare with self. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + bool: Whether other is an odd neighbor of self when restricted by `where`. + """ for nbr in self.odd_neighbors_generator(where=where): if other == nbr: return True @@ -502,12 +618,23 @@ def is_odd_neighbor( def is_neighbor( self, other: ZNode, - nbr_kind: str | Iterable[str] | None = None, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Tells if another ZNode is a neighbor when restricted to nbr_kind and where""" - if not isinstance(other, ZNode): - raise TypeError(f"Expected {other} to be ZNode, got {type(other)}") + """Returns whether other is a neighbor of self when restricted by `nbr_kind` and `where`. + + Args: + other (ZNode): Another instance to compare with self. + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + bool: Whether other is a neighbor of self when restricted by `nbr_kind` and `where`. + """ for nbr in self.neighbors_generator(nbr_kind=nbr_kind, where=where): if other == nbr: return True @@ -515,18 +642,42 @@ def is_neighbor( def incident_edges( self, - nbr_kind: str | Iterable[str] | None = None, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> list[ZEdge]: - """Returns incident edges when restricted to nbr_kind and where""" + """Returns incident edges with self when restricted by `nbr_kind` and `where`. + + Args: + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + list[ZEdge]: list edges incident with self when restricted by `nbr_kind` and `where`. + """ return [ZEdge(self, v) for v in self.neighbors(nbr_kind=nbr_kind, where=where)] def degree( self, - nbr_kind: str | Iterable[str] | None = None, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> int: - """Returns degree when restricted to nbr_kind and where""" + """Returns degree of self when restricted by `nbr_kind` and `where`. + + Args: + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + int: degree of self when restricted by `nbr_kind` and `where`. + """ return len(self.neighbors(nbr_kind=nbr_kind, where=where)) def __eq__(self, other: ZNode) -> bool: diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py index 7716de6d..8cd7afa3 100644 --- a/tests/utils/zephyr/test_node_edge.py +++ b/tests/utils/zephyr/test_node_edge.py @@ -19,7 +19,7 @@ from itertools import product from minorminer.utils.zephyr.coordinate_systems import CartesianCoord, ZephyrCoord -from minorminer.utils.zephyr.node_edge import Edge, ZEdge, ZNode, ZShape +from minorminer.utils.zephyr.node_edge import Edge, EdgeKind, NodeKind, ZEdge, ZNode, ZShape from minorminer.utils.zephyr.plane_shift import PlaneShift @@ -265,17 +265,17 @@ def test_direction_node_kind(self) -> None: self.assertEqual(zn.direction, u) if u == 0: self.assertTrue(zn.is_vertical()) - self.assertEqual(zn.node_kind(), "vertical") + self.assertEqual(zn.node_kind, NodeKind.VERTICAL) else: self.assertTrue(zn.is_horizontal()) - self.assertEqual(zn.node_kind(), "horizontal") + self.assertEqual(zn.node_kind, NodeKind.HORIZONTAL) def test_neighbor_kind(self) -> None: zn = ZNode((0, 1)) - self.assertTrue(zn.neighbor_kind(ZNode((1, 0))) == "internal") - self.assertTrue(zn.neighbor_kind(ZNode((1, 2))) == "internal") - self.assertTrue(zn.neighbor_kind(ZNode((0, 3))) == "odd") - self.assertTrue(zn.neighbor_kind(ZNode((0, 5))) == "external") + self.assertTrue(zn.neighbor_kind(ZNode((1, 0))) is EdgeKind.INTERNAL) + self.assertTrue(zn.neighbor_kind(ZNode((1, 2))) is EdgeKind.INTERNAL) + self.assertTrue(zn.neighbor_kind(ZNode((0, 3))) is EdgeKind.ODD) + self.assertTrue(zn.neighbor_kind(ZNode((0, 5))) is EdgeKind.EXTERNAL) self.assertTrue(zn.neighbor_kind(ZNode((0, 7))) is None) self.assertTrue(zn.neighbor_kind(ZNode((1, 6))) is None) @@ -319,48 +319,48 @@ def test_degree(self) -> None: qzn1 = ZNode(coord=(x, y), shape=ZShape(m=m)) self.assertEqual(len(qzn1.neighbors()), 8) self.assertEqual(qzn1.degree(), 8) - self.assertEqual(qzn1.degree(nbr_kind="internal"), 4) - self.assertEqual(qzn1.degree(nbr_kind="external"), 2) - self.assertEqual(qzn1.degree(nbr_kind="odd"), 2) + self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.INTERNAL), 4) + self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.ODD), 2) for t in [1, 4, 6]: zn1 = ZNode(coord=(x, y, 0), shape=ZShape(m=m, t=t)) self.assertEqual(zn1.degree(), 4 * t + 4) - self.assertEqual(zn1.degree(nbr_kind="internal"), 4 * t) - self.assertEqual(zn1.degree(nbr_kind="external"), 2) - self.assertEqual(zn1.degree(nbr_kind="odd"), 2) + self.assertEqual(zn1.degree(nbr_kind=EdgeKind.INTERNAL), 4 * t) + self.assertEqual(zn1.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(zn1.degree(nbr_kind=EdgeKind.ODD), 2) qzn2 = ZNode(coord=(0, 1)) self.assertEqual(qzn2.degree(), 4) - self.assertEqual(qzn2.degree(nbr_kind="internal"), 2) - self.assertEqual(qzn2.degree(nbr_kind="external"), 1) - self.assertEqual(qzn2.degree(nbr_kind="odd"), 1) + self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.INTERNAL), 2) + self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.EXTERNAL), 1) + self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.ODD), 1) for t in [1, 4, 6]: zn2 = ZNode(coord=(0, 1, 0), shape=ZShape(t=t)) self.assertEqual(zn2.degree(), 2 * t + 2) - self.assertEqual(zn2.degree(nbr_kind="internal"), 2 * t) - self.assertEqual(zn2.degree(nbr_kind="external"), 1) - self.assertEqual(zn2.degree(nbr_kind="odd"), 1) + self.assertEqual(zn2.degree(nbr_kind=EdgeKind.INTERNAL), 2 * t) + self.assertEqual(zn2.degree(nbr_kind=EdgeKind.EXTERNAL), 1) + self.assertEqual(zn2.degree(nbr_kind=EdgeKind.ODD), 1) qzn3 = ZNode((24, 5), ZShape(m=6)) self.assertEqual(qzn3.degree(), 6) - self.assertEqual(qzn3.degree(nbr_kind="internal"), 2) - self.assertEqual(qzn3.degree(nbr_kind="external"), 2) - self.assertEqual(qzn3.degree(nbr_kind="odd"), 2) + self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.INTERNAL), 2) + self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.ODD), 2) for t in [1, 5, 6]: zn3 = ZNode((24, 5, 0), ZShape(m=6, t=t)) self.assertEqual(zn3.degree(), 2 * t + 4) - self.assertEqual(zn3.degree(nbr_kind="internal"), 2 * t) - self.assertEqual(zn3.degree(nbr_kind="external"), 2) - self.assertEqual(zn3.degree(nbr_kind="odd"), 2) + self.assertEqual(zn3.degree(nbr_kind=EdgeKind.INTERNAL), 2 * t) + self.assertEqual(zn3.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(zn3.degree(nbr_kind=EdgeKind.ODD), 2) qzn4 = ZNode((24, 5)) self.assertEqual(qzn4.degree(), 8) - self.assertEqual(qzn4.degree(nbr_kind="internal"), 4) - self.assertEqual(qzn4.degree(nbr_kind="external"), 2) - self.assertEqual(qzn4.degree(nbr_kind="odd"), 2) + self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.INTERNAL), 4) + self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.ODD), 2) for t in [1, 5, 6]: zn4 = ZNode((24, 5, 0), ZShape(t=t)) self.assertEqual(zn4.degree(), 4 * t + 4) - self.assertEqual(zn4.degree(nbr_kind="internal"), 4 * t) - self.assertEqual(zn4.degree(nbr_kind="external"), 2) - self.assertEqual(zn4.degree(nbr_kind="odd"), 2) + self.assertEqual(zn4.degree(nbr_kind=EdgeKind.INTERNAL), 4 * t) + self.assertEqual(zn4.degree(nbr_kind=EdgeKind.EXTERNAL), 2) + self.assertEqual(zn4.degree(nbr_kind=EdgeKind.ODD), 2) From 4a699f241e86153af12f5321c9542722d9823943 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Tue, 8 Apr 2025 17:51:09 -0700 Subject: [PATCH 008/127] Reformat --- tests/utils/zephyr/test_coordinate_systems.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/utils/zephyr/test_coordinate_systems.py b/tests/utils/zephyr/test_coordinate_systems.py index 57f3f331..fdbad58a 100644 --- a/tests/utils/zephyr/test_coordinate_systems.py +++ b/tests/utils/zephyr/test_coordinate_systems.py @@ -17,8 +17,12 @@ import unittest -from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, - cartesian_to_zephyr, zephyr_to_cartesian) +from minorminer.utils.zephyr.coordinate_systems import ( + CartesianCoord, + ZephyrCoord, + cartesian_to_zephyr, + zephyr_to_cartesian, +) class TestCoordinateSystems(unittest.TestCase): From fd78154bb8a05b02fcd4f8cc75569591166c3272 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Tue, 8 Apr 2025 18:08:08 -0700 Subject: [PATCH 009/127] Explicitly return None --- minorminer/utils/zephyr/node_edge.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 4543c673..ab519684 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -328,7 +328,7 @@ def neighbor_kind( if not isinstance(other, ZNode): other = ZNode(other) if self._shape != other._shape: - return + return None coord1 = self._ccoord coord2 = other._ccoord x1, y1 = coord1.x, coord1.y @@ -336,18 +336,18 @@ def neighbor_kind( if abs(x1 - x2) == abs(y1 - y2) == 1: return EdgeKind.INTERNAL if x1 % 2 != x2 % 2: - return + return None if coord1.k != coord2.k: # odd, external neighbors only on the same k - return + return None if self.node_kind is NodeKind.VERTICAL: # self vertical if x1 != x2: # odd, external neighbors only on the same vertical lines - return + return None diff_y = abs(y1 - y2) return EdgeKind.ODD if diff_y == 2 else EdgeKind.EXTERNAL if diff_y == 4 else None # else, self is horizontal if y1 != y2: # odd, external neighbors only on the same horizontal lines - return + return None diff_x = abs(x1 - x2) return EdgeKind.ODD if diff_x == 2 else EdgeKind.EXTERNAL if diff_x == 4 else None From 9f7e5ab841c21b74bb7ddb875472aa38faf93a14 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Tue, 8 Apr 2025 18:15:40 -0700 Subject: [PATCH 010/127] Simplify, Add space --- minorminer/utils/zephyr/node_edge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index ab519684..057b35c3 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -113,14 +113,14 @@ def __init__( if check_edge_valid: if not isinstance(x, ZNode) or not isinstance(y, ZNode): raise TypeError(f"Expected x, y to be ZNode, got {type(x), type(y)}") + if x.shape != y.shape: raise ValueError(f"Expected x, y to have the same shape, got {x.shape, y.shape}") - kind_found = False + for kind in EdgeKind: if x.is_neighbor(y, nbr_kind=kind): - kind_found = True break - if not kind_found: + else: raise ValueError(f"Expected x, y to be neighbors, got {x, y}") self._edge = self._set_edge(x, y) From 13a29b4972d1e7985889b2579a26b47dff983368 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:18:46 -0700 Subject: [PATCH 011/127] Clarify arguments Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 057b35c3..a015f353 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -83,8 +83,8 @@ class ZEdge(Edge): """Initializes a ZEdge with 'ZNode' nodes x, y. Args: - x (ZNode): One endpoint of edge. - y (ZNode): Another endpoint of edge. + x (ZNode): Endpoint of edge. Must have same shape as ``y``. + y (ZNode): Endpoint of edge. Must have same shape as ``x`` check_edge_valid (bool, optional): Flag to whether check the validity of values and types of ``x``, ``y``. Defaults to True. From a15e92b5cabe59afe1e8c8630e87870d4eec332a Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:19:19 -0700 Subject: [PATCH 012/127] Capitalize Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index a015f353..a784bce0 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -130,7 +130,7 @@ class ZNode: """Initializes 'ZNode' with coord and optional shape. Args: - coord (CartesianCoord | ZephyrCoord | tuple[int]): coordinate in (quotient) Zephyr or (quotient) Cartesian + coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian shape (ZShape | tuple[int | None] | None, optional): shape of Zephyr graph containing ZNode. m: grid size, t: tile size Defaults to None. From 94e85be81d9e5a409129ddd5668e4d2a167a5525 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:21:16 -0700 Subject: [PATCH 013/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index a784bce0..b8211fea 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -135,7 +135,7 @@ class ZNode: m: grid size, t: tile size Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. - Defaults to None. + Defaults to None. Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), shape = None raises ValueError. In this case the tile size of Zephyr, t, From 904a4a3fd6cb547fa25c9e0a16091f4087977ec9 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:22:05 -0700 Subject: [PATCH 014/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index b8211fea..f0111fcb 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -195,18 +195,22 @@ def shape(self, new_shape) -> None: new_shape = ZShape() elif isinstance(new_shape, tuple): new_shape = ZShape(*new_shape) + if not isinstance(new_shape, ZShape): raise TypeError( f"Expected shape to be tuple[int | None] or ZShape or None, got {type(new_shape)}" ) + if hasattr(self, "_ccoord"): if (self._ccoord.k is None) != (new_shape.t is None): raise ValueError( f"ccoord, shape must be both quotient or non-quotient, got {self._ccoord, new_shape}" ) + for var, val in {"m": new_shape.m, "t": new_shape.t}.items(): if (val is not None) and (not isinstance(val, int)): raise TypeError(f"Expected {var} to be None or 'int', got {type(val)}") + self._shape = new_shape @property From bb8afc9dd44a098374445b1bf72addbf701430f9 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:23:00 -0700 Subject: [PATCH 015/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index f0111fcb..1bf7b26f 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -227,7 +227,7 @@ def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): raise TypeError( f"Expected ccoord to be CartesianCoord or tuple[int], got {type(new_ccoord)}" ) - for c in (new_ccoord.x, new_ccoord.y): + for c in new_ccoord: if not isinstance(c, int): raise TypeError(f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}") if c < 0: From 0f4a1bdb09f87de487e1b5091a50d1505c1fc082 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:23:48 -0700 Subject: [PATCH 016/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 1bf7b26f..50159756 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -257,8 +257,7 @@ def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): @staticmethod def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: - """Takes a tuple[int] and returns the corresponding CartesianCoord - or ZephyrCoord""" + """Takes a tuple[int] and returns the corresponding ``CartesianCoord`` or ``ZephyrCoord``""" if (not isinstance(coord, tuple)) or (not all(isinstance(c, int) for c in coord)): raise TypeError(f"Expected {coord} to be a tuple[int], got {coord}") if any(c < 0 for c in coord): From c53380d141207a302f5e1abfa6413e5bc5bc9718 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Tue, 8 Apr 2025 18:25:08 -0700 Subject: [PATCH 017/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 50159756..2107a61b 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -270,7 +270,7 @@ def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: return CartesianCoord(x=x, y=y) if len(k) == 0 else CartesianCoord(x=x, y=y, k=k[0]) if len_coord in (4, 5): u, w, *k, j, z = coord - for var, val in {"u": u, "j": j}.items(): + for var, val in [("u", u), ("j", j)]: if not val in [0, 1]: raise ValueError(f"Expected {var} to be in [0, 1], got {val}") return ( From 974489cc015e2990ee4df22953be9a4e374857f7 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 9 Apr 2025 10:27:01 -0700 Subject: [PATCH 018/127] Clarify documentation, set shape in init --- minorminer/utils/zephyr/node_edge.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 2107a61b..842953c9 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -84,7 +84,7 @@ class ZEdge(Edge): Args: x (ZNode): Endpoint of edge. Must have same shape as ``y``. - y (ZNode): Endpoint of edge. Must have same shape as ``x`` + y (ZNode): Endpoint of edge. Must have same shape as ``x``. check_edge_valid (bool, optional): Flag to whether check the validity of values and types of ``x``, ``y``. Defaults to True. @@ -131,15 +131,16 @@ class ZNode: Args: coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian - shape (ZShape | tuple[int | None] | None, optional): shape of Zephyr graph containing ZNode. - m: grid size, t: tile size + shape (ZShape | tuple[int | None] | None, optional): Shape of the Zephyr graph containing this ZNode. + If a ZShape is passed, it should be a namedtuple with fields `m` (grid size of the Zephyr graph) and `t` (tile size of the Zephyr graph). Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. Defaults to None. - Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), - shape = None raises ValueError. In this case the tile size of Zephyr, t, - must be provided. + Note: + If the `k` field of the given `coord` (whether a `CartesianCoord` or `ZephyrCoord`) is not `None`, + then `shape` must be provided and its `t` field (the tile size of the Zephyr graph) must not be `None`. + Otherwise, a `ValueError` will be raised. Example: >>> from zephyr_utils.node_edge import ZNode, ZShape @@ -166,7 +167,10 @@ def __init__( shape: ZShape | tuple[int | None] | None = None, convert_to_z: bool | None = None, ) -> None: - self.shape = shape + if shape: + self.shape = shape + else: + self.shape = ZShape() if convert_to_z is None: self.convert_to_z = len(coord) in (4, 5) @@ -189,11 +193,9 @@ def shape(self) -> ZShape | None: return self._shape @shape.setter - def shape(self, new_shape) -> None: + def shape(self, new_shape: ZShape | tuple[int | None]) -> None: """Sets a new value for shape""" - if new_shape is None: - new_shape = ZShape() - elif isinstance(new_shape, tuple): + if isinstance(new_shape, tuple): new_shape = ZShape(*new_shape) if not isinstance(new_shape, ZShape): @@ -207,7 +209,7 @@ def shape(self, new_shape) -> None: f"ccoord, shape must be both quotient or non-quotient, got {self._ccoord, new_shape}" ) - for var, val in {"m": new_shape.m, "t": new_shape.t}.items(): + for var, val in new_shape._asdict().items(): if (val is not None) and (not isinstance(val, int)): raise TypeError(f"Expected {var} to be None or 'int', got {type(val)}") From 70d84ed616e04331a0f181015fdf1fb3e2774596 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 9 Apr 2025 14:24:00 -0700 Subject: [PATCH 019/127] Add doc to EdgeKind, NodeKind, clarify doc re. direction, Change name --- minorminer/utils/zephyr/node_edge.py | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 842953c9..ba974409 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -19,7 +19,6 @@ from collections import namedtuple from enum import Enum -from functools import cached_property from itertools import product from typing import Callable, Generator, Iterable @@ -31,14 +30,16 @@ class EdgeKind(Enum): + """Kinds of an edge (coupler) between two nodes in a Zephyr graph.""" INTERNAL = 1 EXTERNAL = 2 ODD = 3 class NodeKind(Enum): - VERTICAL = 0 - HORIZONTAL = 1 + """Kinds of a node (qubit) in a Zephyr graph.""" + VERTICAL = 0 # The `u` coordinate of a Zephyr coordinate of a vertical node (qubit) is zero. + HORIZONTAL = 1 # The `u` coordinate of a Zephyr coordinate of a horizontal node (qubit) is one. class Edge: @@ -179,7 +180,7 @@ def __init__( # convert coord to CartesianCoord or ZephyrCoord if not isinstance(coord, (CartesianCoord, ZephyrCoord)): - coord = self.get_coord(coord) + coord = self.tuple_to_coord(coord) # convert coord to CartesianCoord if isinstance(coord, ZephyrCoord): @@ -189,7 +190,7 @@ def __init__( @property def shape(self) -> ZShape | None: - """Returns the shape of the Zephyr graph ZNode belongs to.""" + """Returns the shape of the Zephyr graph the node belongs to.""" return self._shape @shape.setter @@ -258,7 +259,7 @@ def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): self._ccoord = new_ccoord @staticmethod - def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: + def tuple_to_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: """Takes a tuple[int] and returns the corresponding ``CartesianCoord`` or ``ZephyrCoord``""" if (not isinstance(coord, tuple)) or (not all(isinstance(c, int) for c in coord)): raise TypeError(f"Expected {coord} to be a tuple[int], got {coord}") @@ -275,11 +276,10 @@ def get_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: for var, val in [("u", u), ("j", j)]: if not val in [0, 1]: raise ValueError(f"Expected {var} to be in [0, 1], got {val}") - return ( - ZephyrCoord(u=u, w=w, j=j, z=z) - if len(k) == 0 - else ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) - ) + if len(k) == 0: + return ZephyrCoord(u=u, w=w, j=j, z=z) + return ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) + raise ValueError(f"coord can have length 2, 3, 4 or 5, got {len_coord}") @property @@ -287,7 +287,7 @@ def zcoord(self) -> ZephyrCoord: """Returns ZephyrCoordinate corresponding to ccoord""" return cartesian_to_zephyr(self._ccoord) - @cached_property + @property def node_kind(self) -> NodeKind: """Returns the node kind of self""" if self._ccoord.x % 2 == 0: @@ -296,7 +296,7 @@ def node_kind(self) -> NodeKind: @property def direction(self) -> int: - """Returns direction, 0 or 1""" + """Returns the direction of node, i.e. its `u` coordinate in Zephyr coordinates.""" return self.node_kind.value def is_quo(self) -> bool: @@ -310,11 +310,11 @@ def to_quo(self) -> ZNode: return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) def is_vertical(self) -> bool: - """Decides whether self is a vertical qubit""" + """Returns True if the node represents a vertical qubit (i.e., its `u` coordinate in Zephyr coordinates is 0).""" return self.node_kind is NodeKind.VERTICAL def is_horizontal(self) -> bool: - """Decides whether self is a horizontal qubit""" + """Returns True if the node represents a horizontal qubit (i.e., its `u` coordinate in Zephyr coordinates is 1).""" return self.node_kind is NodeKind.HORIZONTAL def neighbor_kind( From 03765f1ef058d706f78dc2af623d763137ff0994 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 11 Apr 2025 13:16:00 -0700 Subject: [PATCH 020/127] Edit document --- minorminer/utils/zephyr/node_edge.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index ba974409..615cbd8e 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -502,7 +502,7 @@ def neighbors( Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): - Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + Edge kind filter. Restricts returned neighbors to those connected by the given edge kind(s). If None, no filtering is applied. Defaults to None. where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, @@ -631,7 +631,7 @@ def is_neighbor( Args: other (ZNode): Another instance to compare with self. nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): - Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + Edge kind filter. Restricts neighbors to those connected by the given edge kind(s). If None, no filtering is applied. Defaults to None. where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, @@ -654,7 +654,7 @@ def incident_edges( Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): - Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + Edge kind filter. Restricts returned edges to those having the given edge kind(s). If None, no filtering is applied. Defaults to None. where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, @@ -674,7 +674,7 @@ def degree( Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): - Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). + Edge kind filter. Restricts counting the neighbors to those connected by the given edge kind(s). If None, no filtering is applied. Defaults to None. where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, From edfde0f8301dd5c0fbe82a3f5c4811b8db18fa8d Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 11 Apr 2025 13:19:04 -0700 Subject: [PATCH 021/127] Change nbr_kind argument type --- minorminer/utils/zephyr/qfloor.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 12a7ec85..a86869d3 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -22,7 +22,7 @@ from typing import Callable, Iterable, Iterator from minorminer.utils.zephyr.coordinate_systems import * -from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape +from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape, EdgeKind from minorminer.utils.zephyr.plane_shift import PlaneShift Dim = namedtuple("Dim", ["Lx", "Ly"]) @@ -115,11 +115,23 @@ def hor_zns(self) -> list[ZNode]: def edges( self, + nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, - nbr_kind: str | Iterable[str] | None = None, ) -> list[ZEdge]: - """Returns the list of edges of the graph induced on the tile, - when resticted to nbr_kind and where.""" + """Returns the list of edges of the graph induced on the tile, when restricted by `nbr_kind` and `where`. + + Args: + nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): + Edge kind filter. Restricts returned edges to those having the given edge kind(s). + If None, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, + or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + + Returns: + list[ZEdge]: List of edges of the graph induced on the tile, when restricted by `nbr_kind` and `where`. + """ + zns = self._zns tile_coords = [zn.zcoord for zn in zns] if self.convert_to_z else [zn.ccoord for zn in zns] where_tile = lambda coord: where(coord) and coord in tile_coords From 7a8da7e060834fc11b4c0cfc937f969abb9b9820 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 11 Apr 2025 13:44:56 -0700 Subject: [PATCH 022/127] Add edge_kind to ZEdge --- minorminer/utils/zephyr/node_edge.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 615cbd8e..1fcb7dc6 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -58,7 +58,7 @@ def __init__( self._edge = self._set_edge(x, y) def _set_edge(self, x, y): - """Reutrns ordered tuple corresponding to the set {x, y}.""" + """Returns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) else: @@ -120,12 +120,17 @@ def __init__( for kind in EdgeKind: if x.is_neighbor(y, nbr_kind=kind): + self._edge_kind = kind break else: raise ValueError(f"Expected x, y to be neighbors, got {x, y}") self._edge = self._set_edge(x, y) + @property + def edge_kind(self) -> EdgeKind: + return self._edge_kind + class ZNode: """Initializes 'ZNode' with coord and optional shape. From f525e258dbfb947dd43daedf28bedee50500c17e Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 25 Apr 2025 14:44:31 -0700 Subject: [PATCH 023/127] Disable changing znodes of QuoTile, change Dim to Dim_2D --- minorminer/utils/zephyr/qfloor.py | 76 +++++++++++++++---------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index a86869d3..3b05f304 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -19,13 +19,14 @@ from collections import defaultdict, namedtuple from itertools import product +from functools import cached_property from typing import Callable, Iterable, Iterator from minorminer.utils.zephyr.coordinate_systems import * from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape, EdgeKind from minorminer.utils.zephyr.plane_shift import PlaneShift -Dim = namedtuple("Dim", ["Lx", "Ly"]) +Dim_2D = namedtuple("Dim_2D", ["Lx", "Ly"]) UWJ = namedtuple("UWJ", ["u", "w", "j"]) ZSE = namedtuple("ZSE", ["z_start", "z_end"]) @@ -50,65 +51,64 @@ def __init__( self, zns: Iterable[ZNode], ) -> None: - self.zns = zns - - @property - def zns(self) -> list[ZNode]: - """Returns the sorted list of ZNodes the tile contains""" - return self._zns - - @zns.setter - def zns(self, new_zns: Iterable[ZNode]) -> None: - """Sets the zns""" - if not isinstance(new_zns, Iterable): - raise TypeError(f"Expected {new_zns} to be Iterable[ZNode], got {type(new_zns)}") - for zn in new_zns: - if not isinstance(zn, ZNode): - raise TypeError(f"Expected elements of {new_zns} to be ZNode, got {type(zn)}") + if len(zns) == 0: + raise ValueError(f"Expected zns to be non-empty, got {zns}") + for zn in zns: if not zn.is_quo(): - raise ValueError(f"Expected elements of {new_zns} to be quotient, got {zn}") - new_zns_shape = {zn.shape for zn in new_zns} - if len(new_zns_shape) != 1: + raise ValueError(f"Expected elements of {zns} to be quotient, got {zn}") + zns_shape = {zn.shape for zn in zns} + if len(zns_shape) != 1: raise ValueError( - f"Expected all elements of zns to have the same shape, got {new_zns_shape}" + f"Expected all elements of zns to have the same shape, got {zns_shape}" ) - temp_zns = sorted(list(set(new_zns))) - if len(temp_zns) == 0: - return temp_zns + temp_zns = sorted(list(set(zns))) seed_convert = temp_zns[0].convert_to_z - result = [] + zns_result = [] for zn in temp_zns: zn.convert_to_z = seed_convert - result.append(zn) - self._zns = result + zns_result.append(zn) + self._zns: list[ZNode] = zns_result + + @property + def zns(self) -> list[ZNode]: + """Returns the sorted list of ZNodes the tile contains""" + return self._zns + + @cached_property + def index_zns(self) -> dict[int, ZNode]: + return {i: zn for i, zn in enumerate(self._zns)} @property def seed(self) -> ZNode: """Returns the smallest ZNode in Zns""" return self._zns[0] - @property + @cached_property def shifts(self) -> list[PlaneShift]: """Returns the list of shift of each ZNode in zns from seed""" seed = self.seed return [zn - seed for zn in self._zns] - @property + @cached_property + def index_shifts(self) -> dict[int, PlaneShift]: + return {i: ps for i, ps in enumerate(self.shifts)} + + @cached_property def shape(self) -> ZShape: """Returns the shape of the Zephyr graph the tile belongs to.""" return self.seed.shape - @property + @cached_property def convert_to_z(self) -> bool: """Returns the convert_to_z attribute of the ZNodes of the tile""" return self.seed.convert_to_z - @property + @cached_property def ver_zns(self) -> list[ZNode]: """Returns the list of vertical ZNodes of the tile""" return [zn for zn in self._zns if zn.is_vertical()] - @property + @cached_property def hor_zns(self) -> list[ZNode]: """Returns the list of horizontal ZNodes of the tile""" return [zn for zn in self._zns if zn.is_horizontal()] @@ -198,7 +198,7 @@ class QuoFloor: def __init__( self, corner_qtile: QuoTile | Iterable[ZNode], - dim: Dim | tuple[int], + dim: Dim_2D | tuple[int], tile_connector: dict[tuple[int], PlaneShift] = tile_connector0, ) -> None: self.tile_connector = tile_connector @@ -228,7 +228,7 @@ def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: def check_dim_corner_qtile_compatibility( self, - dim: Dim, + dim: Dim_2D, corner_qtile: QuoTile, ) -> None: """Checks whether dimension and corner tile are compatible, i.e. @@ -246,21 +246,21 @@ def check_dim_corner_qtile_compatibility( raise ValueError(f"{dim, corner_qtile} are not compatible") @property - def dim(self) -> Dim: + def dim(self) -> Dim_2D: """Returns dimension of the floor""" return self._dim @dim.setter - def dim(self, new_dim: Dim | tuple[int]) -> None: + def dim(self, new_dim: Dim_2D | tuple[int]) -> None: """Sets dimension of the floor""" if isinstance(new_dim, tuple): - new_dim = Dim(*new_dim) - if not isinstance(new_dim, Dim): + new_dim = Dim_2D(*new_dim) + if not isinstance(new_dim, Dim_2D): raise TypeError(f"Expected dim to be Dim or tuple[int], got {type(new_dim)}") if not all(isinstance(x, int) for x in new_dim): raise TypeError(f"Expected Dim elements to be int, got {new_dim}") if any(x <= 0 for x in new_dim): - raise ValueError(f"Expected elements of Dim to be positive integers, got {Dim}") + raise ValueError(f"Expected elements of Dim to be positive integers, got {Dim_2D}") if hasattr(self, "_corner_qtile"): self.check_dim_corner_qtile_compatibility(dim=new_dim, corner_qtile=self._corner_qtile) self._dim = new_dim From d496bfc2ece4a4adf806a938f383fb0e010b06bb Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:37:37 -0700 Subject: [PATCH 024/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 1fcb7dc6..041bcb39 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -50,11 +50,7 @@ class Edge: y : Another endpoint of edge. """ - def __init__( - self, - x, - y, - ) -> None: + def __init__(self, x: int , y: int) -> None: self._edge = self._set_edge(x, y) def _set_edge(self, x, y): From b415ad34a5438aacc13b3237eda4862a7fe20c6c Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:38:11 -0700 Subject: [PATCH 025/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 041bcb39..5db8924c 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -66,7 +66,7 @@ def __hash__(self): def __getitem__(self, index: int) -> int: return self._edge[index] - def __eq__(self, other: Edge): + def __eq__(self, other: Edge) -> bool: return self._edge == other._edge def __str__(self) -> str: From bd461a4600cf5c02d0347b4f3a69a7b7ef2dd5cc Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:39:50 -0700 Subject: [PATCH 026/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 5db8924c..94a7d683 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -46,8 +46,8 @@ class Edge: """Initializes an Edge with nodes x, y. Args: - x : One endpoint of edge. - y : Another endpoint of edge. + x: One endpoint of edge. + y: Another endpoint of edge. """ def __init__(self, x: int , y: int) -> None: From 4a1f63d421e1c6b8fe13f5ae89c59cf3d2393a47 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:40:36 -0700 Subject: [PATCH 027/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 94a7d683..51bd0718 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -53,7 +53,7 @@ class Edge: def __init__(self, x: int , y: int) -> None: self._edge = self._set_edge(x, y) - def _set_edge(self, x, y): + def _set_edge(self, x: int, y: int) -> tuple[int, int]: """Returns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) From e1ea6c6f34287af230d1c0934d2349cbfec617d6 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:41:02 -0700 Subject: [PATCH 028/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index fe12e4cf..25b81b2a 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -66,7 +66,7 @@ def y(self) -> int: return self._shift.y def __mul__(self, scale: int | float) -> PlaneShift: - """Multiplies the self from left by the number value``scale``. + """Multiplies the self from left by the number value ``scale``. Args: scale (int | float): The scale for left-multiplying self with. From 36f39c7843a384ff559fcdc2ea3414b162951249 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:41:38 -0700 Subject: [PATCH 029/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index 25b81b2a..ba7648f4 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -80,10 +80,12 @@ def __mul__(self, scale: int | float) -> PlaneShift: """ if not isinstance(scale, (int, float)): raise TypeError(f"Expected scale to be int or float, got {type(scale)}") + new_shift_x = scale * self._shift.x new_shift_y = scale * self._shift.y if int(new_shift_x) != new_shift_x or int(new_shift_y) != new_shift_y: raise ValueError(f"{scale} cannot be multiplied by {self}") + return PlaneShift(int(new_shift_x), int(new_shift_y)) def __rmul__(self, scale: int | float) -> PlaneShift: From 0f3c70da6fd25b8ae6ecbbe425411db3c3cdfb98 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:41:57 -0700 Subject: [PATCH 030/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index ba7648f4..ba2b48dc 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -89,7 +89,7 @@ def __mul__(self, scale: int | float) -> PlaneShift: return PlaneShift(int(new_shift_x), int(new_shift_y)) def __rmul__(self, scale: int | float) -> PlaneShift: - """Multiplies the self from right by the number value``scale``. + """Multiplies the self from right by the number value ``scale``. Args: scale (int | float): The scale for right-multiplying self with. From 04df70498f03ae6811a15c0f27c4332cf3e07173 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:42:13 -0700 Subject: [PATCH 031/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index ba2b48dc..b6020ce9 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -106,6 +106,7 @@ def __rmul__(self, scale: int | float) -> PlaneShift: def __add__(self, other: PlaneShift) -> PlaneShift: """ Adds another PlaneShift object to self. + Args: other (PlaneShift): The object to add self by. From abd56fa9f580839d4c065ede06d04192316da5c0 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:42:29 -0700 Subject: [PATCH 032/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index b6020ce9..2e8c7311 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -124,7 +124,7 @@ def __iter__(self) -> Iterator[int]: return self._shift.__iter__() def __len__(self) -> int: - return self._shift.__len__() + return len(self._shift) def __hash__(self) -> int: return self._shift.__hash__() From 1bc85f03488e048deac1f695dbdc1a8d8c31c638 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:42:45 -0700 Subject: [PATCH 033/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index 2e8c7311..8b681a21 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -127,7 +127,7 @@ def __len__(self) -> int: return len(self._shift) def __hash__(self) -> int: - return self._shift.__hash__() + return hash(self._shift) def __getitem__(self, key) -> int: return self._shift.__getitem__(key) From 4f86bdceaf8cf9ea1e1c349b302360a9236b7ce9 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:43:10 -0700 Subject: [PATCH 034/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index 8b681a21..cf8d93f6 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -130,7 +130,7 @@ def __hash__(self) -> int: return hash(self._shift) def __getitem__(self, key) -> int: - return self._shift.__getitem__(key) + return self._shift[key] def __eq__(self, other: PlaneShift) -> bool: if not isinstance(other, PlaneShift): From 5c1c0f145ef7dfc2d2c75d54bdc6ac619be26b81 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:44:26 -0700 Subject: [PATCH 035/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index cf8d93f6..df06e741 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -137,10 +137,6 @@ def __eq__(self, other: PlaneShift) -> bool: raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") return self._shift == other._shift - def __ne__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return not self._shift == other._shift def __lt__(self, other: PlaneShift) -> bool: if not isinstance(other, PlaneShift): From 9c2b0c8d68067e689b702d8105fae0f13176503c Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:44:51 -0700 Subject: [PATCH 036/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index ac2d36e4..15f2d8c1 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -85,6 +85,7 @@ def graph_shape_coord(G: nx.Graph) -> dict[str, int | str]: G_top = G_info.get("family") if G_top != "zephyr": raise ValueError(f"Expected a graph with zephyr topology, got {G_top}") + m, t, coord = G_info.get("rows"), G_info.get("tile"), G_info.get("labels") return ZShape(m=m, t=t), coord From 7c29e5f98b1e49736b7be18c29b98ece5755f764 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:45:55 -0700 Subject: [PATCH 037/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 15f2d8c1..6d7e7262 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -118,8 +118,8 @@ def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: return graph_shape_coord(G) if isinstance(G, DWaveSampler): return sampler_shape_coord(G) - else: - raise TypeError(f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}") + + raise TypeError(f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}") @cached_property def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: From 2479071fb1bfccdcc6c092c602f62fb621826f67 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:46:33 -0700 Subject: [PATCH 038/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 6d7e7262..d39e76a9 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -141,7 +141,7 @@ def shape(self) -> ZShape: @property def nodes(self) -> set[ZNode]: - """Returns the ZNodes of the sampler/graph""" + """Returns the ``ZNode``s of the sampler/graph.""" return self._nodes @cached_property From eee8d401a806a089afc60bae9864e9ab72c1d64d Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:47:28 -0700 Subject: [PATCH 039/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index d39e76a9..48aab8ce 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -237,8 +237,7 @@ def degree( return len(self.neighbors(v, nbr_kind=nbr_kind)) def _ext_path(self, uwkj: UWKJ) -> set[ZSE]: - """ - Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) + """Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) for each non-overlapping external path segment of uwkj. """ z_vals = list(range(self.shape.m)) # As in zephyr coordinates From e2a891b3eda5a3219899b21e240a23230d2efe27 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:47:50 -0700 Subject: [PATCH 040/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 48aab8ce..95f87331 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -243,8 +243,7 @@ def _ext_path(self, uwkj: UWKJ) -> set[ZSE]: z_vals = list(range(self.shape.m)) # As in zephyr coordinates def _ext_seg(z_start: int) -> ZSE | None: - """ - If (u, w, k, j, z_start) does not exist, returns None. + """If (u, w, k, j, z_start) does not exist, returns None. Else, finds the external path segment starting at z_start going to right, and returns the endpoints of the segment (z_start, z_end). """ From e05fe418a8bd85862d859a3f8b496fd6a4f66881 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:48:57 -0700 Subject: [PATCH 041/127] Update minorminer/utils/zephyr/survey.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/survey.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 95f87331..f2adc475 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -250,18 +250,23 @@ def _ext_seg(z_start: int) -> ZSE | None: cur: ZNode = ZNode(coord=ZephyrCoord(*uwkj, z_start), shape=self.shape) if cur in self.missing_nodes: return None + upper_z: int = z_vals[-1] if z_start > upper_z: return None + if z_start == upper_z: return ZSE(z_start, z_start) + next_ext = ZNode(coord=ZephyrCoord(*uwkj, z_start + 1), shape=self.shape) ext_edge = ZEdge(cur, next_ext) if ext_edge in self.missing_edges: return ZSE(z_start, z_start) + is_extensible = _ext_seg(z_start + 1) if is_extensible is None: return ZSE(z_start, z_start) + return ZSE(z_start, is_extensible.z_end) uwkj_sur: set[ZSE] = set() From db9b0d9b0b09458b13fe97488f2308c8356c71b2 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:49:16 -0700 Subject: [PATCH 042/127] Update minorminer/utils/zephyr/coordinate_systems.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/coordinate_systems.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index fe4478e0..624db2eb 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -28,8 +28,9 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: - """Converts a CartesianCoord to its corresponding ZephyrCoord. - Note: It assumes the given CartesianCoord is valid. + """Converts a ``CartesianCoord`` to its corresponding ``ZephyrCoord``. + + Note: It assumes the given ``CartesianCoord`` is valid. Args: ccoord (CartesianCoord): The coodinate in Cartesian system to be converted. From c6c7c72ad7b1eb808f5cd21dc641e0aa86283bab Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 09:49:36 -0700 Subject: [PATCH 043/127] Update minorminer/utils/zephyr/coordinate_systems.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/coordinate_systems.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index 624db2eb..2acfdf23 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -53,7 +53,8 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: def zephyr_to_cartesian(zcoord: ZephyrCoord) -> CartesianCoord: - """Converts a ZephyrCoord to its corresponding CartesianCoord. + """Converts a ``ZephyrCoord``` to its corresponding ``CartesianCoord``. + Note: It assumes the given ZephyrCoord is valid. Args: From 3cbeec1fe30f7e3d4d1377eddccd69e2ac821f58 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:20:32 -0700 Subject: [PATCH 044/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 51bd0718..97fc0c86 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -132,10 +132,10 @@ class ZNode: """Initializes 'ZNode' with coord and optional shape. Args: - coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian + coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian. shape (ZShape | tuple[int | None] | None, optional): Shape of the Zephyr graph containing this ZNode. - If a ZShape is passed, it should be a namedtuple with fields `m` (grid size of the Zephyr graph) and `t` (tile size of the Zephyr graph). - Defaults to None. + If a ZShape is passed, it should be a namedtuple with fields `m` (grid size of the Zephyr graph) + and `t` (tile size of the Zephyr graph). Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. Defaults to None. From 166cf1520100dfab66b5c2f1c5de83de978cfd64 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:21:26 -0700 Subject: [PATCH 045/127] Update minorminer/utils/zephyr/qfloor.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/qfloor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 3b05f304..f6cfa5ab 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -80,7 +80,7 @@ def index_zns(self) -> dict[int, ZNode]: @property def seed(self) -> ZNode: - """Returns the smallest ZNode in Zns""" + """Returns the smallest ZNode in Zns.""" return self._zns[0] @cached_property From 628610a402dbd9ce27a563a0515f438a63618e1d Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:21:52 -0700 Subject: [PATCH 046/127] Update minorminer/utils/zephyr/qfloor.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/qfloor.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index f6cfa5ab..d989d81a 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -178,15 +178,17 @@ class QuoFloor: tile_connector (dict[tuple[int], PlaneShift], optional): Determines how to get the tiles (x+1, y) and (x, y+1) from tile (x, y). Defaults to tile_connector0. + Example: .. code-block:: python - >>> from minorminer.utils.zephyr.node_edge import ZNode - >>> from minorminer.utils.zephyr.qfloor import QuoFloor - >>> coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] - >>> zns = [ZNode(coord=c) for c in coords] - >>> floor = QuoFloor(corner_qtile=zns, dim=(3, 5)) - >>> print(f"{floor.qtile_xy(2, 3) = }") - floor.qtile_xy(2, 3) = QuoTile[ZNode(CartesianCoord(x=8, y=13, k=None)), ZNode(CartesianCoord(x=9, y=12, k=None)), ZNode(CartesianCoord(x=9, y=14, k=None)), ZNode(CartesianCoord(x=10, y=13, k=None)), ZNode(CartesianCoord(x=10, y=15, k=None)), ZNode(CartesianCoord(x=11, y=14, k=None)), ZNode(CartesianCoord(x=11, y=16, k=None)), ZNode(CartesianCoord(x=12, y=15, k=None))] + + >>> from minorminer.utils.zephyr.node_edge import ZNode + >>> from minorminer.utils.zephyr.qfloor import QuoFloor + >>> coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] + >>> zns = [ZNode(coord=c) for c in coords] + >>> floor = QuoFloor(corner_qtile=zns, dim=(3, 5)) + >>> print(f"{floor.qtile_xy(2, 3) = }") + floor.qtile_xy(2, 3) = QuoTile[ZNode(CartesianCoord(x=8, y=13, k=None)), ZNode(CartesianCoord(x=9, y=12, k=None)), ZNode(CartesianCoord(x=9, y=14, k=None)), ZNode(CartesianCoord(x=10, y=13, k=None)), ZNode(CartesianCoord(x=10, y=15, k=None)), ZNode(CartesianCoord(x=11, y=14, k=None)), ZNode(CartesianCoord(x=11, y=16, k=None)), ZNode(CartesianCoord(x=12, y=15, k=None))] """ From 7125603ed8e960f43984e783e96e535142a5b3f2 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:23:58 -0700 Subject: [PATCH 047/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 97fc0c86..50403362 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -264,21 +264,27 @@ def tuple_to_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: """Takes a tuple[int] and returns the corresponding ``CartesianCoord`` or ``ZephyrCoord``""" if (not isinstance(coord, tuple)) or (not all(isinstance(c, int) for c in coord)): raise TypeError(f"Expected {coord} to be a tuple[int], got {coord}") + if any(c < 0 for c in coord): raise ValueError(f"Expected elements of coord to be non-negative, got {coord}") + len_coord = len(coord) if len_coord in (2, 3): x, y, *k = coord if x % 2 == y % 2: raise ValueError(f"Expected x, y to differ in parity, got {x, y}") + return CartesianCoord(x=x, y=y) if len(k) == 0 else CartesianCoord(x=x, y=y, k=k[0]) + if len_coord in (4, 5): u, w, *k, j, z = coord for var, val in [("u", u), ("j", j)]: if not val in [0, 1]: raise ValueError(f"Expected {var} to be in [0, 1], got {val}") + if len(k) == 0: - return ZephyrCoord(u=u, w=w, j=j, z=z) + return ZephyrCoord(u=u, w=w, j=j, z=z) + return ZephyrCoord(u=u, w=w, k=k[0], j=j, z=z) raise ValueError(f"coord can have length 2, 3, 4 or 5, got {len_coord}") From 486d8fa5c5dfc999b85e3803f036d958559d516d Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:28:00 -0700 Subject: [PATCH 048/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 50403362..c65897c6 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -328,8 +328,9 @@ def neighbor_kind( self, other: ZNode, ) -> EdgeKind | None: - """Returns the kind of edge between self and other; - EdgeKind if there is an edge in perfect Zephyr between them or None. + """Returns the kind of edge between two ZNodes. + + :class:`EdgeKind` if there is an edge in perfect Zephyr between them or ``None``. Args: other (ZNode): a ZNode From 5886529597f7cf62c70c44933aaa927b487862bd Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:29:03 -0700 Subject: [PATCH 049/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index c65897c6..115e5376 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -333,7 +333,7 @@ def neighbor_kind( :class:`EdgeKind` if there is an edge in perfect Zephyr between them or ``None``. Args: - other (ZNode): a ZNode + other (ZNode): The neigboring :class:`ZNode`. Returns: EdgeKind | None: The edge kind between self and other in perfect Zephyr or None if there is no edge between in perfect Zephyr. From 26191b101be281c6a2dfbae953fc02d81c7208fa Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:29:39 -0700 Subject: [PATCH 050/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 115e5376..3b33f991 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -340,27 +340,34 @@ def neighbor_kind( """ if not isinstance(other, ZNode): other = ZNode(other) + if self._shape != other._shape: return None + coord1 = self._ccoord coord2 = other._ccoord x1, y1 = coord1.x, coord1.y x2, y2 = coord2.x, coord2.y if abs(x1 - x2) == abs(y1 - y2) == 1: return EdgeKind.INTERNAL + if x1 % 2 != x2 % 2: return None + if coord1.k != coord2.k: # odd, external neighbors only on the same k return None + if self.node_kind is NodeKind.VERTICAL: # self vertical if x1 != x2: # odd, external neighbors only on the same vertical lines return None + diff_y = abs(y1 - y2) return EdgeKind.ODD if diff_y == 2 else EdgeKind.EXTERNAL if diff_y == 4 else None # else, self is horizontal if y1 != y2: # odd, external neighbors only on the same horizontal lines return None + diff_x = abs(x1 - x2) return EdgeKind.ODD if diff_x == 2 else EdgeKind.EXTERNAL if diff_x == 4 else None From fd295aa8c180eeadba9492a44068b40346fa1f52 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:32:47 -0700 Subject: [PATCH 051/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 3b33f991..1ee059b4 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -170,7 +170,7 @@ def __init__( convert_to_z: bool | None = None, ) -> None: if shape: - self.shape = shape + self.shape = ZShape(*shape) else: self.shape = ZShape() From 8ee09a74395227a115b3d859ce29f1da4bc5883a Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 10:59:52 -0700 Subject: [PATCH 052/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 1ee059b4..5093ffc2 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -709,14 +709,6 @@ def __eq__(self, other: ZNode) -> bool: ) return self._ccoord == other._ccoord - def __ne__(self, other: ZNode) -> bool: - if not isinstance(other, ZNode): - raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") - if self._shape != other._shape: - raise ValueError( - f"Expected {self, other} to have the same shape, got {self._shape, other._shape}" - ) - return self._ccoord != other._ccoord def __gt__(self, other: ZNode) -> bool: if not isinstance(other, ZNode): From 66e65ba8d9514b8b48658ead9f48b86278872a21 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 11:01:34 -0700 Subject: [PATCH 053/127] capitalize --- minorminer/utils/zephyr/qfloor.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index d989d81a..85ae8784 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -192,10 +192,10 @@ class QuoFloor: """ - implemented_connectors: tuple[dict[tuple[int], PlaneShift]] = ( + IMPLEMENTED_CONNECTORS: tuple[dict[tuple[int], PlaneShift]] = ( {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 4)}, ) - tile_connector0 = implemented_connectors[0] + tile_connector0 = IMPLEMENTED_CONNECTORS[0] def __init__( self, @@ -217,10 +217,10 @@ def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: """Sets the tile connector""" if not isinstance(new_connector, dict): raise TypeError(f"Expected tile_connector to be dict, got {type(new_connector)}") - if not new_connector in self.implemented_connectors: + if not new_connector in self.IMPLEMENTED_CONNECTORS: raise NotImplementedError( f"{new_connector} not implemented. " - f"Availabale options are {self.implemented_connectors}" + f"Availabale options are {self.IMPLEMENTED_CONNECTORS}" ) if any(dir_con not in new_connector.keys() for dir_con in ((1, 0), (0, 1))): raise ValueError( From 659c815b09837c69111ec4c4f09c0e8c6508f9a3 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 11:12:38 -0700 Subject: [PATCH 054/127] Update docstring --- minorminer/utils/zephyr/qfloor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 85ae8784..2dfe6fa5 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -32,10 +32,10 @@ class QuoTile: - """Initializes a 'QuoTile' object with zns. + """Initializes a 'QuoTile' instance from a collection of ``ZNode`` objects. Args: - zns (Iterable[ZNode]): The ZNodes the tile contains + zns (Iterable[ZNode]): The ``ZNode``s the tile contains. Example: .. code-block:: python From c9a8d358900e3692e73b6947d2f5b193f30d95a0 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:27:14 -0700 Subject: [PATCH 055/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 5093ffc2..88cefa89 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -772,8 +772,8 @@ def __sub__( y_shift: int = self._ccoord.y - other._ccoord.y try: return PlaneShift(x_shift=x_shift, y_shift=y_shift) - except: - raise ValueError(f"{other} cannot be subtracted from {self}") + except ValueError as e: + raise ValueError(f"{other} cannot be subtracted from {self}") from e def __hash__(self) -> int: return (self._ccoord, self._shape).__hash__() From d0fe844d4d72341a30eff996a9739ae99fd1b47c Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 12:35:07 -0700 Subject: [PATCH 056/127] Disable 'float' scale --- minorminer/utils/zephyr/plane_shift.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index df06e741..fec8ec2f 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -65,28 +65,19 @@ def y(self) -> int: """Returns the shift in y direction""" return self._shift.y - def __mul__(self, scale: int | float) -> PlaneShift: + def __mul__(self, scale: int) -> PlaneShift: """Multiplies the self from left by the number value ``scale``. Args: - scale (int | float): The scale for left-multiplying self with. - - Raises: - TypeError: If scale is not 'int' or 'float'. - ValueError: If the resulting PlaneShift has non-whole values. + scale (int): The scale for left-multiplying self with. Returns: PlaneShift: The result of left-multiplying self by scale. """ - if not isinstance(scale, (int, float)): - raise TypeError(f"Expected scale to be int or float, got {type(scale)}") new_shift_x = scale * self._shift.x new_shift_y = scale * self._shift.y - if int(new_shift_x) != new_shift_x or int(new_shift_y) != new_shift_y: - raise ValueError(f"{scale} cannot be multiplied by {self}") - - return PlaneShift(int(new_shift_x), int(new_shift_y)) + return PlaneShift(new_shift_x, new_shift_y) def __rmul__(self, scale: int | float) -> PlaneShift: """Multiplies the self from right by the number value ``scale``. From 3365f07a9f7b912d76bfbe768054fcae62241369 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 12:43:41 -0700 Subject: [PATCH 057/127] Correct header --- minorminer/utils/zephyr/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/__init__.py b/minorminer/utils/zephyr/__init__.py index 5c73648c..b7b0a2b8 100644 --- a/minorminer/utils/zephyr/__init__.py +++ b/minorminer/utils/zephyr/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 5ef99a2bd9751c3565b7897ae4a5280abae10a3b Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 13:35:13 -0700 Subject: [PATCH 058/127] Add, correct documentation --- minorminer/utils/zephyr/survey.py | 111 +++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 26 deletions(-) diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index f2adc475..32e6e736 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -19,13 +19,13 @@ from collections import namedtuple from functools import cached_property -from typing import Callable +from typing import Callable, Literal import dwave_networkx as dnx import networkx as nx from dwave.system import DWaveSampler from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord -from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape +from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape, EdgeKind UWKJ = namedtuple("UWKJ", ["u", "w", "k", "j"]) ZSE = namedtuple("ZSE", ["z_start", "z_end"]) @@ -75,12 +75,31 @@ def __init__( } @staticmethod - def get_shape_coord(G: nx.Graph | DWaveSampler) -> dict[str, ZShape | str]: - """Returns the shape, coordinates of G, which must be a zephyr graph or - DWaveSampler with zephyr topology. + def get_shape_coord(G: nx.Graph | DWaveSampler) -> tuple[ZShape, Literal["int", "coordinate"]]: """ + Returns the shape and coordinates of G, which must be a zephyr graph or + DWaveSampler with zephyr topology. - def graph_shape_coord(G: nx.Graph) -> dict[str, int | str]: + Args: + G (nx.Graph | DWaveSampler): A zephyr graph or DWaveSampler with zephyr topology. + + Returns: + tuple[ZShape, Literal["int", "coordinate"]]: + - 0-th index indicates the :class:`ZShape` of ``G``. + - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. + """ + def _graph_shape_coord(G: nx.Graph) -> tuple[ZShape, Literal["int", "coordinate"]]: + """ + Returns the shape and coordinates of G, which must be a zephyr graph. + + Args: + G (nx.Graph): A zephyr graph with zephyr topology. + + Returns: + tuple[ZShape, Literal["int", "coordinate"]]: + - 0-th index indicates the :class:`ZShape` of ``G``. + - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. + """ G_info = G.graph G_top = G_info.get("family") if G_top != "zephyr": @@ -89,7 +108,18 @@ def graph_shape_coord(G: nx.Graph) -> dict[str, int | str]: m, t, coord = G_info.get("rows"), G_info.get("tile"), G_info.get("labels") return ZShape(m=m, t=t), coord - def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: + def _sampler_shape_coord(sampler: DWaveSampler) -> tuple[ZShape, Literal["int", "coordinate"]]: + """ + Returns the shape and coordinates of G, which must be a DWaveSampler with zephyr topology. + + Args: + G (DWaveSampler): A DWaveSampler with zephyr topology. + + Returns: + tuple[ZShape, Literal["int", "coordinate"]]: + - 0-th index indicates the :class:`ZShape` of ``G``. + - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. + """ sampler_top: dict[str, str | int] = sampler.properties.get("topology") if sampler_top.get("type") != "zephyr": raise ValueError(f"Expected a sampler with zephyr topology, got {sampler_top}") @@ -115,11 +145,10 @@ def sampler_shape_coord(sampler: DWaveSampler) -> dict[str, int | str]: return ZShape(*sampler_top.get("shape")), coord if isinstance(G, nx.Graph): - return graph_shape_coord(G) + return _graph_shape_coord(G) if isinstance(G, DWaveSampler): - return sampler_shape_coord(G) + return _sampler_shape_coord(G) - raise TypeError(f"Expected G to be networkx.Graph or DWaveSampler, got {type(G)}") @cached_property def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: @@ -136,7 +165,7 @@ def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: @property def shape(self) -> ZShape: - """Returns the ZShape of G""" + """Returns the :class:`ZShape` of ``G``.""" return self._shape @property @@ -208,21 +237,37 @@ def num_extra_missing_edges(self) -> int: def neighbors( self, v: ZNode, - nbr_kind: str | None = None, + nbr_kind: EdgeKind | None = None, ) -> set[ZNode]: - """Returns the neighbours of v when restricted to nbr_kind""" - if not isinstance(v, ZNode): - raise TypeError(f"Expected v to be ZNode, got {v}") + """Returns the neighbours of ``v`` when restricted to ``nbr_kind``. + + Args: + v (ZNode): Node to retrieve its neighbors. + nbr_kind (EdgeKind | None, optional): + Edge kind filter. Restricts returned neighbors to those connected by the given edge kind(s). + If None, no filtering is applied. Defaults to None. + + Returns: + set[ZNode]: Set of neighbors of self when restricted by ``nbr_kind``. + """ if v in self.missing_nodes: - return {} + return set() return {v_nbr for v_nbr in v.neighbors(nbr_kind=nbr_kind) if ZEdge(v, v_nbr) in self._edges} def incident_edges( self, v: ZNode, - nbr_kind: str | None = None, + nbr_kind: EdgeKind | None = None, ) -> set[ZEdge]: - """Returns the edges incident with v when restricted to nbr_kind""" + """Returns the edges incident with ``v`` when restricted to ``nbr_kind``. + + Args: + v (ZNode): Node to retrieve its incident edges. + nbr_kind (EdgeKind | None, optional): _description_. Defaults to None. + + Returns: + set[ZEdge]: Set of edges incident with ``v`` when restricted by ``nbr_kind``. + """ nbrs = self.neighbors(v, nbr_kind=nbr_kind) if len(nbrs) == 0: return set() @@ -231,12 +276,22 @@ def incident_edges( def degree( self, v: ZNode, - nbr_kind: str | None = None, + nbr_kind: EdgeKind | None = None, ) -> int: - """Returns the degree of v when restricted to nbr_kind""" + """Returns degree of ``v`` when restricted by ``nbr_kind``. + + Args: + v (ZNode): Node to calculate its degree. + + nbr_kind (EdgeKind | None, optional): + Edge kind filter. Restricts counting the neighbors to those connected by the given edge kind. + If None, no filtering is applied. Defaults to None. + Returns: + int: Degree of ``v``. + """ return len(self.neighbors(v, nbr_kind=nbr_kind)) - def _ext_path(self, uwkj: UWKJ) -> set[ZSE]: + def _ext_path(self, uwkj: UWKJ) -> list[ZSE]: """Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) for each non-overlapping external path segment of uwkj. """ @@ -269,22 +324,26 @@ def _ext_seg(z_start: int) -> ZSE | None: return ZSE(z_start, is_extensible.z_end) - uwkj_sur: set[ZSE] = set() + uwkj_sur: list[ZSE] = [] while z_vals: z_start = z_vals[0] seg = _ext_seg(z_start=z_start) if seg is None: z_vals.remove(z_start) else: - uwkj_sur.add(seg) + uwkj_sur.append(seg) for i in range(seg.z_start, seg.z_end + 1): z_vals.remove(i) return uwkj_sur - def calculate_external_paths_stretch(self) -> dict[UWKJ, set[ZSE]]: - """ - Returns {uwkj: set of connected segments (z_start, z_end)} + def calculate_external_paths_stretch(self) -> dict[UWKJ, list[ZSE]]: + """Calculates the maximal connected z-segments of external paths (expressed as :class:`UWKJ`) of ``G``. + + Returns: + dict[UWKJ, list[ZSE]]: A dictionary in the form of {uwkj: list of maximal connected z-segments (z_start, z_end)}, where + - keys correspond to external paths (expressed as :class:`UWKJ`) of ``G``, + - values correspond to list of maximal connected z-segments (z_start, z_end) of uwkj. """ uwkj_vals = [ UWKJ(u=u, w=w, k=k, j=j) From 241667a401728fce80a3a6f0f4c047567a7d9084 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 13:36:12 -0700 Subject: [PATCH 059/127] Add, correct documentation --- minorminer/utils/zephyr/qfloor.py | 76 +++++++++++++++++++------------ 1 file changed, 48 insertions(+), 28 deletions(-) diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 2dfe6fa5..1040b88b 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -71,46 +71,48 @@ def __init__( @property def zns(self) -> list[ZNode]: - """Returns the sorted list of ZNodes the tile contains""" + """Returns the sorted list of :class:`ZNode`s the tile contains.""" return self._zns @cached_property def index_zns(self) -> dict[int, ZNode]: + """Returns a dictionary mapping the index of a node in the tile to its corresponding :class:`ZNode`.""" return {i: zn for i, zn in enumerate(self._zns)} @property def seed(self) -> ZNode: - """Returns the smallest ZNode in Zns.""" + """Returns the smallest :class:`ZNode` in zns.""" return self._zns[0] @cached_property def shifts(self) -> list[PlaneShift]: - """Returns the list of shift of each ZNode in zns from seed""" + """Returns a list of :class:`PlaneShift` values, one for each :class:`ZNode` in :py:attr:`self.zns`, measured relative to the :py:attr:`self.seed`.""" seed = self.seed return [zn - seed for zn in self._zns] @cached_property def index_shifts(self) -> dict[int, PlaneShift]: + """Returns a dictionary mapping the index of a node in the tile to its corresponding :class:`PlaneShift` in the tile.""" return {i: ps for i, ps in enumerate(self.shifts)} @cached_property def shape(self) -> ZShape: - """Returns the shape of the Zephyr graph the tile belongs to.""" + """Returns the :class:`ZShape` of the Zephyr graph the tile belongs to.""" return self.seed.shape @cached_property def convert_to_z(self) -> bool: - """Returns the convert_to_z attribute of the ZNodes of the tile""" + """Returns the convert_to_z attribute of the ``ZNode``s of the tile.""" return self.seed.convert_to_z @cached_property def ver_zns(self) -> list[ZNode]: - """Returns the list of vertical ZNodes of the tile""" + """Returns the list of vertical ``ZNode``s of the tile.""" return [zn for zn in self._zns if zn.is_vertical()] @cached_property def hor_zns(self) -> list[ZNode]: - """Returns the list of horizontal ZNodes of the tile""" + """Returns the list of horizontal ``ZNode``s of the tile.""" return [zn for zn in self._zns if zn.is_horizontal()] def edges( @@ -118,20 +120,19 @@ def edges( nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> list[ZEdge]: - """Returns the list of edges of the graph induced on the tile, when restricted by `nbr_kind` and `where`. + """Returns the list of edges of the graph induced on the tile, when restricted by ``nbr_kind`` and ``where``. Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts returned edges to those having the given edge kind(s). If None, no filtering is applied. Defaults to None. where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + A coordinate filter. Applies to ``ccoord`` if ``self.convert_to_z`` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - list[ZEdge]: List of edges of the graph induced on the tile, when restricted by `nbr_kind` and `where`. + list[ZEdge]: List of edges of the graph induced on the tile, when restricted by ``nbr_kind`` and ``where``. """ - zns = self._zns tile_coords = [zn.zcoord for zn in zns] if self.convert_to_z else [zn.ccoord for zn in zns] where_tile = lambda coord: where(coord) and coord in tile_coords @@ -167,7 +168,7 @@ def __str__(self) -> str: class QuoFloor: - """Initializes QuoFloor object with a corner tile and dimension and + """Initializes 'QuoFloor' object with a corner tile and dimension and optional tile connector. Args: @@ -209,12 +210,12 @@ def __init__( @property def tile_connector(self) -> dict[tuple[int], PlaneShift] | None: - """Returns the tile connector""" + """Returns the tile connector.""" return self.tile_connector @tile_connector.setter def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: - """Sets the tile connector""" + """Sets the tile connector.""" if not isinstance(new_connector, dict): raise TypeError(f"Expected tile_connector to be dict, got {type(new_connector)}") if not new_connector in self.IMPLEMENTED_CONNECTORS: @@ -228,14 +229,22 @@ def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: ) self._tile_connector = new_connector - def check_dim_corner_qtile_compatibility( + def _check_dim_corner_qtile_compatibility( self, dim: Dim_2D, corner_qtile: QuoTile, ) -> None: - """Checks whether dimension and corner tile are compatible, i.e. - whether given the tile connector, the floor can be + """Checks whether dimension of floor and corner tile are compatible. + + Checks whether given the tile connector, the floor can be constructed with the provided corner tile and dimensions. + + Args: + dim (Dim_2D): The dimension of the floor + corner_qtile (QuoTile): The upper left corner tile of the floor. + + Raises: + ValueError: If the floor cannot be constructed with the provided corner tile and dimensions. """ if any(par is None for par in (dim, corner_qtile, self._tile_connector)): return @@ -249,12 +258,12 @@ def check_dim_corner_qtile_compatibility( @property def dim(self) -> Dim_2D: - """Returns dimension of the floor""" + """Returns dimension of the floor.""" return self._dim @dim.setter def dim(self, new_dim: Dim_2D | tuple[int]) -> None: - """Sets dimension of the floor""" + """Sets dimension of the floor.""" if isinstance(new_dim, tuple): new_dim = Dim_2D(*new_dim) if not isinstance(new_dim, Dim_2D): @@ -264,17 +273,17 @@ def dim(self, new_dim: Dim_2D | tuple[int]) -> None: if any(x <= 0 for x in new_dim): raise ValueError(f"Expected elements of Dim to be positive integers, got {Dim_2D}") if hasattr(self, "_corner_qtile"): - self.check_dim_corner_qtile_compatibility(dim=new_dim, corner_qtile=self._corner_qtile) + self._check_dim_corner_qtile_compatibility(dim=new_dim, corner_qtile=self._corner_qtile) self._dim = new_dim @property def corner_qtile(self) -> QuoTile: - """Returns the corner tile of floor""" + """Returns the corner tile of floor.""" return self._corner_qtile @corner_qtile.setter def corner_qtile(self, new_qtile: QuoTile) -> None: - """Sets corner tile of the floor""" + """Sets corner tile of the floor.""" if isinstance(new_qtile, Iterable): new_qtile = QuoTile(zns=new_qtile) if not isinstance(new_qtile, QuoTile): @@ -297,11 +306,22 @@ def corner_qtile(self, new_qtile: QuoTile) -> None: if abs(ydiff) >= x_jump: raise ValueError(f"This tile may overlap other tiles on {x = }") if hasattr(self, "_dim"): - self.check_dim_corner_qtile_compatibility(dim=self._dim, corner_qtile=new_qtile) + self._check_dim_corner_qtile_compatibility(dim=self._dim, corner_qtile=new_qtile) self._corner_qtile = new_qtile def qtile_xy(self, x: int, y: int) -> QuoTile: - """Returns the tile at the position x, y of floor, i.e. column x, row y.""" + """Returns the :class:`QuoTile` located at column ``x`` and row ``y`` of the floor. + + Args: + x (int): The column number. + y (int): The row number. + + Raises: + ValueError: If the floor does not have a tile at position (x, y). + + Returns: + QuoTile: The tile at the position (x, y) of the floor. + """ if (x, y) not in product(range(self._dim.Lx), range(self._dim.Ly)): raise ValueError( f"Expected x to be in {range(self._dim.Lx)} and y to be in {range(self._dim.Ly)}. " @@ -327,21 +347,21 @@ def qtiles(self) -> dict[tuple[int], QuoTile]: @property def zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, - and the values are the ZNodes the tile corresponding to the position + and the values are the ``ZNode``s the tile corresponding to the position contains.""" return {xy: xy_tile.zns for xy, xy_tile in self.qtiles.items()} @property def ver_zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, - and the values are the vertical ZNodes the tile corresponding to + and the values are the vertical ``ZNode``s the tile corresponding to the position contains.""" return {xy: xy_tile.ver_zns for xy, xy_tile in self.qtiles.items()} @property def hor_zns(self) -> dict[tuple[int], list[ZNode]]: """Returns the dictionary where the keys are positions of floor, - and the values are the horizontal ZNodes the tile corresponding to + and the values are the horizontal ``ZNode``s the tile corresponding to the position contains.""" return {xy: xy_tile.hor_zns for xy, xy_tile in self.qtiles.items()} From 1a8b937d5dce9308e495fadcafb3184c96bf7879 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 13:36:38 -0700 Subject: [PATCH 060/127] Correct documentation --- minorminer/utils/zephyr/node_edge.py | 29 ++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 88cefa89..081d4eb7 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -125,6 +125,7 @@ def __init__( @property def edge_kind(self) -> EdgeKind: + """Returns the :class:`EdgeKind` of ``self``.""" return self._edge_kind @@ -191,12 +192,12 @@ def __init__( @property def shape(self) -> ZShape | None: - """Returns the shape of the Zephyr graph the node belongs to.""" + """Returns the :class:`ZShape` of the Zephyr graph the node belongs to.""" return self._shape @shape.setter def shape(self, new_shape: ZShape | tuple[int | None]) -> None: - """Sets a new value for shape""" + """Sets a new value for shape.""" if isinstance(new_shape, tuple): new_shape = ZShape(*new_shape) @@ -219,12 +220,16 @@ def shape(self, new_shape: ZShape | tuple[int | None]) -> None: @property def ccoord(self) -> CartesianCoord: - """Returns the CartesianCoord of self, ccoord""" + """Returns the :class:`CartesianCoord` of ``self``.""" return self._ccoord @ccoord.setter def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): - """Sets a new value for ccoord""" + """Sets a new value for the ``ccoord`` attribute. + + Args: + new_ccoord (CartesianCoord or tuple[int]): The new Cartesian coordinate to set. + """ if isinstance(new_ccoord, tuple): new_ccoord = CartesianCoord(*new_ccoord) if not isinstance(new_ccoord, CartesianCoord): @@ -291,27 +296,27 @@ def tuple_to_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: @property def zcoord(self) -> ZephyrCoord: - """Returns ZephyrCoordinate corresponding to ccoord""" + """Returns :class:`ZephyrCoord` corresponding to ccoord.""" return cartesian_to_zephyr(self._ccoord) @property def node_kind(self) -> NodeKind: - """Returns the node kind of self""" + """Returns the :class:`NodeKind` of ``self``.""" if self._ccoord.x % 2 == 0: return NodeKind.VERTICAL return NodeKind.HORIZONTAL @property def direction(self) -> int: - """Returns the direction of node, i.e. its `u` coordinate in Zephyr coordinates.""" + """Returns the direction of node, i.e. its ``u`` coordinate in Zephyr coordinates.""" return self.node_kind.value def is_quo(self) -> bool: - """Decides if the ZNode object is quotient""" + """Decides if the class:`ZNode` object is quotient.""" return (self._ccoord.k is None) and (self._shape.t is None) def to_quo(self) -> ZNode: - """Returns the quotient ZNode corresponding to self""" + """Returns the quotient class:`ZNode` corresponding to self""" qshape = ZShape(m=self._shape.m) qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) @@ -665,7 +670,7 @@ def incident_edges( nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> list[ZEdge]: - """Returns incident edges with self when restricted by `nbr_kind` and `where`. + """Returns incident edges with self when restricted by ``nbr_kind`` and ``where``. Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): @@ -676,7 +681,7 @@ def incident_edges( or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. Returns: - list[ZEdge]: list edges incident with self when restricted by `nbr_kind` and `where`. + list[ZEdge]: List of edges incident with self when restricted by ``nbr_kind`` and ``where``. """ return [ZEdge(self, v) for v in self.neighbors(nbr_kind=nbr_kind, where=where)] @@ -685,7 +690,7 @@ def degree( nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> int: - """Returns degree of self when restricted by `nbr_kind` and `where`. + """Returns degree of self when restricted by ``nbr_kind`` and ``where``. Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): From 7cd6fbc17a274e2267ca9096bb1357c82d67df1b Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 13:37:10 -0700 Subject: [PATCH 061/127] Correct documentation --- minorminer/utils/zephyr/plane_shift.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index fec8ec2f..b28922c5 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -72,7 +72,7 @@ def __mul__(self, scale: int) -> PlaneShift: scale (int): The scale for left-multiplying self with. Returns: - PlaneShift: The result of left-multiplying self by scale. + PlaneShift: The result of left-multiplying self by ``scale``. """ new_shift_x = scale * self._shift.x @@ -80,17 +80,17 @@ def __mul__(self, scale: int) -> PlaneShift: return PlaneShift(new_shift_x, new_shift_y) def __rmul__(self, scale: int | float) -> PlaneShift: - """Multiplies the self from right by the number value ``scale``. + """Multiplies the ``self`` from right by the number value ``scale``. Args: - scale (int | float): The scale for right-multiplying self with. + scale (int | float): The scale for right-multiplying ``self`` with. Raises: TypeError: If scale is not 'int' or 'float'. ValueError: If the resulting PlaneShift has non-whole values. Returns: - PlaneShift: The result of right-multiplying self by scale. + PlaneShift: The result of right-multiplying ``self`` by ``scale``. """ return self * scale From b47c87600716f50401304390553eb32f0383ea8f Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Wed, 30 Apr 2025 14:13:13 -0700 Subject: [PATCH 062/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 081d4eb7..6ee2472f 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -781,7 +781,7 @@ def __sub__( raise ValueError(f"{other} cannot be subtracted from {self}") from e def __hash__(self) -> int: - return (self._ccoord, self._shape).__hash__() + return hash((self._ccoord, self._shape)) def __repr__(self) -> str: if self.convert_to_z: From cc2e009a0484d628a96e0ef32748dbbb4c286fbb Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 14:22:18 -0700 Subject: [PATCH 063/127] Correct documentation --- minorminer/utils/zephyr/node_edge.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 6ee2472f..72607d6d 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -380,13 +380,11 @@ def internal_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of internal neighbors of self when restricted by `where`. - + """Generator of internal neighbors of self when restricted by ``where``. Args: where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. - + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always Yields: ZNode: Internal neighbors of self when restricted by `where`. """ @@ -413,13 +411,11 @@ def external_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of external neighbors of self when restricted by `where`. - + """Generator of external neighbors of self when restricted by ``where``. Args: where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. - + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always Yields: ZNode: External neighbors of self when restricted by `where`. """ @@ -448,13 +444,11 @@ def odd_neighbors_generator( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of odd neighbors of self when restricted by `where`. - + """Generator of odd neighbors of self when restricted by ``where``. Args: where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. - + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always Yields: ZNode: Odd neighbors of self when restricted by `where`. """ From a56aa8453ea0915c9c4b824e5606939d762a3561 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 16:07:34 -0700 Subject: [PATCH 064/127] Disable shape.setter, coord.setter + Separate checks for ccoord + Modify internal, external, odd neighbors generators+ Edit the docstrings --- minorminer/utils/zephyr/node_edge.py | 317 ++++++++++++++------------- 1 file changed, 161 insertions(+), 156 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 72607d6d..d84f1f80 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -167,13 +167,13 @@ class ZNode: def __init__( self, coord: CartesianCoord | ZephyrCoord | tuple[int], - shape: ZShape | tuple[int | None] | None = None, + shape: ZShape | None = None, convert_to_z: bool | None = None, ) -> None: if shape: - self.shape = ZShape(*shape) + self._shape = self._set_shape(shape) else: - self.shape = ZShape() + self._shape = ZShape() if convert_to_z is None: self.convert_to_z = len(coord) in (4, 5) @@ -188,81 +188,77 @@ def __init__( if isinstance(coord, ZephyrCoord): coord = zephyr_to_cartesian(coord) - self.ccoord = coord + self._ccoord = self._set_ccoord(coord) @property - def shape(self) -> ZShape | None: + def shape(self) -> ZShape: """Returns the :class:`ZShape` of the Zephyr graph the node belongs to.""" return self._shape - @shape.setter - def shape(self, new_shape: ZShape | tuple[int | None]) -> None: - """Sets a new value for shape.""" - if isinstance(new_shape, tuple): - new_shape = ZShape(*new_shape) - - if not isinstance(new_shape, ZShape): - raise TypeError( - f"Expected shape to be tuple[int | None] or ZShape or None, got {type(new_shape)}" - ) - - if hasattr(self, "_ccoord"): - if (self._ccoord.k is None) != (new_shape.t is None): - raise ValueError( - f"ccoord, shape must be both quotient or non-quotient, got {self._ccoord, new_shape}" - ) + def _set_shape(self, shape: ZShape | tuple[int | None]) -> ZShape: + """Returns the :class:`Zshape` corresponding to ``shape``. + + Args: + shape (ZShape | tuple[int | None]): The shape to set. + """ + if isinstance(shape, tuple): + shape = ZShape(*shape) - for var, val in new_shape._asdict().items(): + for var, val in shape._asdict().items(): if (val is not None) and (not isinstance(val, int)): raise TypeError(f"Expected {var} to be None or 'int', got {type(val)}") - - self._shape = new_shape + return shape @property def ccoord(self) -> CartesianCoord: """Returns the :class:`CartesianCoord` of ``self``.""" return self._ccoord - @ccoord.setter - def ccoord(self, new_ccoord: CartesianCoord | tuple[int]): - """Sets a new value for the ``ccoord`` attribute. - - Args: - new_ccoord (CartesianCoord or tuple[int]): The new Cartesian coordinate to set. - """ - if isinstance(new_ccoord, tuple): - new_ccoord = CartesianCoord(*new_ccoord) - if not isinstance(new_ccoord, CartesianCoord): - raise TypeError( - f"Expected ccoord to be CartesianCoord or tuple[int], got {type(new_ccoord)}" - ) - for c in new_ccoord: - if not isinstance(c, int): - raise TypeError(f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}") + def _check_ccoord_val(self, coord: CartesianCoord): + for c in coord: if c < 0: raise ValueError(f"Expected ccoord.x and ccoord.y to be non-negative, got {c}") - if new_ccoord.x % 2 == new_ccoord.y % 2: + + if coord.x % 2 == coord.y % 2: + raise ValueError( + f"Expected ccoord.x and ccoord.y to differ in parity, got {coord.x, coord.y}" + ) + + def _check_ccoord_shape(self, coord: CartesianCoord) -> None: + """Check value of ``coord`` is consistent with ``t``.""" + + # k value of coord is consistent with t + if (self._shape.t is None) != (coord.k is None): raise ValueError( - f"Expected ccoord.x and ccoord.y to differ in parity, got {new_ccoord.x, new_ccoord.y}" + f"shape and ccoord must be both quotient or non-quotient, got {self._shape}, {coord}" ) - # check k value of CartesianCoord is consistent with t - if hasattr(self, "_shape"): - if (self._shape.t is None) != (new_ccoord.k is None): + if (self._shape.t is not None) and (coord.k not in range(self._shape.t)): + raise ValueError(f"Expected k to be in {range(self._shape.t)}, got {coord.k}") + + # check x, y value of coord is consistent with m + if self._shape.m is not None: + if not all(val in range(4 * self._shape.m + 1) for val in (coord.x, coord.y)): raise ValueError( - f"shape and ccoord must be both quotient or non-quotient, got {self._shape}, {new_ccoord}" + f"Expected ccoord.x and ccoord.y to be in {range(4*self._shape.m+1)}, got {coord.x, coord.y}" ) - if (self._shape.t is not None) and (new_ccoord.k not in range(self._shape.t)): - raise ValueError(f"Expected k to be in {range(self._shape.t)}, got {new_ccoord.k}") - - # check x, y value of CartesianCoord is consistent with m - if self._shape.m is not None: - if all(val in range(4 * self._shape.m + 1) for val in (new_ccoord.x, new_ccoord.y)): - self._ccoord = new_ccoord - else: - raise ValueError( - f"Expected ccoord.x and ccoord.y to be in {range(4*self._shape.m+1)}, got {new_ccoord.x, new_ccoord.y}" - ) - self._ccoord = new_ccoord + + def _set_ccoord(self, coord: CartesianCoord | tuple[int]) -> CartesianCoord: + """Returns the :class:`CartesianCoord` corresponding to ``coord``. + + Args: + coord (CartesianCoord or tuple[int]): The Cartesian coordinate to set. + """ + if isinstance(coord, tuple): + coord = CartesianCoord(*coord) + + for c in coord: + if not isinstance(c, int): + raise TypeError(f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}") + + self._check_ccoord_val(coord) + self._check_ccoord_shape(coord) + + return coord @staticmethod def tuple_to_coord(coord: tuple[int]) -> CartesianCoord | ZephyrCoord: @@ -312,11 +308,11 @@ def direction(self) -> int: return self.node_kind.value def is_quo(self) -> bool: - """Decides if the class:`ZNode` object is quotient.""" + """Decides if the :class:`ZNode` object is quotient.""" return (self._ccoord.k is None) and (self._shape.t is None) def to_quo(self) -> ZNode: - """Returns the quotient class:`ZNode` corresponding to self""" + """Returns the quotient :class:`ZNode` corresponding to self""" qshape = ZShape(m=self._shape.m) qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) @@ -382,30 +378,33 @@ def internal_neighbors_generator( ) -> Generator[ZNode]: """Generator of internal neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + where (Callable[[CartesianCoord | ZephyrCoord], bool]): A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, - or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Yields: - ZNode: Internal neighbors of self when restricted by `where`. + ZNode: Internal neighbors of self when restricted by ``where``. """ x, y, _ = self._ccoord convert = self.convert_to_z k_vals = [None] if self._shape.t is None else range(self._shape.t) for i, j, k in product((-1, 1), (-1, 1), k_vals): ccoord = CartesianCoord(x=x + i, y=y + j, k=k) + + # Check ccoord is valid. If not valid, ignore this ccoord + try: + self._check_ccoord_val(ccoord) + except ValueError: + continue + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) if not where(coord): continue - try: - yield ZNode( - coord=ccoord, - shape=self._shape, - convert_to_z=convert, - ) - except GeneratorExit: - raise - except (TypeError, ValueError): - pass + + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) def external_neighbors_generator( self, @@ -413,11 +412,11 @@ def external_neighbors_generator( ) -> Generator[ZNode]: """Generator of external neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + where (Callable[[CartesianCoord | ZephyrCoord], bool]): A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, - or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Yields: - ZNode: External neighbors of self when restricted by `where`. + ZNode: External neighbors of self when restricted by ``where``. """ x, y, k = self._ccoord convert = self.convert_to_z @@ -426,19 +425,22 @@ def external_neighbors_generator( new_x = x + s if changing_index == 0 else x new_y = y + s if changing_index == 1 else y ccoord = CartesianCoord(x=new_x, y=new_y, k=k) + + # Check ccoord is valid. If not valid, ignore this ccoord + try: + self._check_ccoord_val(ccoord) + except ValueError: + continue + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) if not where(coord): continue - try: - yield ZNode( - coord=ccoord, - shape=self._shape, - convert_to_z=convert, - ) - except GeneratorExit: - raise - except (TypeError, ValueError): - pass + + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) def odd_neighbors_generator( self, @@ -446,11 +448,11 @@ def odd_neighbors_generator( ) -> Generator[ZNode]: """Generator of odd neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + where (Callable[[CartesianCoord | ZephyrCoord], bool]): A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, - or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Yields: - ZNode: Odd neighbors of self when restricted by `where`. + ZNode: Odd neighbors of self when restricted by ``where``. """ x, y, k = self._ccoord convert = self.convert_to_z @@ -459,37 +461,40 @@ def odd_neighbors_generator( new_x = x + s if changing_index == 0 else x new_y = y + s if changing_index == 1 else y ccoord = CartesianCoord(x=new_x, y=new_y, k=k) + + # Check ccoord is valid. If not valid, ignore this ccoord + try: + self._check_ccoord_val(ccoord) + except ValueError: + continue + coord = ccoord if not convert else cartesian_to_zephyr(ccoord) if not where(coord): continue - try: - yield ZNode( - coord=ccoord, - shape=self._shape, - convert_to_z=convert, - ) - except GeneratorExit: - raise - except (TypeError, ValueError): - pass + + yield ZNode( + coord=ccoord, + shape=self._shape, + convert_to_z=convert, + ) def neighbors_generator( self, nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> Generator[ZNode]: - """Generator of neighbors of self when restricted by `nbr_kind` and `where`. + """Generator of neighbors of self when restricted by ``nbr_kind`` and ``where``. Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts yielded neighbors to those connected by the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + If ``None``, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Yields: - ZNode: Neighbors of self when restricted by `nbr_kind` and `where`. + ZNode: Neighbors of self when restricted by ``nbr_kind`` and ``where``. """ if nbr_kind is None: kinds = {kind for kind in EdgeKind} @@ -512,17 +517,17 @@ def neighbors( nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns set of neighbors of self when restricted by `nbr_kind` and `where`. + """Returns set of neighbors of self when restricted by ``nbr_kind`` and ``where``. Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts returned neighbors to those connected by the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + If ``None``, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - set[ZNode]: Set of neighbors of self when restricted by `nbr_kind` and `where`. + set[ZNode]: Set of neighbors of self when restricted by ``nbr_kind`` and ``where``. """ return set(self.neighbors_generator(nbr_kind=nbr_kind, where=where)) @@ -530,14 +535,14 @@ def internal_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns the set of internal neighbors of self when restricted by `where`. + """Returns the set of internal neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - set[ZNode]: Set of internal neighbors of self when restricted by `where`. + set[ZNode]: Set of internal neighbors of self when restricted by ``where``. """ return set(self.neighbors_generator(nbr_kind=EdgeKind.INTERNAL, where=where)) @@ -545,14 +550,14 @@ def external_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns the set of external neighbors of self when restricted by `where`. + """Returns the set of external neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - set[ZNode]: Set of external neighbors of self when restricted by `where`. + set[ZNode]: Set of external neighbors of self when restricted by ``where``. """ return set(self.neighbors_generator(nbr_kind=EdgeKind.EXTERNAL, where=where)) @@ -560,14 +565,14 @@ def odd_neighbors( self, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> set[ZNode]: - """Returns the set of odd neighbors of self when restricted by `where`. + """Returns the set of odd neighbors of self when restricted by ``where``. Args: - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - set[ZNode]: Set of odd neighbors of self when restricted by `where`. + set[ZNode]: Set of odd neighbors of self when restricted by ``where``. """ return set(self.neighbors_generator(nbr_kind=EdgeKind.ODD, where=where)) @@ -576,16 +581,16 @@ def is_internal_neighbor( other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Returns whether other is an internal neighbor of self when restricted by `where`. + """Returns whether other is an internal neighbor of self when restricted by ``where``. Args: other (ZNode): Another instance to compare with self. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - bool: Whether other is an internal neighbor of self when restricted by `where`. + bool: Whether other is an internal neighbor of self when restricted by ``where``. """ for nbr in self.internal_neighbors_generator(where=where): if other == nbr: @@ -597,16 +602,16 @@ def is_external_neighbor( other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Returns whether other is an external neighbor of self when restricted by `where`. + """Returns whether other is an external neighbor of self when restricted by ``where``. Args: other (ZNode): Another instance to compare with self. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - bool: Whether other is an external neighbor of self when restricted by `where`. + bool: Whether other is an external neighbor of self when restricted by ``where``. """ for nbr in self.external_neighbors_generator(where=where): if other == nbr: @@ -618,16 +623,16 @@ def is_odd_neighbor( other: ZNode, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Returns whether other is an odd neighbor of self when restricted by `where`. + """Returns whether other is an odd neighbor of self when restricted by ``where``. Args: other (ZNode): Another instance to compare with self. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - bool: Whether other is an odd neighbor of self when restricted by `where`. + bool: Whether other is an odd neighbor of self when restricted by ``where``. """ for nbr in self.odd_neighbors_generator(where=where): if other == nbr: @@ -640,19 +645,19 @@ def is_neighbor( nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, ) -> bool: - """Returns whether other is a neighbor of self when restricted by `nbr_kind` and `where`. + """Returns whether other is a neighbor of self when restricted by ``nbr_kind`` and ``where``. Args: other (ZNode): Another instance to compare with self. nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts neighbors to those connected by the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + If ``None``, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - bool: Whether other is a neighbor of self when restricted by `nbr_kind` and `where`. + bool: Whether other is a neighbor of self when restricted by ``nbr_kind`` and ``where``. """ for nbr in self.neighbors_generator(nbr_kind=nbr_kind, where=where): if other == nbr: @@ -669,10 +674,10 @@ def incident_edges( Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts returned edges to those having the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + If ``None``, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: list[ZEdge]: List of edges incident with self when restricted by ``nbr_kind`` and ``where``. @@ -689,13 +694,13 @@ def degree( Args: nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts counting the neighbors to those connected by the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): - A coordinate filter. Applies to `ccoord` if `self.convert_to_z` is False, - or to `zcoord` if `self.convert_to_z` is True. Defaults to `lambda coord: True`. + If ``None``, no filtering is applied. Defaults to None. + where (Callable[[CartesianCoord | ZephyrCoord], bool]): + A coordinate filter. Applies to ``ccoord`` if :py:attr:`self.convert_to_z` is ``False``, + or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. Returns: - int: degree of self when restricted by `nbr_kind` and `where`. + int: degree of self when restricted by ``nbr_kind`` and ``where``. """ return len(self.neighbors(nbr_kind=nbr_kind, where=where)) From 8d6c9743daaa52f0a6c39d2d1bb64ef6c45dbd84 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 30 Apr 2025 16:35:26 -0700 Subject: [PATCH 065/127] Edit docstrings --- minorminer/utils/zephyr/coordinate_systems.py | 12 ++++++------ minorminer/utils/zephyr/node_edge.py | 18 +++++++++--------- minorminer/utils/zephyr/qfloor.py | 2 +- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index 2acfdf23..ccf84965 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -28,15 +28,15 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: - """Converts a ``CartesianCoord`` to its corresponding ``ZephyrCoord``. + """Converts a :class:`CartesianCoord` to its corresponding :class:`ZephyrCoord`. - Note: It assumes the given ``CartesianCoord`` is valid. + Note: It assumes the given :class:`CartesianCoord` is valid. Args: ccoord (CartesianCoord): The coodinate in Cartesian system to be converted. Returns: - ZephyrCoord: The coordinate of the ccoord in Zephyr system. + ZephyrCoord: The coordinate of the ``ccoord`` in Zephyr system. """ x, y, k = ccoord if x % 2 == 0: @@ -53,15 +53,15 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: def zephyr_to_cartesian(zcoord: ZephyrCoord) -> CartesianCoord: - """Converts a ``ZephyrCoord``` to its corresponding ``CartesianCoord``. + """Converts a :class:`ZephyrCoord` to its corresponding :class:`CartesianCoord`. - Note: It assumes the given ZephyrCoord is valid. + Note: It assumes the given ``zcoord`` is a valid Zephyr coordinate. Args: zcoord (ZephyrCoord): The coodinate in Zephyr system to be converted. Returns: - CartesianCoord: The coordinate of the ccoord in Cartesian system. + CartesianCoord: The coordinate of the ``ccoord`` in Cartesian system. """ u, w, k, j, z = zcoord if u == 0: diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index d84f1f80..6658ebd1 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -38,8 +38,8 @@ class EdgeKind(Enum): class NodeKind(Enum): """Kinds of a node (qubit) in a Zephyr graph.""" - VERTICAL = 0 # The `u` coordinate of a Zephyr coordinate of a vertical node (qubit) is zero. - HORIZONTAL = 1 # The `u` coordinate of a Zephyr coordinate of a horizontal node (qubit) is one. + VERTICAL = 0 # The ``u`` coordinate of a Zephyr coordinate of a vertical node (qubit) is zero. + HORIZONTAL = 1 # The ``u`` coordinate of a Zephyr coordinate of a horizontal node (qubit) is one. class Edge: @@ -135,15 +135,15 @@ class ZNode: Args: coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian. shape (ZShape | tuple[int | None] | None, optional): Shape of the Zephyr graph containing this ZNode. - If a ZShape is passed, it should be a namedtuple with fields `m` (grid size of the Zephyr graph) - and `t` (tile size of the Zephyr graph). Defaults to None. + If a ZShape is passed, it should be a namedtuple with fields ``m`` (grid size of the Zephyr graph) + and ``t`` (tile size of the Zephyr graph). Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. Defaults to None. Note: - If the `k` field of the given `coord` (whether a `CartesianCoord` or `ZephyrCoord`) is not `None`, - then `shape` must be provided and its `t` field (the tile size of the Zephyr graph) must not be `None`. - Otherwise, a `ValueError` will be raised. + If the ``k`` field of the given ``coord`` (whether a :class:`CartesianCoord` or :class:`ZephyrCoord`) is not ``None``, + then ``shape`` must be provided and its ``t`` field (the tile size of the Zephyr graph) must not be ``None``. + Otherwise, a ``ValueError`` will be raised. Example: >>> from zephyr_utils.node_edge import ZNode, ZShape @@ -318,11 +318,11 @@ def to_quo(self) -> ZNode: return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) def is_vertical(self) -> bool: - """Returns True if the node represents a vertical qubit (i.e., its `u` coordinate in Zephyr coordinates is 0).""" + """Returns True if the node represents a vertical qubit (i.e., its ``u`` coordinate in Zephyr coordinates is 0).""" return self.node_kind is NodeKind.VERTICAL def is_horizontal(self) -> bool: - """Returns True if the node represents a horizontal qubit (i.e., its `u` coordinate in Zephyr coordinates is 1).""" + """Returns True if the node represents a horizontal qubit (i.e., its ``u`` coordinate in Zephyr coordinates is 1).""" return self.node_kind is NodeKind.HORIZONTAL def neighbor_kind( diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py index 1040b88b..e5fcf9b1 100644 --- a/minorminer/utils/zephyr/qfloor.py +++ b/minorminer/utils/zephyr/qfloor.py @@ -126,7 +126,7 @@ def edges( nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): Edge kind filter. Restricts returned edges to those having the given edge kind(s). If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool], optional): + where (Callable[[CartesianCoord | ZephyrCoord], bool]): A coordinate filter. Applies to ``ccoord`` if ``self.convert_to_z`` is ``False``, or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. From b8a35650c58436ef4cbfac7f8c85be4d27db4641 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Thu, 30 Oct 2025 12:35:12 -0700 Subject: [PATCH 066/127] Fix bug --- minorminer/utils/zephyr/node_edge.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 6658ebd1..68ccafb8 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -126,7 +126,16 @@ def __init__( @property def edge_kind(self) -> EdgeKind: """Returns the :class:`EdgeKind` of ``self``.""" - return self._edge_kind + if not hasattr(self, "_edge_kind"): + x, y = self._edge + for kind in EdgeKind: + if x.is_neighbor(y, nbr_kind=kind): + self._edge_kind = kind + break + try: + return self._edge_kind + except AttributeError: + raise ValueError(f"{self._edge} is not an edge in Zephyr topology") class ZNode: From eeaf537427b9701a3bda17604ebb4bce3a9cf57e Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Thu, 30 Oct 2025 15:10:01 -0700 Subject: [PATCH 067/127] Add check node validity flag --- minorminer/utils/zephyr/node_edge.py | 72 +++++++++++++++------------- minorminer/utils/zephyr/survey.py | 17 ++++--- 2 files changed, 50 insertions(+), 39 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 68ccafb8..0fd25c21 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -25,36 +25,39 @@ from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, cartesian_to_zephyr, zephyr_to_cartesian) from minorminer.utils.zephyr.plane_shift import PlaneShift - ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) + class EdgeKind(Enum): - """Kinds of an edge (coupler) between two nodes in a Zephyr graph.""" INTERNAL = 1 EXTERNAL = 2 ODD = 3 class NodeKind(Enum): - """Kinds of a node (qubit) in a Zephyr graph.""" - VERTICAL = 0 # The ``u`` coordinate of a Zephyr coordinate of a vertical node (qubit) is zero. - HORIZONTAL = 1 # The ``u`` coordinate of a Zephyr coordinate of a horizontal node (qubit) is one. + VERTICAL = 0 + HORIZONTAL = 1 + class Edge: """Initializes an Edge with nodes x, y. Args: - x: One endpoint of edge. - y: Another endpoint of edge. + x : One endpoint of edge. + y : Another endpoint of edge. """ - def __init__(self, x: int , y: int) -> None: + def __init__( + self, + x, + y, + ) -> None: self._edge = self._set_edge(x, y) - def _set_edge(self, x: int, y: int) -> tuple[int, int]: - """Returns ordered tuple corresponding to the set {x, y}.""" + def _set_edge(self, x, y): + """Reutrns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) else: @@ -66,7 +69,7 @@ def __hash__(self): def __getitem__(self, index: int) -> int: return self._edge[index] - def __eq__(self, other: Edge) -> bool: + def __eq__(self, other: Edge): return self._edge == other._edge def __str__(self) -> str: @@ -80,8 +83,8 @@ class ZEdge(Edge): """Initializes a ZEdge with 'ZNode' nodes x, y. Args: - x (ZNode): Endpoint of edge. Must have same shape as ``y``. - y (ZNode): Endpoint of edge. Must have same shape as ``x``. + x (ZNode): One endpoint of edge. + y (ZNode): Another endpoint of edge. check_edge_valid (bool, optional): Flag to whether check the validity of values and types of ``x``, ``y``. Defaults to True. @@ -125,7 +128,6 @@ def __init__( @property def edge_kind(self) -> EdgeKind: - """Returns the :class:`EdgeKind` of ``self``.""" if not hasattr(self, "_edge_kind"): x, y = self._edge for kind in EdgeKind: @@ -138,21 +140,21 @@ def edge_kind(self) -> EdgeKind: raise ValueError(f"{self._edge} is not an edge in Zephyr topology") + class ZNode: """Initializes 'ZNode' with coord and optional shape. Args: - coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian. - shape (ZShape | tuple[int | None] | None, optional): Shape of the Zephyr graph containing this ZNode. - If a ZShape is passed, it should be a namedtuple with fields ``m`` (grid size of the Zephyr graph) - and ``t`` (tile size of the Zephyr graph). Defaults to None. + coord (CartesianCoord | ZephyrCoord | tuple[int]): coordinate in (quotient) Zephyr or (quotient) Cartesian + shape (ZShape | tuple[int | None] | None, optional): shape of Zephyr graph containing ZNode. + m: grid size, t: tile size + Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. - Defaults to None. + Defaults to None. - Note: - If the ``k`` field of the given ``coord`` (whether a :class:`CartesianCoord` or :class:`ZephyrCoord`) is not ``None``, - then ``shape`` must be provided and its ``t`` field (the tile size of the Zephyr graph) must not be ``None``. - Otherwise, a ``ValueError`` will be raised. + Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), + shape = None raises ValueError. In this case the tile size of Zephyr, t, + must be provided. Example: >>> from zephyr_utils.node_edge import ZNode, ZShape @@ -172,12 +174,12 @@ class ZNode: [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] """ - def __init__( self, coord: CartesianCoord | ZephyrCoord | tuple[int], shape: ZShape | None = None, convert_to_z: bool | None = None, + check_node_valid: bool = True, ) -> None: if shape: self._shape = self._set_shape(shape) @@ -197,7 +199,7 @@ def __init__( if isinstance(coord, ZephyrCoord): coord = zephyr_to_cartesian(coord) - self._ccoord = self._set_ccoord(coord) + self._ccoord = self._set_ccoord(coord=coord, check_node_valid=check_node_valid) @property def shape(self) -> ZShape: @@ -225,8 +227,8 @@ def ccoord(self) -> CartesianCoord: def _check_ccoord_val(self, coord: CartesianCoord): for c in coord: - if c < 0: - raise ValueError(f"Expected ccoord.x and ccoord.y to be non-negative, got {c}") + if c is not None and c < 0: + raise ValueError(f"Expected ccoord elements to be None or non-negative, got {c}") if coord.x % 2 == coord.y % 2: raise ValueError( @@ -251,7 +253,7 @@ def _check_ccoord_shape(self, coord: CartesianCoord) -> None: f"Expected ccoord.x and ccoord.y to be in {range(4*self._shape.m+1)}, got {coord.x, coord.y}" ) - def _set_ccoord(self, coord: CartesianCoord | tuple[int]) -> CartesianCoord: + def _set_ccoord(self, coord: CartesianCoord | tuple[int], check_node_valid: bool) -> CartesianCoord: """Returns the :class:`CartesianCoord` corresponding to ``coord``. Args: @@ -261,11 +263,12 @@ def _set_ccoord(self, coord: CartesianCoord | tuple[int]) -> CartesianCoord: coord = CartesianCoord(*coord) for c in coord: - if not isinstance(c, int): - raise TypeError(f"Expected ccoord.x and ccoord.y to be 'int', got {type(c)}") - - self._check_ccoord_val(coord) - self._check_ccoord_shape(coord) + if (c is not None) and (not isinstance(c, int)): + raise TypeError(f"Expected elements of ccoord to be 'int' or None, got {type(c)}") + + if check_node_valid: + self._check_ccoord_val(coord) + self._check_ccoord_shape(coord) return coord @@ -402,6 +405,7 @@ def internal_neighbors_generator( # Check ccoord is valid. If not valid, ignore this ccoord try: self._check_ccoord_val(ccoord) + self._check_ccoord_shape(ccoord) except ValueError: continue @@ -438,6 +442,7 @@ def external_neighbors_generator( # Check ccoord is valid. If not valid, ignore this ccoord try: self._check_ccoord_val(ccoord) + self._check_ccoord_shape(ccoord) except ValueError: continue @@ -474,6 +479,7 @@ def odd_neighbors_generator( # Check ccoord is valid. If not valid, ignore this ccoord try: self._check_ccoord_val(ccoord) + self._check_ccoord_shape(ccoord) except ValueError: continue diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 32e6e736..9812e6ab 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -62,13 +62,13 @@ def __init__( G_nodes = G.nodelist G_edges = G.edgelist self._nodes: set[ZNode] = { - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape) + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape, check_node_valid=False) for v in G_nodes } self._edges: set[ZEdge] = { ZEdge( - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape), - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape), + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape, check_node_valid=False), + ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape, check_node_valid=False), check_edge_valid=False, ) for (u, v) in G_edges @@ -86,7 +86,8 @@ def get_shape_coord(G: nx.Graph | DWaveSampler) -> tuple[ZShape, Literal["int", Returns: tuple[ZShape, Literal["int", "coordinate"]]: - 0-th index indicates the :class:`ZShape` of ``G``. - - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. + - 1-st index is 'int' if the node lables of ``G`` are integers; + it is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. """ def _graph_shape_coord(G: nx.Graph) -> tuple[ZShape, Literal["int", "coordinate"]]: """ @@ -179,7 +180,7 @@ def missing_nodes(self) -> set[ZNode]: Zephyr graph on the same shape. """ parent_nodes = [ - ZNode(coord=ZephyrCoord(*v), shape=self._shape) + ZNode(coord=ZephyrCoord(*v), shape=self._shape, check_node_valid=False) for v in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).nodes() ] return {v for v in parent_nodes if not v in self._nodes} @@ -194,7 +195,11 @@ def missing_edges(self) -> set[ZEdge]: """Returns the ZEdges of the sampler/graph which are missing compared to perfect yield Zephyr graph on the same shape.""" parent_edges = [ - ZEdge(ZNode(coord=u, shape=self.shape), ZNode(coord=v, shape=self.shape)) + ZEdge( + ZNode(coord=u, shape=self.shape, check_node_valid=False), + ZNode(coord=v, shape=self.shape, check_node_valid=False), + check_edge_valid=False + ) for (u, v) in dnx.zephyr_graph( m=self._shape.m, t=self._shape.t, coordinates=True ).edges() From 7f6aa26b0973cc58e0028fbdced76015c1243193 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 31 Oct 2025 12:37:17 -0700 Subject: [PATCH 068/127] Edit examples --- minorminer/utils/zephyr/node_edge.py | 8 ++++---- minorminer/utils/zephyr/survey.py | 19 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 0fd25c21..79cc3014 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -95,12 +95,12 @@ class ZEdge(Edge): Zephyr graph. Example 1: - >>> from zephyr_utils.node_edge import ZNode, ZEdge + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge >>> e = ZEdge(ZNode((3, 2)), ZNode((7, 2))) >>> print(e) ZEdge(ZNode(CartesianCoord(x=3, y=2, k=None)), ZNode(CartesianCoord(x=7, y=2, k=None))) Example 2: - >>> from zephyr_utils.node_edge import ZNode, ZEdge + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZEdge >>> ZEdge(ZNode((2, 3)), ZNode((6, 3))) # raises error, since the two are not neighbors """ @@ -157,7 +157,7 @@ class ZNode: must be provided. Example: - >>> from zephyr_utils.node_edge import ZNode, ZShape + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) >>> zn1.neighbors() [ZNode(CartesianCoord(x=4, y=1, k=None), shape=ZShape(m=5, t=None)), @@ -168,7 +168,7 @@ class ZNode: ZNode(CartesianCoord(x=9, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), ZNode(CartesianCoord(x=7, y=2, k=None), shape=ZShape(m=5, t=None))] - >>> from zephyr_utils.node_edge import ZNode, ZShape + >>> from minorminer.utils.zephyr.node_edge import ZNode, ZShape >>> zn1 = ZNode((5, 2), ZShape(m=5)) >>> zn1.neighbors(nbr_kind=EdgeKind.ODD) [ZNode(CartesianCoord(x=3, y=2, k=None), shape=ZShape(m=5, t=None)), diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py index 9812e6ab..346499a9 100644 --- a/minorminer/utils/zephyr/survey.py +++ b/minorminer/utils/zephyr/survey.py @@ -39,14 +39,17 @@ class ZSurvey: Args: G (nx.Graph | DWaveSampler): A graph or DWaveSampler with Zephyr topology Example: - >>> from dwave.system import DWaveSampler - >>> from minorminer.utils.zephyr.zephyr_survey import Survey - >>> sampler = DWaveSampler(solver="Advantage2_prototype2.6", profile='defaults') - >>> survey = Survey(sampler) - >>> print(f"Number of missing nodes is {survey.num_missing_nodes}") - Number of missing nodes is 33 - >>> print(f"Number of missing edges with both endpoints present is {survey.num_extra_missing_edges}") - Number of missing edges with both endpoints present is 18 + >>> m, t = 3, 2 + >>> G = zephyr_graph(m=m, t=t) + >>> G.remove_nodes_from ([0, 10, 100]) + >>> G.remove_edges_from (list(G.edges())[:20]) + >>> zsur = ZSurvey(G) + >>> print(f"Number of missing nodes is {zsur.num_missing_nodes}") + Number of missing nodes is 3 + >>> print(f"Number of missing edges is {zsur.num_missing_edges}") + Number of missing edges is 46 + >>> print(f"Number of missing edges with both endpoints present is {zsur.num_extra_missing_edges}") + Number of missing edges with both endpoints present is 20 """ def __init__( From e2df11da4708084ce48402c3ecb631021af0189f Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 5 Nov 2025 12:11:51 -0800 Subject: [PATCH 069/127] Remove unused files --- minorminer/utils/zephyr/qfloor.py | 424 ----------------------- minorminer/utils/zephyr/survey.py | 363 ------------------- tests/utils/zephyr/test_qfloor.py | 237 ------------- tests/utils/zephyr/test_zephyr_base.py | 85 ----- tests/utils/zephyr/test_zephyr_survey.py | 88 ----- 5 files changed, 1197 deletions(-) delete mode 100644 minorminer/utils/zephyr/qfloor.py delete mode 100644 minorminer/utils/zephyr/survey.py delete mode 100644 tests/utils/zephyr/test_qfloor.py delete mode 100644 tests/utils/zephyr/test_zephyr_base.py delete mode 100644 tests/utils/zephyr/test_zephyr_survey.py diff --git a/minorminer/utils/zephyr/qfloor.py b/minorminer/utils/zephyr/qfloor.py deleted file mode 100644 index e5fcf9b1..00000000 --- a/minorminer/utils/zephyr/qfloor.py +++ /dev/null @@ -1,424 +0,0 @@ -# Copyright 2025 D-Wave -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ================================================================================================ - - -from __future__ import annotations - -from collections import defaultdict, namedtuple -from itertools import product -from functools import cached_property -from typing import Callable, Iterable, Iterator - -from minorminer.utils.zephyr.coordinate_systems import * -from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape, EdgeKind -from minorminer.utils.zephyr.plane_shift import PlaneShift - -Dim_2D = namedtuple("Dim_2D", ["Lx", "Ly"]) -UWJ = namedtuple("UWJ", ["u", "w", "j"]) -ZSE = namedtuple("ZSE", ["z_start", "z_end"]) - - -class QuoTile: - """Initializes a 'QuoTile' instance from a collection of ``ZNode`` objects. - - Args: - zns (Iterable[ZNode]): The ``ZNode``s the tile contains. - - Example: - .. code-block:: python - >>> from minorminer.utils.zephyr.node_edge import ZNode - >>> from minorminer.utils.zephyr.qfloor import QuoTile - >>> ccoords = [(k, 1+k) for k in range(4)] + [(1+k, k) for k in range(4)] - >>> zns = [ZNode(coord=ccoord) for ccoord in ccoords] - >>> tile = QuoTile(zns) - >>> print(f"{tile.edges() = }\n{tile.seed = }\n{tile.shifts = }") - """ - - def __init__( - self, - zns: Iterable[ZNode], - ) -> None: - if len(zns) == 0: - raise ValueError(f"Expected zns to be non-empty, got {zns}") - for zn in zns: - if not zn.is_quo(): - raise ValueError(f"Expected elements of {zns} to be quotient, got {zn}") - zns_shape = {zn.shape for zn in zns} - if len(zns_shape) != 1: - raise ValueError( - f"Expected all elements of zns to have the same shape, got {zns_shape}" - ) - temp_zns = sorted(list(set(zns))) - seed_convert = temp_zns[0].convert_to_z - zns_result = [] - for zn in temp_zns: - zn.convert_to_z = seed_convert - zns_result.append(zn) - self._zns: list[ZNode] = zns_result - - @property - def zns(self) -> list[ZNode]: - """Returns the sorted list of :class:`ZNode`s the tile contains.""" - return self._zns - - @cached_property - def index_zns(self) -> dict[int, ZNode]: - """Returns a dictionary mapping the index of a node in the tile to its corresponding :class:`ZNode`.""" - return {i: zn for i, zn in enumerate(self._zns)} - - @property - def seed(self) -> ZNode: - """Returns the smallest :class:`ZNode` in zns.""" - return self._zns[0] - - @cached_property - def shifts(self) -> list[PlaneShift]: - """Returns a list of :class:`PlaneShift` values, one for each :class:`ZNode` in :py:attr:`self.zns`, measured relative to the :py:attr:`self.seed`.""" - seed = self.seed - return [zn - seed for zn in self._zns] - - @cached_property - def index_shifts(self) -> dict[int, PlaneShift]: - """Returns a dictionary mapping the index of a node in the tile to its corresponding :class:`PlaneShift` in the tile.""" - return {i: ps for i, ps in enumerate(self.shifts)} - - @cached_property - def shape(self) -> ZShape: - """Returns the :class:`ZShape` of the Zephyr graph the tile belongs to.""" - return self.seed.shape - - @cached_property - def convert_to_z(self) -> bool: - """Returns the convert_to_z attribute of the ``ZNode``s of the tile.""" - return self.seed.convert_to_z - - @cached_property - def ver_zns(self) -> list[ZNode]: - """Returns the list of vertical ``ZNode``s of the tile.""" - return [zn for zn in self._zns if zn.is_vertical()] - - @cached_property - def hor_zns(self) -> list[ZNode]: - """Returns the list of horizontal ``ZNode``s of the tile.""" - return [zn for zn in self._zns if zn.is_horizontal()] - - def edges( - self, - nbr_kind: EdgeKind | Iterable[EdgeKind] | None = None, - where: Callable[[CartesianCoord | ZephyrCoord], bool] = lambda coord: True, - ) -> list[ZEdge]: - """Returns the list of edges of the graph induced on the tile, when restricted by ``nbr_kind`` and ``where``. - - Args: - nbr_kind (EdgeKind | Iterable[EdgeKind] | None, optional): - Edge kind filter. Restricts returned edges to those having the given edge kind(s). - If None, no filtering is applied. Defaults to None. - where (Callable[[CartesianCoord | ZephyrCoord], bool]): - A coordinate filter. Applies to ``ccoord`` if ``self.convert_to_z`` is ``False``, - or to ``zcoord`` if :py:attr:`self.convert_to_z` is ``True``. Defaults to always. - - Returns: - list[ZEdge]: List of edges of the graph induced on the tile, when restricted by ``nbr_kind`` and ``where``. - """ - zns = self._zns - tile_coords = [zn.zcoord for zn in zns] if self.convert_to_z else [zn.ccoord for zn in zns] - where_tile = lambda coord: where(coord) and coord in tile_coords - _edges = { - edge for zn in zns for edge in zn.incident_edges(nbr_kind=nbr_kind, where=where_tile) - } - return list(_edges) - - def __len__(self) -> int: - return len(self._zns) - - def __iter__(self) -> Iterator[ZNode]: - for zn in self._zns: - yield zn - - def __getitem__(self, key) -> ZNode: - return self._zns[key] - - def __hash__(self) -> int: - return hash(self._zns) - - def __eq__(self, other: QuoTile) -> bool: - return self._zns == other._zns - - def __add__(self, shift: PlaneShift) -> QuoTile: - return QuoTile(zns=[zn + shift for zn in self._zns]) - - def __repr__(self) -> str: - return f"{type(self).__name__}{self._zns!r}" - - def __str__(self) -> str: - return f"{type(self).__name__}{self._zns}" - - -class QuoFloor: - """Initializes 'QuoFloor' object with a corner tile and dimension and - optional tile connector. - - Args: - corner_qtile (QuoTile): The tile at the top left corner of desired - subgrid of Quotient Zephyr graph. - dim (Dim | tuple[int]): The dimension of floor, i.e. - the number of columns of floor, the number of rows of floor. - tile_connector (dict[tuple[int], PlaneShift], optional): - Determines how to get the tiles (x+1, y) and (x, y+1) from tile (x, y). - Defaults to tile_connector0. - - Example: - .. code-block:: python - - >>> from minorminer.utils.zephyr.node_edge import ZNode - >>> from minorminer.utils.zephyr.qfloor import QuoFloor - >>> coords = [(k, k+1) for k in range(4)] + [(k+1, k) for k in range(4)] - >>> zns = [ZNode(coord=c) for c in coords] - >>> floor = QuoFloor(corner_qtile=zns, dim=(3, 5)) - >>> print(f"{floor.qtile_xy(2, 3) = }") - floor.qtile_xy(2, 3) = QuoTile[ZNode(CartesianCoord(x=8, y=13, k=None)), ZNode(CartesianCoord(x=9, y=12, k=None)), ZNode(CartesianCoord(x=9, y=14, k=None)), ZNode(CartesianCoord(x=10, y=13, k=None)), ZNode(CartesianCoord(x=10, y=15, k=None)), ZNode(CartesianCoord(x=11, y=14, k=None)), ZNode(CartesianCoord(x=11, y=16, k=None)), ZNode(CartesianCoord(x=12, y=15, k=None))] - - """ - - IMPLEMENTED_CONNECTORS: tuple[dict[tuple[int], PlaneShift]] = ( - {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 4)}, - ) - tile_connector0 = IMPLEMENTED_CONNECTORS[0] - - def __init__( - self, - corner_qtile: QuoTile | Iterable[ZNode], - dim: Dim_2D | tuple[int], - tile_connector: dict[tuple[int], PlaneShift] = tile_connector0, - ) -> None: - self.tile_connector = tile_connector - self.dim = dim - self.corner_qtile = corner_qtile - - @property - def tile_connector(self) -> dict[tuple[int], PlaneShift] | None: - """Returns the tile connector.""" - return self.tile_connector - - @tile_connector.setter - def tile_connector(self, new_connector: dict[tuple[int], PlaneShift]) -> None: - """Sets the tile connector.""" - if not isinstance(new_connector, dict): - raise TypeError(f"Expected tile_connector to be dict, got {type(new_connector)}") - if not new_connector in self.IMPLEMENTED_CONNECTORS: - raise NotImplementedError( - f"{new_connector} not implemented. " - f"Availabale options are {self.IMPLEMENTED_CONNECTORS}" - ) - if any(dir_con not in new_connector.keys() for dir_con in ((1, 0), (0, 1))): - raise ValueError( - f"Expected tile_connector to have (1, 0), (0, 1) as keys, got {new_connector}" - ) - self._tile_connector = new_connector - - def _check_dim_corner_qtile_compatibility( - self, - dim: Dim_2D, - corner_qtile: QuoTile, - ) -> None: - """Checks whether dimension of floor and corner tile are compatible. - - Checks whether given the tile connector, the floor can be - constructed with the provided corner tile and dimensions. - - Args: - dim (Dim_2D): The dimension of the floor - corner_qtile (QuoTile): The upper left corner tile of the floor. - - Raises: - ValueError: If the floor cannot be constructed with the provided corner tile and dimensions. - """ - if any(par is None for par in (dim, corner_qtile, self._tile_connector)): - return - ver_step = self._tile_connector[(0, 1)] - hor_step = self._tile_connector[(1, 0)] - try: - [h + hor_step * (dim.Lx - 1) for h in corner_qtile.hor_zns] - [v + ver_step * (dim.Ly - 1) for v in corner_qtile.ver_zns] - except (ValueError, TypeError): - raise ValueError(f"{dim, corner_qtile} are not compatible") - - @property - def dim(self) -> Dim_2D: - """Returns dimension of the floor.""" - return self._dim - - @dim.setter - def dim(self, new_dim: Dim_2D | tuple[int]) -> None: - """Sets dimension of the floor.""" - if isinstance(new_dim, tuple): - new_dim = Dim_2D(*new_dim) - if not isinstance(new_dim, Dim_2D): - raise TypeError(f"Expected dim to be Dim or tuple[int], got {type(new_dim)}") - if not all(isinstance(x, int) for x in new_dim): - raise TypeError(f"Expected Dim elements to be int, got {new_dim}") - if any(x <= 0 for x in new_dim): - raise ValueError(f"Expected elements of Dim to be positive integers, got {Dim_2D}") - if hasattr(self, "_corner_qtile"): - self._check_dim_corner_qtile_compatibility(dim=new_dim, corner_qtile=self._corner_qtile) - self._dim = new_dim - - @property - def corner_qtile(self) -> QuoTile: - """Returns the corner tile of floor.""" - return self._corner_qtile - - @corner_qtile.setter - def corner_qtile(self, new_qtile: QuoTile) -> None: - """Sets corner tile of the floor.""" - if isinstance(new_qtile, Iterable): - new_qtile = QuoTile(zns=new_qtile) - if not isinstance(new_qtile, QuoTile): - raise TypeError(f"Expected corner_qtile to be QuoTile, got {type(new_qtile)}") - ccoords = [zn.ccoord for zn in new_qtile.zns] - x_values = defaultdict(list) - y_values = defaultdict(list) - for x, y, *_ in ccoords: - x_values[x].append(y) - y_values[y].append(x) - x_diff = {x: max(x_list) - min(x_list) for x, x_list in x_values.items()} - y_diff = {y: max(y_list) - min(y_list) for y, y_list in y_values.items()} - x_jump = self._tile_connector[(1, 0)].x - y_jump = self._tile_connector[(0, 1)].y - - for y, xdiff in y_diff.items(): - if abs(xdiff) >= y_jump: - raise ValueError(f"This tile may overlap other tiles on {y = }") - for x, ydiff in x_diff.items(): - if abs(ydiff) >= x_jump: - raise ValueError(f"This tile may overlap other tiles on {x = }") - if hasattr(self, "_dim"): - self._check_dim_corner_qtile_compatibility(dim=self._dim, corner_qtile=new_qtile) - self._corner_qtile = new_qtile - - def qtile_xy(self, x: int, y: int) -> QuoTile: - """Returns the :class:`QuoTile` located at column ``x`` and row ``y`` of the floor. - - Args: - x (int): The column number. - y (int): The row number. - - Raises: - ValueError: If the floor does not have a tile at position (x, y). - - Returns: - QuoTile: The tile at the position (x, y) of the floor. - """ - if (x, y) not in product(range(self._dim.Lx), range(self._dim.Ly)): - raise ValueError( - f"Expected x to be in {range(self._dim.Lx)} and y to be in {range(self._dim.Ly)}. " - f"Got {x, y}." - ) - xy_shift = x * self._tile_connector[(1, 0)] + y * self._tile_connector[(0, 1)] - return self._corner_qtile + xy_shift - - @property - def qtiles(self) -> dict[tuple[int], QuoTile]: - """Returns the dictionary where the keys are positions of floor, - and the values are the tiles corresponding to the position. - """ - if any(par is None for par in (self._dim, self._corner_qtile, self._tile_connector)): - raise AttributeError( - f"Cannot access 'qtiles' because either 'dim', 'corner_qtile', or 'tile_connector' is None." - ) - return { - (x, y): self.qtile_xy(x=x, y=y) - for (x, y) in product(range(self._dim.Lx), range(self._dim.Ly)) - } - - @property - def zns(self) -> dict[tuple[int], list[ZNode]]: - """Returns the dictionary where the keys are positions of floor, - and the values are the ``ZNode``s the tile corresponding to the position - contains.""" - return {xy: xy_tile.zns for xy, xy_tile in self.qtiles.items()} - - @property - def ver_zns(self) -> dict[tuple[int], list[ZNode]]: - """Returns the dictionary where the keys are positions of floor, - and the values are the vertical ``ZNode``s the tile corresponding to - the position contains.""" - return {xy: xy_tile.ver_zns for xy, xy_tile in self.qtiles.items()} - - @property - def hor_zns(self) -> dict[tuple[int], list[ZNode]]: - """Returns the dictionary where the keys are positions of floor, - and the values are the horizontal ``ZNode``s the tile corresponding to - the position contains.""" - return {xy: xy_tile.hor_zns for xy, xy_tile in self.qtiles.items()} - - @property - def quo_ext_paths(self) -> dict[str, dict[int, tuple[UWJ, ZSE]]]: - """ - Returns {"col": {col_num: (UWJ, SE)}, "row": {hor_num: (UWJ, SE)}} - of the quo-external-paths - necessary to be covered from z_start to z_end. - """ - if any(par is None for par in (self._dim, self._corner_qtile, self._tile_connector)): - raise AttributeError( - f"Cannot access 'quo_ext_paths' because either 'dim', 'corner_qtile', or 'tile_connector' is None." - ) - result = {"col": defaultdict(list), "row": defaultdict(list)} - hor_con = self._tile_connector[(1, 0)] - ver_con = self._tile_connector[(0, 1)] - if hor_con == PlaneShift(4, 0): - for row_num in range(self._dim.Ly): - for hzn in self.qtile_xy(0, row_num).hor_zns: - hzn_z = hzn.zcoord - result["row"][row_num].append( - ( - UWJ(u=hzn_z.u, w=hzn_z.w, j=hzn_z.j), - ZSE(z_start=hzn_z.z, z_end=hzn_z.z + self._dim.Lx - 1), - ) - ) - elif hor_con == PlaneShift(2, 0): - result["row"] = dict() - else: - raise NotImplementedError - if ver_con == PlaneShift(0, 4): - for col_num in range(self._dim.Lx): - for vzn in self.qtile_xy(col_num, 0).ver_zns: - vzn_z = vzn.zcoord - result["col"][col_num].append( - ( - UWJ(u=vzn_z.u, w=vzn_z.w, j=vzn_z.j), - ZSE(z_start=vzn_z.z, z_end=vzn_z.z + self._dim.Ly - 1), - ) - ) - elif ver_con == PlaneShift(0, 2): - result["col"] = dict() - else: - raise NotImplementedError - return {direction: dict(dir_dict) for direction, dir_dict in result.items()} - - def __repr__(self) -> str: - if self._tile_connector == self.tile_connector0: - tile_connector_str = ")" - else: - tile_connector_str = ", {self._tile_connector!r})" - return f"{type(self).__name__}({self._corner_qtile!r}, {self._dim!r}" + tile_connector_str - - def __str__(self) -> str: - if self._tile_connector == self.tile_connector0: - tile_connector_str = ")" - else: - tile_connector_str = ", {self._tile_connector})" - return f"{type(self).__name__}({self._corner_qtile}, {self._dim})" + tile_connector_str diff --git a/minorminer/utils/zephyr/survey.py b/minorminer/utils/zephyr/survey.py deleted file mode 100644 index 346499a9..00000000 --- a/minorminer/utils/zephyr/survey.py +++ /dev/null @@ -1,363 +0,0 @@ -# Copyright 2025 D-Wave -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ================================================================================================ - - -from __future__ import annotations - -from collections import namedtuple -from functools import cached_property -from typing import Callable, Literal - -import dwave_networkx as dnx -import networkx as nx -from dwave.system import DWaveSampler -from minorminer.utils.zephyr.coordinate_systems import ZephyrCoord -from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape, EdgeKind - -UWKJ = namedtuple("UWKJ", ["u", "w", "k", "j"]) -ZSE = namedtuple("ZSE", ["z_start", "z_end"]) - - -class ZSurvey: - """Takes a Zephyr graph or DWaveSampler with Zephyr topology and - initializes a survey of the existing/missing nodes, edges. Also, gives a survey of - external paths. - - Args: - G (nx.Graph | DWaveSampler): A graph or DWaveSampler with Zephyr topology - Example: - >>> m, t = 3, 2 - >>> G = zephyr_graph(m=m, t=t) - >>> G.remove_nodes_from ([0, 10, 100]) - >>> G.remove_edges_from (list(G.edges())[:20]) - >>> zsur = ZSurvey(G) - >>> print(f"Number of missing nodes is {zsur.num_missing_nodes}") - Number of missing nodes is 3 - >>> print(f"Number of missing edges is {zsur.num_missing_edges}") - Number of missing edges is 46 - >>> print(f"Number of missing edges with both endpoints present is {zsur.num_extra_missing_edges}") - Number of missing edges with both endpoints present is 20 - """ - - def __init__( - self, - G: nx.Graph | DWaveSampler, - ) -> None: - - self._shape, self._input_coord_type = self.get_shape_coord(G) - if isinstance(G, nx.Graph): - G_nodes = list(G.nodes()) - G_edges = list(G.edges()) - elif isinstance(G, DWaveSampler): - G_nodes = G.nodelist - G_edges = G.edgelist - self._nodes: set[ZNode] = { - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape, check_node_valid=False) - for v in G_nodes - } - self._edges: set[ZEdge] = { - ZEdge( - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(u)), shape=self.shape, check_node_valid=False), - ZNode(coord=ZephyrCoord(*self._input_coord_to_coord(v)), shape=self.shape, check_node_valid=False), - check_edge_valid=False, - ) - for (u, v) in G_edges - } - - @staticmethod - def get_shape_coord(G: nx.Graph | DWaveSampler) -> tuple[ZShape, Literal["int", "coordinate"]]: - """ - Returns the shape and coordinates of G, which must be a zephyr graph or - DWaveSampler with zephyr topology. - - Args: - G (nx.Graph | DWaveSampler): A zephyr graph or DWaveSampler with zephyr topology. - - Returns: - tuple[ZShape, Literal["int", "coordinate"]]: - - 0-th index indicates the :class:`ZShape` of ``G``. - - 1-st index is 'int' if the node lables of ``G`` are integers; - it is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. - """ - def _graph_shape_coord(G: nx.Graph) -> tuple[ZShape, Literal["int", "coordinate"]]: - """ - Returns the shape and coordinates of G, which must be a zephyr graph. - - Args: - G (nx.Graph): A zephyr graph with zephyr topology. - - Returns: - tuple[ZShape, Literal["int", "coordinate"]]: - - 0-th index indicates the :class:`ZShape` of ``G``. - - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. - """ - G_info = G.graph - G_top = G_info.get("family") - if G_top != "zephyr": - raise ValueError(f"Expected a graph with zephyr topology, got {G_top}") - - m, t, coord = G_info.get("rows"), G_info.get("tile"), G_info.get("labels") - return ZShape(m=m, t=t), coord - - def _sampler_shape_coord(sampler: DWaveSampler) -> tuple[ZShape, Literal["int", "coordinate"]]: - """ - Returns the shape and coordinates of G, which must be a DWaveSampler with zephyr topology. - - Args: - G (DWaveSampler): A DWaveSampler with zephyr topology. - - Returns: - tuple[ZShape, Literal["int", "coordinate"]]: - - 0-th index indicates the :class:`ZShape` of ``G``. - - 1-st index is 'int' if the node lables of ``G`` are integers; and is 'coordinate' if the node lables of ``G`` are Zephyr coordinates. - """ - sampler_top: dict[str, str | int] = sampler.properties.get("topology") - if sampler_top.get("type") != "zephyr": - raise ValueError(f"Expected a sampler with zephyr topology, got {sampler_top}") - nodes: list[int] = sampler.nodelist - edges: list[tuple[int]] = sampler.edgelist - for v in nodes: - if not isinstance(v, int): - raise NotImplementedError( - f"This is implemented only for nodelist containing 'int' elements , got {v}" - ) - for e in edges: - if not isinstance(e, tuple): - raise NotImplementedError( - f"This is implemented only for edgelist containing 'tuple' elements, got {e}" - ) - if len(e) != 2: - raise ValueError(f"Expected tuple of length 2 in edgelist, got {e}") - if not isinstance(e[0], int) or not isinstance(e[1], int): - raise NotImplementedError( - f"This is implemented only for 'tuple[int]' edgelist, got {e}" - ) - coord: str = "int" - return ZShape(*sampler_top.get("shape")), coord - - if isinstance(G, nx.Graph): - return _graph_shape_coord(G) - if isinstance(G, DWaveSampler): - return _sampler_shape_coord(G) - - - @cached_property - def _input_coord_to_coord(self) -> Callable[[int | tuple[int]], tuple[int]]: - """Returns a function that converts the linear or zephyr coordinates to - the corresponding zephyr coordinates""" - if self._input_coord_type == "int": - return dnx.zephyr_coordinates(m=self.shape.m, t=self.shape.t).linear_to_zephyr - elif self._input_coord_type == "coordinate": - return lambda v: v - else: - raise ValueError( - f"Expected 'int' or 'coordinate' for self.coord, got {self._input_coord_type}" - ) - - @property - def shape(self) -> ZShape: - """Returns the :class:`ZShape` of ``G``.""" - return self._shape - - @property - def nodes(self) -> set[ZNode]: - """Returns the ``ZNode``s of the sampler/graph.""" - return self._nodes - - @cached_property - def missing_nodes(self) -> set[ZNode]: - """Returns the ZNodes of the sampler/graph which are missing compared to perfect yield - Zephyr graph on the same shape. - """ - parent_nodes = [ - ZNode(coord=ZephyrCoord(*v), shape=self._shape, check_node_valid=False) - for v in dnx.zephyr_graph(m=self._shape.m, t=self._shape.t, coordinates=True).nodes() - ] - return {v for v in parent_nodes if not v in self._nodes} - - @property - def edges(self) -> set[ZEdge]: - """Returns the ZEdges of the sampler/graph""" - return self._edges - - @cached_property - def missing_edges(self) -> set[ZEdge]: - """Returns the ZEdges of the sampler/graph which are missing compared to - perfect yield Zephyr graph on the same shape.""" - parent_edges = [ - ZEdge( - ZNode(coord=u, shape=self.shape, check_node_valid=False), - ZNode(coord=v, shape=self.shape, check_node_valid=False), - check_edge_valid=False - ) - for (u, v) in dnx.zephyr_graph( - m=self._shape.m, t=self._shape.t, coordinates=True - ).edges() - ] - return {e for e in parent_edges if not e in self._edges} - - @property - def extra_missing_edges(self) -> set[ZEdge]: - """Returns the ZEdges of the sampler/graph which are missing compared to - perfect yield Zephyr graph on the same shape and are not incident with - a missing node.""" - return {e for e in self.missing_edges if e[0] in self._nodes and e[1] in self._nodes} - - @property - def num_nodes(self) -> int: - """Returns the number of nodes""" - return len(self._nodes) - - @property - def num_missing_nodes(self) -> int: - """Returns the number of missing nodes""" - return len(self.missing_nodes) - - @property - def num_edges(self) -> int: - """Returns the number of edges""" - return len(self._edges) - - @property - def num_missing_edges(self) -> int: - """Returns the number of missing edges""" - return len(self.missing_edges) - - @property - def num_extra_missing_edges(self) -> int: - """Returns the number of missing edges that are not incident - with a missing node""" - return len(self.extra_missing_edges) - - def neighbors( - self, - v: ZNode, - nbr_kind: EdgeKind | None = None, - ) -> set[ZNode]: - """Returns the neighbours of ``v`` when restricted to ``nbr_kind``. - - Args: - v (ZNode): Node to retrieve its neighbors. - nbr_kind (EdgeKind | None, optional): - Edge kind filter. Restricts returned neighbors to those connected by the given edge kind(s). - If None, no filtering is applied. Defaults to None. - - Returns: - set[ZNode]: Set of neighbors of self when restricted by ``nbr_kind``. - """ - if v in self.missing_nodes: - return set() - return {v_nbr for v_nbr in v.neighbors(nbr_kind=nbr_kind) if ZEdge(v, v_nbr) in self._edges} - - def incident_edges( - self, - v: ZNode, - nbr_kind: EdgeKind | None = None, - ) -> set[ZEdge]: - """Returns the edges incident with ``v`` when restricted to ``nbr_kind``. - - Args: - v (ZNode): Node to retrieve its incident edges. - nbr_kind (EdgeKind | None, optional): _description_. Defaults to None. - - Returns: - set[ZEdge]: Set of edges incident with ``v`` when restricted by ``nbr_kind``. - """ - nbrs = self.neighbors(v, nbr_kind=nbr_kind) - if len(nbrs) == 0: - return set() - return {ZEdge(v, v_nbr) for v_nbr in nbrs} - - def degree( - self, - v: ZNode, - nbr_kind: EdgeKind | None = None, - ) -> int: - """Returns degree of ``v`` when restricted by ``nbr_kind``. - - Args: - v (ZNode): Node to calculate its degree. - - nbr_kind (EdgeKind | None, optional): - Edge kind filter. Restricts counting the neighbors to those connected by the given edge kind. - If None, no filtering is applied. Defaults to None. - Returns: - int: Degree of ``v``. - """ - return len(self.neighbors(v, nbr_kind=nbr_kind)) - - def _ext_path(self, uwkj: UWKJ) -> list[ZSE]: - """Returns uwkj_sur, where uwkj_sur contains ZSE(z_start, z_end) - for each non-overlapping external path segment of uwkj. - """ - z_vals = list(range(self.shape.m)) # As in zephyr coordinates - - def _ext_seg(z_start: int) -> ZSE | None: - """If (u, w, k, j, z_start) does not exist, returns None. - Else, finds the external path segment starting at z_start going to right, and - returns the endpoints of the segment (z_start, z_end). - """ - cur: ZNode = ZNode(coord=ZephyrCoord(*uwkj, z_start), shape=self.shape) - if cur in self.missing_nodes: - return None - - upper_z: int = z_vals[-1] - if z_start > upper_z: - return None - - if z_start == upper_z: - return ZSE(z_start, z_start) - - next_ext = ZNode(coord=ZephyrCoord(*uwkj, z_start + 1), shape=self.shape) - ext_edge = ZEdge(cur, next_ext) - if ext_edge in self.missing_edges: - return ZSE(z_start, z_start) - - is_extensible = _ext_seg(z_start + 1) - if is_extensible is None: - return ZSE(z_start, z_start) - - return ZSE(z_start, is_extensible.z_end) - - uwkj_sur: list[ZSE] = [] - while z_vals: - z_start = z_vals[0] - seg = _ext_seg(z_start=z_start) - if seg is None: - z_vals.remove(z_start) - else: - uwkj_sur.append(seg) - for i in range(seg.z_start, seg.z_end + 1): - z_vals.remove(i) - - return uwkj_sur - - def calculate_external_paths_stretch(self) -> dict[UWKJ, list[ZSE]]: - """Calculates the maximal connected z-segments of external paths (expressed as :class:`UWKJ`) of ``G``. - - Returns: - dict[UWKJ, list[ZSE]]: A dictionary in the form of {uwkj: list of maximal connected z-segments (z_start, z_end)}, where - - keys correspond to external paths (expressed as :class:`UWKJ`) of ``G``, - - values correspond to list of maximal connected z-segments (z_start, z_end) of uwkj. - """ - uwkj_vals = [ - UWKJ(u=u, w=w, k=k, j=j) - for u in range(2) # As in zephyr coordinates - for w in range(2 * self.shape.m + 1) # As in zephyr coordinates - for k in range(self.shape.t) # As in zephyr coordinates - for j in range(2) # As in zephyr coordinates - ] - return {uwkj: self._ext_path(uwkj) for uwkj in uwkj_vals} diff --git a/tests/utils/zephyr/test_qfloor.py b/tests/utils/zephyr/test_qfloor.py deleted file mode 100644 index a4f549d6..00000000 --- a/tests/utils/zephyr/test_qfloor.py +++ /dev/null @@ -1,237 +0,0 @@ -# Copyright 2025 D-Wave Systems Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ================================================================================================ - - -import unittest -from itertools import product - -from minorminer.utils.zephyr.node_edge import ZEdge, ZNode, ZShape -from minorminer.utils.zephyr.plane_shift import PlaneShift -from minorminer.utils.zephyr.qfloor import QuoFloor, QuoTile - - -class TestQuoTile(unittest.TestCase): - def test_quo_tile_runs(self) -> None: - zn = ZNode((0, 1), ZShape(m=6)) - shifts0 = [(1, -1), (0, 2), (1, 1), (1, 3)] - zns = [zn + PlaneShift(*x) for x in shifts0] - QuoTile(zns=zns) - - def test_zns_define(self) -> None: - seed0 = ZNode((0, 1), ZShape(m=6)) - shifts0 = [PlaneShift(1, 1), PlaneShift(0, 2)] - zns0 = [seed0 + shift for shift in shifts0] - seed1 = seed0 + PlaneShift(0, 2) - shifts1 = [PlaneShift(1, -1), PlaneShift(0, 0)] - zns1 = [seed1 + shift for shift in shifts1] - zns2 = [ZNode((1, 2), ZShape(m=6)), ZNode((0, 0, 1, 0), ZShape(m=6))] - qtile0 = QuoTile(zns=zns0) - qtile1 = QuoTile(zns=zns1) - qtile2 = QuoTile(zns=zns2) - self.assertEqual(qtile0, qtile1) - self.assertEqual(qtile0, qtile2) - self.assertEqual(qtile2, qtile1) - shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] - shifts_repeat = 2 * shifts - for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: - qtile0 = QuoTile(zns=[zn + shift for shift in shifts]) - qtile1 = QuoTile(zns=[zn + shift for shift in shifts_repeat]) - self.assertEqual(qtile0, qtile1) - - def test_edges(self) -> None: - shifts = [PlaneShift(1, 1), PlaneShift(0, 2)] - for zn in [ZNode((0, 1), ZShape(m=6)), ZNode((5, 12))]: - edges_ = QuoTile([zn + shift for shift in shifts]).edges() - for xy in edges_: - self.assertTrue(isinstance(xy, ZEdge)) - self.assertTrue(isinstance(xy[0], ZNode)) - self.assertTrue(isinstance(xy[1], ZNode)) - self.assertTrue(xy[0].is_neighbor(xy[1])) - - -class TestQuoFloor(unittest.TestCase): - valid_connector = QuoFloor.tile_connector0 - - def test_init_runs_as_expected(self) -> None: - corner_qtile = [ZNode((0, 1)), ZNode((1, 0)), ZNode((1, 2)), ZNode((2, 3))] - bad_corner_qtile = [ZNode((0, 1)), ((1, 0))] - not_imp_connector = {(1, 0): PlaneShift(4, 0), (0, 1): PlaneShift(0, 2)} - QuoFloor(corner_qtile=corner_qtile, dim=(100, 100)) - for m in [6, 8]: - corner_qtile2 = [ZNode((0, 1), ZShape(m=m)), ZNode((1, 0), ZShape(m=m))] - for dim in [(1, 1), (m, m), (m, 1), (1, m), (m // 2, m // 2)]: - qfl = QuoFloor(corner_qtile=corner_qtile2, dim=dim) - self.assertEqual(qfl.dim, dim) - self.assertEqual(set(qfl.corner_qtile.zns), set(corner_qtile2)) - with self.assertRaises(TypeError): - qfl.corner_qtile = bad_corner_qtile - with self.assertRaises(ValueError): - qfl.dim = (1, m + 1) - with self.assertRaises(ValueError): - qfl.dim = (1, 0) - with self.assertRaises(ValueError): - qfl.dim = (0, 1) - with self.assertRaises(NotImplementedError): - qfl.tile_connector = not_imp_connector - with self.assertRaises(ValueError): - QuoFloor(corner_qtile=corner_qtile2, dim=(m + 1, m + 1)) - corner_qtile3 = [ZNode((0, 1)), ZNode((1, 0), ZShape(m=m))] - with self.assertRaises(ValueError): - QuoFloor(corner_qtile=corner_qtile3, dim=(1, 1)) - with self.assertRaises(TypeError): - QuoFloor(corner_qtile=bad_corner_qtile, dim=(1, 1)) - bad_coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] + [(6, 3)] - bad_corner_qtile2 = [ZNode(coord=coord) for coord in bad_coords] - with self.assertRaises(ValueError): - QuoFloor(corner_qtile=bad_corner_qtile2, dim=(1, 1)) - - def test_qtile_xy(self) -> None: - coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] - corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) - qfl = QuoFloor(corner_qtile=corner_qtile, dim=(10, 7)) - # Check the ZNodes in each tile are as expected - for x, y in product([1, 6, 8], [1, 2, 3, 6]): - xy_tile = qfl.qtile_xy(x, y) - coords_xy = sorted([(a + 4 * x, b + 4 * y) for (a, b) in coords]) - xy_tile_expected_zns = [ZNode(coord=c) for c in coords_xy] - self.assertEqual(xy_tile.zns, xy_tile_expected_zns) - - def test_qtiles(self) -> None: - coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(4)] - corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) - for a, b in product([1, 6, 8], [1, 3, 6]): - qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) - tiles = qfl.qtiles - # Check it has m*n values in it - self.assertEqual(len(tiles), a * b) - # Check each tile contains the same number of znodes, none is a repeat - for xy, xy_tile in tiles.items(): - self.assertEqual(len(set(xy_tile)), len(set(coords))) - self.assertEqual(len(set(xy_tile)), len(xy_tile)) - self.assertTrue(xy in product(range(a), range(b))) - for m in [4, 6, 7]: - corner_qtile = QuoTile([ZNode(coord=coord, shape=ZShape(m=m)) for coord in coords]) - for a, b in product([1, 6, 8], [1, 3, 6]): - if a <= m and b <= m: - qfl = QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) - tiles = qfl.qtiles - # Check it has m*n values in it - self.assertEqual(len(tiles), a * b) - # Check each tile contains the same number of znodes, none is a repeat - for xy, xy_tile in tiles.items(): - self.assertEqual(len(set(xy_tile)), len(set(coords))) - self.assertEqual(len(set(xy_tile)), len(xy_tile)) - self.assertTrue(xy in product(range(a), range(b))) - else: - with self.assertRaises(ValueError): - QuoFloor(corner_qtile=corner_qtile, dim=(a, b)) - - def test_zns(self) -> None: - coords = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(3)] - corner_qtile = QuoTile([ZNode(coord=coord) for coord in coords]) - for m, n in product([1, 6, 8], [1, 2, 3, 6]): - qfl = QuoFloor(corner_qtile=corner_qtile, dim=(m, n)) - tiles_zns = qfl.zns - # Check it has m*n values in it - self.assertEqual(len(tiles_zns), m * n) - # Check each tile contains the same number of znodes, none is a repeat - for xy, xy_zns in tiles_zns.items(): - self.assertEqual(len(set(xy_zns)), len(set(coords))) - self.assertEqual(len(set(xy_zns)), len(xy_zns)) - self.assertTrue(xy in product(range(m), range(n))) - - def test_ver_zns(self) -> None: - coords = [(0, 1), (1, 0)] - nodes = [ZNode(coord=c) for c in coords] - nodes += [ - n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] - ] - for dim in [(1, 1), (3, 5), (6, 6)]: - qfl = QuoFloor(corner_qtile=nodes, dim=dim) - qfl_ver = qfl.ver_zns - for xy, xy_ver in qfl_ver.items(): - self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) - for v in xy_ver: - self.assertTrue(v.is_vertical()) - self.assertEqual(len(qfl_ver), dim[0] * dim[1]) - - def test_hor_zns(self) -> None: - coords = [(0, 1), (1, 2)] - nodes = [ZNode(coord=c) for c in coords] - nodes += [ - n + p for n in nodes for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] - ] - for dim in [(4, 7), (1, 1)]: - qfl = QuoFloor(corner_qtile=nodes, dim=dim) - qfl_hor = qfl.hor_zns - for xy, xy_hor in qfl_hor.items(): - self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) - for v in xy_hor: - self.assertTrue(v.is_horizontal()) - self.assertEqual(len(qfl_hor), dim[0] * dim[1]) - for m in [4, 5]: - nodes2 = [ZNode(coord=c, shape=ZShape(m=m)) for c in coords] - nodes2 += [ - n + p - for n in nodes2 - for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] - ] - for dim in [(4, 7), (1, 1), (4, 4), (2, 3)]: - if dim[0] <= m and dim[1] <= m: - qfl = QuoFloor(corner_qtile=nodes2, dim=dim) - qfl_hor = qfl.hor_zns - for xy, xy_hor in qfl_hor.items(): - self.assertTrue(xy in product(range(dim[0]), range(dim[1]))) - for v in xy_hor: - self.assertTrue(v.is_horizontal()) - self.assertEqual(len(qfl_hor), dim[0] * dim[1]) - else: - with self.assertRaises(ValueError): - QuoFloor(corner_qtile=nodes2, dim=dim) - - def test_quo_ext_paths(self) -> None: - coords1 = [(2, 1), (1, 2)] - nodes1 = [ZNode(coord=c) for c in coords1] - nodes1 += [ - n + p for n in nodes1 for p in [PlaneShift(2, 0), PlaneShift(0, 2), PlaneShift(2, 2)] - ] - coords2 = [(k, k + 1) for k in range(4)] + [(k + 1, k) for k in range(3)] - nodes2 = [ZNode(coord=coord) for coord in coords2] - for dim in [(1, 1), (5, 5), (3, 6)]: - floors = [ - QuoFloor(corner_qtile=nodes1, dim=dim), - QuoFloor(corner_qtile=nodes2, dim=dim), - ] - for qfl in floors: - ext_paths = qfl.quo_ext_paths - self.assertTrue("col" in ext_paths) - self.assertTrue("row" in ext_paths) - col_ext_paths = ext_paths["col"] - row_ext_paths = ext_paths["row"] - self.assertEqual(len(col_ext_paths), dim[0]) - self.assertEqual(len(row_ext_paths), dim[1]) - for list_c in col_ext_paths.values(): - # Check no repeat - uwjs_c = [x[0] for x in list_c] - self.assertEqual(len(uwjs_c), len(set(uwjs_c))) - for _, zse in list_c: - self.assertEqual(zse.z_end - zse.z_start + 1, dim[1]) - for list_r in row_ext_paths.values(): - # Check no repeat - uwjs_r = [x[0] for x in list_r] - self.assertEqual(len(uwjs_r), len(set(uwjs_r))) - for _, zse in list_r: - self.assertEqual(zse.z_end - zse.z_start + 1, dim[0]) diff --git a/tests/utils/zephyr/test_zephyr_base.py b/tests/utils/zephyr/test_zephyr_base.py deleted file mode 100644 index bb57d604..00000000 --- a/tests/utils/zephyr/test_zephyr_base.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2025 D-Wave Systems Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ================================================================================================ - - -import unittest -from itertools import product - -import dwave_networkx as dnx -import numpy as np -from dwave.cloud import Client -from dwave.system import DWaveSampler - - -class ZephyrBaseTest(unittest.TestCase): - def setUp(self): - self.initialize_samplers() - self.initialize_rngs() - self.initialize_node_del_percents() - self.initialize_edge_del_percents() - self.initialize_zeph_ms() - self.initialize_graphs() - - def initialize_samplers(self): - with Client.from_config(client="qpu") as client: - zeph_solvers = client.get_solvers( - topology__type__eq="zephyr", - ) - self.samplers = [DWaveSampler(solver=z_solver.id) for z_solver in zeph_solvers] - - def initialize_rngs(self): - self.rngs = [ - np.random.default_rng(seed=1), - np.random.default_rng(seed=10), - ] - - def initialize_zeph_ms(self): - self.zeph_ms = [3, 6] - - def initialize_node_del_percents(self): - self.node_del_percents = [0, 0.03] - - def initialize_edge_del_percents(self): - self.edge_del_percents = [0, 0.02] - - def initialize_graphs(self): - self.graphs = list() - for rng in self.rngs: - for m in self.zeph_ms: - for node_del_per, edge_del_per in product( - self.node_del_percents, self.edge_del_percents - ): - G = dnx.zephyr_graph(m=m, coordinates=True) - num_nodes_to_remove = int(node_del_per * G.number_of_nodes()) - nodes_to_remove = [tuple(v) for v in rng.choice(G.nodes(), num_nodes_to_remove)] - G.remove_nodes_from(nodes_to_remove) - num_edges_to_remove = int(edge_del_per * G.number_of_edges()) - edges_to_remove = [ - (tuple(u), tuple(v)) - for (u, v) in rng.choice(G.edges(), num_edges_to_remove) - ] - G.remove_edges_from(edges_to_remove) - G_dict = { - "G": G, - "m": m, - "num_nodes": G.number_of_nodes(), - "num_edges": G.number_of_edges(), - "num_missing_nodes": num_nodes_to_remove, - "num_missing_edges": dnx.zephyr_graph(m=m).number_of_edges() - - G.number_of_edges(), - "num_extra_missing_edges": num_edges_to_remove, - } - self.graphs.append(G_dict) diff --git a/tests/utils/zephyr/test_zephyr_survey.py b/tests/utils/zephyr/test_zephyr_survey.py deleted file mode 100644 index d8cecb39..00000000 --- a/tests/utils/zephyr/test_zephyr_survey.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright 2025 D-Wave Systems Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# ================================================================================================ - - -from minorminer.utils.zephyr.survey import ZSurvey -from tests.utils.zephyr.test_zephyr_base import ZephyrBaseTest - - -class TestZephyrSurvey(ZephyrBaseTest): - def setUp(self) -> None: - super().setUp() - - def test_get_zephyr_shape_coord_sampler(self) -> None: - for z_sampler in self.samplers: - (m, t), coord = ZSurvey(z_sampler).get_shape_coord(z_sampler) - sampler_top = z_sampler.properties["topology"] - self.assertTrue(m, sampler_top.get("shape")[0]) - self.assertTrue(t, sampler_top.get("shape")[1]) - if coord == "int": - for v in z_sampler.nodelist: - self.assertTrue(isinstance(v, int)) - elif coord == "coordinates": - for v in z_sampler.nodelist: - self.assertTrue(isinstance(v, tuple)) - - def test_get_zephyr_shape_coord_graph(self) -> None: - for G_dict in self.graphs: - z_graph = G_dict["G"] - (m, t), coord = ZSurvey(z_graph).get_shape_coord(z_graph) - G_info = z_graph.graph - self.assertTrue(m, G_info.get("rows")) - self.assertTrue(t, G_info.get("tile")) - self.assertTrue(coord, G_info.get("labels")) - - def test_zephyr_survey_runs_sampler(self) -> None: - for z_sampler in self.samplers: - try: - ZSurvey(z_sampler) - except Exception as e: - self.fail( - f"ZephyrSurvey raised an exception {e} when running with sampler = {z_sampler}" - ) - - def test_zephyr_survey_runs_graph(self) -> None: - for G_dict in self.graphs: - z_graph = G_dict["G"] - try: - ZSurvey(z_graph) - except Exception as e: - self.fail( - f"ZephyrSurvey raised an exception {e} when running with graph = {z_graph}" - ) - - def test_num_nodes(self) -> None: - for z_sampler in self.samplers: - num_nodes = len(z_sampler.nodelist) - self.assertEqual(len(ZSurvey(z_sampler).nodes), num_nodes) - for G_dict in self.graphs: - z_graph = G_dict["G"] - num_nodes = G_dict["num_nodes"] - self.assertEqual(len(ZSurvey(z_graph).nodes), num_nodes) - - def test_num_edges(self) -> None: - for z_sampler in self.samplers: - num_edges = len(z_sampler.edgelist) - self.assertEqual(len(ZSurvey(z_sampler).edges), num_edges) - for G_dict in self.graphs: - z_graph = G_dict["G"] - num_edges = G_dict["num_edges"] - self.assertEqual(len(ZSurvey(z_graph).edges), num_edges) - - def test_external_paths(self) -> None: - for z_sampler in self.samplers: - sur = ZSurvey(z_sampler) - sur.calculate_external_paths_stretch() From 27c829b4df84590c7f1a3974e880404484a28ede Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 5 Nov 2025 14:17:00 -0800 Subject: [PATCH 070/127] Refactor, Add documentation --- minorminer/utils/zephyr/node_edge.py | 16 ++-- minorminer/utils/zephyr/plane_shift.py | 108 +++++++++++-------------- tests/utils/zephyr/test_node_edge.py | 10 +-- tests/utils/zephyr/test_plane_shift.py | 68 ++++++++++------ 4 files changed, 104 insertions(+), 98 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 79cc3014..08df31bd 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -24,7 +24,7 @@ from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, cartesian_to_zephyr, zephyr_to_cartesian) -from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.plane_shift import ZPlaneShift ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) @@ -151,6 +151,8 @@ class ZNode: Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. Defaults to None. + check_node_valid (bool, optional): Flag to whether check the validity of values and types of ``coord``, ``shape``. + Defaults to True. Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), shape = None raises ValueError. In this case the tile size of Zephyr, t, @@ -326,7 +328,7 @@ def is_quo(self) -> bool: def to_quo(self) -> ZNode: """Returns the quotient :class:`ZNode` corresponding to self""" qshape = ZShape(m=self._shape.m) - qccoord = CartesianCoord(x=self._ccoord, y=self._ccoord) + qccoord = CartesianCoord(x=self._ccoord.x, y=self._ccoord.y) return ZNode(coord=qccoord, shape=qshape, convert_to_z=self.convert_to_z) def is_vertical(self) -> bool: @@ -767,10 +769,10 @@ def __le__(self, other: ZNode) -> bool: def __add__( self, - shift: PlaneShift | tuple[int], + shift: ZPlaneShift | tuple[int], ) -> ZNode: - if not isinstance(shift, PlaneShift): - shift = PlaneShift(*shift) + if not isinstance(shift, ZPlaneShift): + shift = ZPlaneShift(*shift) x, y, k = self._ccoord new_x = x + shift[0] new_y = y + shift[1] @@ -780,7 +782,7 @@ def __add__( def __sub__( self, other: ZNode, - ) -> PlaneShift: + ) -> ZPlaneShift: if not isinstance(other, ZNode): raise TypeError(f"Expected {other} to be {type(self).__name__}, got {type(other)}") if self._shape != other._shape: @@ -790,7 +792,7 @@ def __sub__( x_shift: int = self._ccoord.x - other._ccoord.x y_shift: int = self._ccoord.y - other._ccoord.y try: - return PlaneShift(x_shift=x_shift, y_shift=y_shift) + return ZPlaneShift(x=x_shift, y=y_shift) except ValueError as e: raise ValueError(f"{other} cannot be subtracted from {self}") from e diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index b28922c5..ccb6eede 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -17,22 +17,16 @@ from __future__ import annotations -from collections import namedtuple from typing import Iterator -Shift = namedtuple("Shift", ["x", "y"]) - class PlaneShift: - """Initializes PlaneShift with an x_shift, y_shift. + """Initializes PlaneShift with an x, y. Args: - x_shift (int): The displacement in the x-direction of a CartesianCoord. - y_shift (int): The displacement in the y-direction of a CartesianCoord. + x (int): The displacement in the x-direction of a Cartesian coordinate. + y (int): The displacement in the y-direction of a Cartesian coordinate. - Raises: - TypeError: If x_shift or y_shift is not 'int'. - ValueError: If x_shift and y_shift have different parity. Example: >>> from minorminer.utils.zephyr.plane_shift import PlaneShift @@ -40,30 +34,18 @@ class PlaneShift: >>> ps2 = PlaneShift(2, -4) >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") """ - - def __init__( - self, - x_shift: int, - y_shift: int, - ) -> None: - for shift in [x_shift, y_shift]: - if not isinstance(shift, int): - raise TypeError(f"Expected {shift} to be 'int', got {type(shift)}") - if x_shift % 2 != y_shift % 2: - raise ValueError( - f"Expected x_shift, y_shift to have the same parity, got {x_shift, y_shift}" - ) - self._shift = Shift(x_shift, y_shift) + def __init__(self, x: int, y: int) -> None: + self._xy = (x, y) @property def x(self) -> int: """Returns the shift in x direction""" - return self._shift.x + return self._xy[0] @property def y(self) -> int: """Returns the shift in y direction""" - return self._shift.y + return self._xy[1] def __mul__(self, scale: int) -> PlaneShift: """Multiplies the self from left by the number value ``scale``. @@ -74,20 +56,14 @@ def __mul__(self, scale: int) -> PlaneShift: Returns: PlaneShift: The result of left-multiplying self by ``scale``. """ + return type(self)(scale * self.x, scale * self.y) - new_shift_x = scale * self._shift.x - new_shift_y = scale * self._shift.y - return PlaneShift(new_shift_x, new_shift_y) - def __rmul__(self, scale: int | float) -> PlaneShift: + def __rmul__(self, scale: int) -> PlaneShift: """Multiplies the ``self`` from right by the number value ``scale``. Args: - scale (int | float): The scale for right-multiplying ``self`` with. - - Raises: - TypeError: If scale is not 'int' or 'float'. - ValueError: If the resulting PlaneShift has non-whole values. + scale (int): The scale for right-multiplying ``self`` with. Returns: PlaneShift: The result of right-multiplying ``self`` by ``scale``. @@ -107,47 +83,55 @@ def __add__(self, other: PlaneShift) -> PlaneShift: Returns: PlaneShift: The displacement in CartesianCoord by self followed by other. """ - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return PlaneShift(self._shift.x + other._shift.x, self._shift.y + other._shift.y) + if type(self) != type(other): + raise TypeError(f"Expected other to be {type(self).__name__}, got {type(other).__name__}") + return type(self)(self.x + other.x, self.y + other.y) def __iter__(self) -> Iterator[int]: - return self._shift.__iter__() + return self._xy.__iter__() def __len__(self) -> int: - return len(self._shift) + return len(self._xy) def __hash__(self) -> int: - return hash(self._shift) + return hash(self._xy) def __getitem__(self, key) -> int: - return self._shift[key] + return self._xy[key] def __eq__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return self._shift == other._shift + return type(self) == type(other) and self._xy == other._xy - def __lt__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return self._shift < other._shift - def __le__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return (self == other) or (self < other) +class ZPlaneShift(PlaneShift): + """Initializes ZPlaneShift with an x, y. - def __gt__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return self._shift > other._shift + Args: + x (int): The displacement in the x-direction of a Cartesian coordinate. + y (int): The displacement in the y-direction of a Cartesian coordinate. - def __ge__(self, other: PlaneShift) -> bool: - if not isinstance(other, PlaneShift): - raise TypeError(f"Expected other to be PlaneShift, got {type(other)}") - return (self == other) or (self > other) + Raises: + TypeError: If x or y is not 'int'. + ValueError: If x and y have different parity. + + Example: + >>> from minorminer.utils.zephyr.plane_shift import ZPlaneShift + >>> ps1 = ZPlaneShift(1, 3) + >>> ps2 = ZPlaneShift(2, -4) + >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") + """ - def __repr__(self) -> str: - return f"{type(self).__name__}{self._shift.x, self._shift.y}" + def __init__( + self, + x: int, + y: int, + ) -> None: + for shift in [x, y]: + if not isinstance(shift, int): + raise TypeError(f"Expected {shift} to be 'int', got {type(shift)}") + if x % 2 != y % 2: + raise ValueError( + f"Expected x, y to have the same parity, got {x, y}" + ) + self._xy = (x, y) diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py index 8cd7afa3..a5485814 100644 --- a/tests/utils/zephyr/test_node_edge.py +++ b/tests/utils/zephyr/test_node_edge.py @@ -20,7 +20,7 @@ from minorminer.utils.zephyr.coordinate_systems import CartesianCoord, ZephyrCoord from minorminer.utils.zephyr.node_edge import Edge, EdgeKind, NodeKind, ZEdge, ZNode, ZShape -from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.plane_shift import ZPlaneShift class TestEdge(unittest.TestCase): @@ -219,13 +219,13 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: def test_add_sub_runs(self) -> None: left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] - lu_qps = PlaneShift(-1, -1) + lu_qps = ZPlaneShift(-1, -1) right_down_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.right_down_xyms] - rd_qps = PlaneShift(1, 1) + rd_qps = ZPlaneShift(1, 1) midgrid_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.midgrid_xyms] for pc in midgrid_pcs: for s1, s2 in product((-2, 2), (-2, 2)): - pc + PlaneShift(s1, s2) + pc + ZPlaneShift(s1, s2) for pc in left_up_pcs: with self.assertRaises(ValueError): pc + lu_qps @@ -239,7 +239,7 @@ def test_add_sub(self) -> None: left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] for pc in midgrid_pcs + right_down_pcs + left_up_pcs: - self.assertEqual(pc + PlaneShift(0, 0), pc) + self.assertEqual(pc + ZPlaneShift(0, 0), pc) def test_neighbors_generator_runs(self) -> None: for _ in ZNode((1, 12, 4), ZShape(t=6)).neighbors_generator(): diff --git a/tests/utils/zephyr/test_plane_shift.py b/tests/utils/zephyr/test_plane_shift.py index 9180215a..8fd6ee95 100644 --- a/tests/utils/zephyr/test_plane_shift.py +++ b/tests/utils/zephyr/test_plane_shift.py @@ -18,38 +18,24 @@ import unittest from itertools import combinations -from minorminer.utils.zephyr.plane_shift import PlaneShift +from minorminer.utils.zephyr.plane_shift import PlaneShift, ZPlaneShift class TestPlaneShift(unittest.TestCase): def setUp(self) -> None: - shifts = [ - (0, 2), - (-3, -1), - (-2, 0), - (1, 1), - (1, -3), + self.shifts = [ + (0, 1), + (-1, 0), + (2, -1), (-4, 6), (10, 4), (0, 0), ] - self.shifts = shifts def test_valid_input_runs(self) -> None: for shift in self.shifts: PlaneShift(*shift) - def test_invalid_input_gives_error(self) -> None: - invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.0)] - with self.assertRaises(TypeError): - for invalid_type_ in invalid_input_types: - PlaneShift(*invalid_type_) - - invalid_input_vals = [(4, 1), (0, 1)] - with self.assertRaises(ValueError): - for invalid_val_ in invalid_input_vals: - PlaneShift(*invalid_val_) - def test_multiply(self) -> None: for shift in self.shifts: for scale in [0, 1, 2, 5, 10, -3]: @@ -67,9 +53,43 @@ def test_add(self) -> None: ) def test_mul(self) -> None: - self.assertEqual(1.5 * PlaneShift(0, -4), PlaneShift(0, -6)) - with self.assertRaises(ValueError): - 0.5 * PlaneShift(0, -6) - PlaneShift(0, -4) * 0.75 + self.assertEqual(-1 * PlaneShift(0, -4), PlaneShift(0, 4)) + self.assertEqual(3 * PlaneShift(2, 10), PlaneShift(6, 30)) + + + +class TestZPlaneShift(TestPlaneShift): + def setUp(self) -> None: + self.shifts = [ + (0, 2), + (-3, -1), + (-2, 0), + (1, 1), + (1, -3), + (-4, 6), + (10, 4), + (0, 0), + ] + + def test_valid_input_runs(self) -> None: + for shift in self.shifts: + ZPlaneShift(*shift) + + def test_invalid_input_gives_error(self) -> None: + invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.0)] with self.assertRaises(TypeError): - "hello" * PlaneShift(0, -4) + for invalid_type_ in invalid_input_types: + ZPlaneShift(*invalid_type_) + + invalid_input_vals = [(4, 1), (0, 1)] + with self.assertRaises(ValueError): + for invalid_val_ in invalid_input_vals: + ZPlaneShift(*invalid_val_) + + def test_eq(self): + self.assertNotEqual(PlaneShift(0, 0), ZPlaneShift(0, 0)) + + def test_mul(self) -> None: + self.assertEqual(-1 * ZPlaneShift(0, 2), ZPlaneShift(0, -2)) + self.assertEqual(3 * ZPlaneShift(2, 4), ZPlaneShift(6, 12)) + From c9cdb6dc2290956c62b49b34277f19ecc7e8d051 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:50:01 -0800 Subject: [PATCH 071/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 1 - 1 file changed, 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 08df31bd..5f6d2803 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -40,7 +40,6 @@ class NodeKind(Enum): HORIZONTAL = 1 - class Edge: """Initializes an Edge with nodes x, y. From 7614defac5a668e0bf4aabae3e511e1b16f14d64 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:52:51 -0800 Subject: [PATCH 072/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 5f6d2803..9b685ebe 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -56,7 +56,7 @@ def __init__( self._edge = self._set_edge(x, y) def _set_edge(self, x, y): - """Reutrns ordered tuple corresponding to the set {x, y}.""" + """Returns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) else: From 06c04edd2b65a49bd93998d879244f6fec4674b2 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:54:16 -0800 Subject: [PATCH 073/127] Update tests/utils/zephyr/test_plane_shift.py Co-authored-by: Theodor Isacsson --- tests/utils/zephyr/test_plane_shift.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/zephyr/test_plane_shift.py b/tests/utils/zephyr/test_plane_shift.py index 8fd6ee95..03f285ad 100644 --- a/tests/utils/zephyr/test_plane_shift.py +++ b/tests/utils/zephyr/test_plane_shift.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 8120074086ce5f98cb91058581d9788c34a97dce Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:54:40 -0800 Subject: [PATCH 074/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 9b685ebe..0676055c 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -68,7 +68,7 @@ def __hash__(self): def __getitem__(self, index: int) -> int: return self._edge[index] - def __eq__(self, other: Edge): + def __eq__(self, other: Edge) -> bool: return self._edge == other._edge def __str__(self) -> str: From 6363001bbe727c9cc6be1dc3703466b78a6f7739 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:55:52 -0800 Subject: [PATCH 075/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 0676055c..49e7b913 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -149,7 +149,7 @@ class ZNode: m: grid size, t: tile size Defaults to None. convert_to_z (bool | None, optional): Whether to express the coordinates in ZephyrCoordinates. - Defaults to None. + Defaults to None. check_node_valid (bool, optional): Flag to whether check the validity of values and types of ``coord``, ``shape``. Defaults to True. From 0489b5d993a7ecf43e91f748341259034097c95b Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:56:24 -0800 Subject: [PATCH 076/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 49e7b913..be556d07 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -144,7 +144,7 @@ class ZNode: """Initializes 'ZNode' with coord and optional shape. Args: - coord (CartesianCoord | ZephyrCoord | tuple[int]): coordinate in (quotient) Zephyr or (quotient) Cartesian + coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian. shape (ZShape | tuple[int | None] | None, optional): shape of Zephyr graph containing ZNode. m: grid size, t: tile size Defaults to None. From 3f03a6c4776a3fbde88ab66ca7d9bf380f91b10d Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:57:50 -0800 Subject: [PATCH 077/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index be556d07..e4676f59 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -153,8 +153,10 @@ class ZNode: check_node_valid (bool, optional): Flag to whether check the validity of values and types of ``coord``, ``shape``. Defaults to True. - Note: If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), - shape = None raises ValueError. In this case the tile size of Zephyr, t, + ..note:: + + If the given coord has non-None k value (in either Cartesian or Zephyr coordinates), + ``shape = None`` raises ValueError. In this case the tile size of Zephyr, t, must be provided. Example: From d08e0233992d86163b821608fe6ffd246cdd2020 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 10:58:27 -0800 Subject: [PATCH 078/127] Update minorminer/utils/zephyr/plane_shift.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/plane_shift.py | 1 - 1 file changed, 1 deletion(-) diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index ccb6eede..f279d502 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -103,7 +103,6 @@ def __eq__(self, other: PlaneShift) -> bool: return type(self) == type(other) and self._xy == other._xy - class ZPlaneShift(PlaneShift): """Initializes ZPlaneShift with an x, y. From c0af994f2c94c11ba27e59fdfe851342a290dfd5 Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:00:16 -0800 Subject: [PATCH 079/127] Update minorminer/utils/zephyr/node_edge.py Co-authored-by: Theodor Isacsson --- minorminer/utils/zephyr/node_edge.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index e4676f59..91872647 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -82,8 +82,8 @@ class ZEdge(Edge): """Initializes a ZEdge with 'ZNode' nodes x, y. Args: - x (ZNode): One endpoint of edge. - y (ZNode): Another endpoint of edge. + x (ZNode): Endpoint of edge. Must have same shape as ``y``. + y (ZNode): Endpoint of edge. Must have same shape as ``x`` check_edge_valid (bool, optional): Flag to whether check the validity of values and types of ``x``, ``y``. Defaults to True. From b4bb9b3e5329ec1a1467ff52f70c114ad1e2a38c Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:01:41 -0800 Subject: [PATCH 080/127] Update tests/utils/zephyr/test_coordinate_systems.py Co-authored-by: Theodor Isacsson --- tests/utils/zephyr/test_coordinate_systems.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/zephyr/test_coordinate_systems.py b/tests/utils/zephyr/test_coordinate_systems.py index fdbad58a..bfff3e3d 100644 --- a/tests/utils/zephyr/test_coordinate_systems.py +++ b/tests/utils/zephyr/test_coordinate_systems.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 9f501b62d613fd37fc298f0c1d86fad8e37e79ca Mon Sep 17 00:00:00 2001 From: mahdiehmalekian <58615019+mahdiehmalekian@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:10:09 -0800 Subject: [PATCH 081/127] Update tests/utils/zephyr/test_node_edge.py Co-authored-by: Theodor Isacsson --- tests/utils/zephyr/test_node_edge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py index a5485814..627c86ef 100644 --- a/tests/utils/zephyr/test_node_edge.py +++ b/tests/utils/zephyr/test_node_edge.py @@ -1,4 +1,4 @@ -# Copyright 2025 D-Wave Systems Inc. +# Copyright 2025 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 0e64a7da78a24ce8649f335b0ab09333e3e7b2a2 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Fri, 27 Dec 2024 10:26:18 -0800 Subject: [PATCH 082/127] Improve defaulting of max_num_emb; move basic helper function from feasibility to parallel_embeddings --- minorminer/utils/feasibility.py | 20 ------------ minorminer/utils/parallel_embeddings.py | 42 ++++++++++++++++++++----- tests/utils/test_feasibility.py | 10 ------ tests/utils/test_parallel_embeddings.py | 27 +++++++++++----- 4 files changed, 54 insertions(+), 45 deletions(-) diff --git a/minorminer/utils/feasibility.py b/minorminer/utils/feasibility.py index e2f804f3..145446c0 100644 --- a/minorminer/utils/feasibility.py +++ b/minorminer/utils/feasibility.py @@ -110,26 +110,6 @@ def embedding_feasibility_filter( return True -def lattice_size(T: nx.Graph = None) -> int: - """Determines the cellular (square) dimension of a lattice - - The lattice size is the parameter ``m`` of a dwave_networkx graph, also - called number of rows, or in the case of a chimera graph max(m,n). This - upper bounds the ``sublattice_size`` for ``find_sublattice_embeddings``. - - Args: - T: The target graph in which to embed. The graph must be of type - zephyr, pegasus or chimera and constructed by dwave_networkx. - Returns: - int: The maximum possible size of a tile - """ - # Possible feature enhancement, determine a stronger upper bound akin - # to lattice_size_lower_bound, accounting for defects, the - # degree distribution and other simple properties. - - return max(T.graph.get("rows"), T.graph.get("columns")) - - def lattice_size_lower_bound( S: nx.Graph, T: nx.Graph = None, diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 529e74bb..0cdd163b 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -20,7 +20,7 @@ import dwave_networkx as dnx import networkx as nx import numpy as np -from typing import Union +from typing import Union, Optional from minorminer.subgraph import find_subgraph @@ -105,7 +105,7 @@ def find_multiple_embeddings( S: nx.Graph, T: nx.Graph, *, - max_num_emb: int = 1, + max_num_emb: Optional[int] = 1, use_filter: bool = False, embedder: callable = None, embedder_kwargs: dict = None, @@ -128,7 +128,7 @@ def find_multiple_embeddings( S: The source graph to embed. T: The target graph in which to embed. max_num_emb: Maximum number of embeddings to find. - Defaults to 1, set to float('Inf') to find the maximum possible + Defaults to 1, set to None to find the maximum possible number. use_filter: Specifies whether to check feasibility of embedding arguments independently of the embedder method. @@ -166,8 +166,10 @@ def find_multiple_embeddings( _T = shuffle_graph(T, seed=prng) else: _T = T - - max_num_emb = min(int(T.number_of_nodes() / S.number_of_nodes()), max_num_emb) + if max_num_emb is None: + max_num_emb = T.number_of_nodes() // S.number_of_nodes() + else: + max_num_emb = min(T.number_of_nodes() // S.number_of_nodes(), max_num_emb) if shuffle_all_graphs: _S = shuffle_graph(S, seed=prng) @@ -197,13 +199,33 @@ def find_multiple_embeddings( return embs +def lattice_size(T: nx.Graph = None) -> int: + """Determines the cellular (square) dimension of a dwave_networkx lattice + + The lattice size is the parameter ``m`` of a dwave_networkx graph, also + called number of rows, or in the case of a chimera graph max(m,n). This + upper bounds the ``sublattice_size`` for ``find_sublattice_embeddings``. + + Args: + T: The target graph in which to embed. The graph must be of type + zephyr, pegasus or chimera and constructed by dwave_networkx. + Returns: + int: The maximum possible size of a tile + """ + # Possible feature enhancement, determine a stronger upper bound akin + # to lattice_size_lower_bound, accounting for defects, the + # degree distribution and other simple properties. + + return max(T.graph.get("rows"), T.graph.get("columns")) + + def find_sublattice_embeddings( S: nx.Graph, T: nx.Graph, *, tile: nx.Graph = None, sublattice_size: int = None, - max_num_emb: int = 1, + max_num_emb: Optional[int] = 1, use_filter: bool = False, seed: Union[int, np.random.RandomState, np.random.Generator] = None, embedder: callable = None, @@ -246,7 +268,8 @@ def find_sublattice_embeddings( family matching T). ``lattice_size_lower_bound()`` provides a lower bound based on a fast feasibility filter. max_num_emb: Maximum number of embeddings to find. - Defaults to inf (unbounded). + Defaults to 1, set to None for unbounded (try unlimited search on + all lattice offsets). use_filter: Specifies whether to check feasibility of arguments for embedding independently of the embedder routine. Defaults to False. embedder: Specifies the embedding search method, a callable taking ``S``, ``T`` as @@ -333,7 +356,10 @@ def find_sublattice_embeddings( "source graphs must a graph constructed by " "dwave_networkx as chimera, pegasus or zephyr type" ) - + if max_num_emb is None: + max_num_emb = T.number_of_nodes() // S.number_of_nodes() + else: + max_num_emb = min(T.number_of_nodes() // S.number_of_nodes(), max_num_emb) tiling = tile == S embs = [] if max_num_emb == 1 and seed is None: diff --git a/tests/utils/test_feasibility.py b/tests/utils/test_feasibility.py index b8500bdc..41a025d1 100644 --- a/tests/utils/test_feasibility.py +++ b/tests/utils/test_feasibility.py @@ -20,7 +20,6 @@ from minorminer.utils.feasibility import ( embedding_feasibility_filter, - lattice_size, lattice_size_lower_bound, ) @@ -154,15 +153,6 @@ def test_embedding_feasibility_filter(self): "it's always infeasible to embed into an empty graph", ) - def test_lattice_size_subgraph_upper_bound(self): - L = np.random.randint(2) + 2 - T = dnx.zephyr_graph(L - 1) - self.assertEqual(L - 1, lattice_size(T=T)) - T = dnx.pegasus_graph(L) - self.assertEqual(L, lattice_size(T=T)) - T = dnx.chimera_graph(L, L - 1, 1) - self.assertEqual(L, lattice_size(T=T)) - def test_lattice_size_lower_bound(self): L = np.random.randint(2) + 2 T = dnx.zephyr_graph(L - 1) diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index 5f4c0b73..f94fad5c 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -20,6 +20,7 @@ from minorminer import find_embedding from minorminer.utils.parallel_embeddings import ( + lattice_size, shuffle_graph, embeddings_to_array, find_multiple_embeddings, @@ -27,6 +28,18 @@ ) +class TestLatticeSize(unittest.TestCase): + # This is a very basic helper function + def test_lattice_size(self): + L = np.random.randint(2) + 2 + T = dnx.zephyr_graph(L - 1) + self.assertEqual(L - 1, lattice_size(T=T)) + T = dnx.pegasus_graph(L) + self.assertEqual(L, lattice_size(T=T)) + T = dnx.chimera_graph(L, L - 1, 1) + self.assertEqual(L, lattice_size(T=T)) + + class TestEmbeddings(unittest.TestCase): def test_shuffle_graph(self): @@ -129,9 +142,9 @@ def test_find_multiple_embeddings_basic(self): for e in square } T = nx.from_edgelist(squares) # Room for 4! - embs = find_multiple_embeddings(S, T, max_num_emb=float("inf")) - - self.assertLess(len(embs), 5, "Impossibly many") + for max_num_emb in [None, 6]: # None means unbounded + embs = find_multiple_embeddings(S, T, max_num_emb=max_num_emb) + self.assertLess(len(embs), 5, "Impossibly many") self.assertTrue( all(set(emb.keys()) == set(S.nodes()) for emb in embs), "bad keys in embedding(s)", @@ -249,7 +262,7 @@ def test_find_sublattice_embeddings_basic(self): self.assertEqual(len(embs), 1, "mismatched number of embeddings") embs = find_sublattice_embeddings( - S, T, sublattice_size=min_sublattice_size, max_num_emb=float("Inf") + S, T, sublattice_size=min_sublattice_size, max_num_emb=None ) self.assertEqual(len(embs), num_emb, "mismatched number of embeddings") self.assertTrue( @@ -275,7 +288,7 @@ def test_find_sublattice_embeddings_tile(self): S, T, sublattice_size=min_sublattice_size, - max_num_emb=float("Inf"), + max_num_emb=None, tile=tile, ) self.assertEqual(len(embs), 4) @@ -289,7 +302,7 @@ def test_find_sublattice_embeddings_tile(self): S, T, sublattice_size=min_sublattice_size, - max_num_emb=float("Inf"), + max_num_emb=None, tile=tile5, ) self.assertEqual(len(embs), 0, "Tile is too small") @@ -299,7 +312,7 @@ def test_find_sublattice_embeddings_tile(self): S, T, sublattice_size=min_sublattice_size, - max_num_emb=float("Inf"), + max_num_emb=None, tile=tile, embedder=lambda x: "without S=tile trigger error", ) From bc8bfef5ed4fccf85d817d9697d034b7b36e12d4 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 6 Jan 2025 13:30:12 -0800 Subject: [PATCH 083/127] Correct lattice_size signature per review request --- minorminer/utils/parallel_embeddings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 0cdd163b..1fa59e27 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -199,7 +199,7 @@ def find_multiple_embeddings( return embs -def lattice_size(T: nx.Graph = None) -> int: +def lattice_size(T: Optional[nx.Graph] = None) -> int: """Determines the cellular (square) dimension of a dwave_networkx lattice The lattice size is the parameter ``m`` of a dwave_networkx graph, also From dd508ae35345b051c3ea8bcc1a874e2a010dd606 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Wed, 12 Mar 2025 15:11:04 -0700 Subject: [PATCH 084/127] Simplify tiling branch behaviour (single fixed embedding). Add timeout parameter --- minorminer/utils/parallel_embeddings.py | 159 +++++++++++++++++++++--- 1 file changed, 139 insertions(+), 20 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 1fa59e27..908bb37c 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -17,10 +17,13 @@ """ import warnings +from time import perf_counter +import time import dwave_networkx as dnx import networkx as nx import numpy as np -from typing import Union, Optional +from typing import Union, Optional, Callable +from dwave.embedding import is_valid_embedding from minorminer.subgraph import find_subgraph @@ -101,17 +104,46 @@ def embeddings_to_array(embs: list, node_order=None, as_ndarray=False): return [[emb[v] for v in node_order] for emb in embs] +def array_to_embeddings(embs: list, node_order=None): + """Convert list of embedding lists (values) to dictionary + + Args: + embs: A list of embeddings, each embedding is a list where values are + chains in node order. + node_order: An iterable giving the ordering of + variables in each row. When not provided variables are ordered to + match the first embedding :code:``embs[0].keys()``. node_order + can define any subset of the source graph nodes (embedding + keys). + + Returns: + An embedding dictionary + """ + if len(embs) is None: + return [] + + if node_order is None: + node_order = range(len(embs[0])) + + if len(embs) == len(node_order): + embs = [{node_order[idx]: v for idx, v in enumerate(embs)}] + else: + embs = [{node_order[idx]: v for idx, v in enumerate(emb)} for emb in embs] + return embs + + def find_multiple_embeddings( S: nx.Graph, T: nx.Graph, *, max_num_emb: Optional[int] = 1, - use_filter: bool = False, + use_filter: bool = True, embedder: callable = None, embedder_kwargs: dict = None, one_to_iterable: bool = False, shuffle_all_graphs: bool = False, seed: Union[int, np.random.RandomState, np.random.Generator] = None, + timeout: float = float("Inf"), ) -> list: """Finds multiple disjoint embeddings of a source graph onto a target graph @@ -120,9 +152,9 @@ def find_multiple_embeddings( after each successful embedding. Embedding multiple times on a large graph can take significant time. - It is recommended the user adjust embedder_kwargs appropriately such - as timeout, and also consider bounding the number of embeddings - returned (with max_num_emb). + It is recommended the user adjust both the timeout parameter and + embedder_kwargs appropriately (incl. timeout), and also consider bounding + the number of embeddings returned (with max_num_emb). Args: S: The source graph to embed. @@ -131,7 +163,8 @@ def find_multiple_embeddings( Defaults to 1, set to None to find the maximum possible number. use_filter: Specifies whether to check feasibility - of embedding arguments independently of the embedder method. + of embedding arguments independently of the embedder method. In some + easy to embed cases use of a filter can slow down operation. embedder: Specifies the embedding search method, a callable taking ``S``, ``T``, as the first two parameters. Defaults to ``minorminer.subgraph.find_subgraph``. @@ -147,6 +180,9 @@ def find_multiple_embeddings( diversification of the embeddings found. seed: seed for the ``numpy.random.Generator`` controlling shuffling (if invoked). + timeout: total time allowed across all embeddings in seconds. Time elapsed is + checked before each call to embedder. The total runtime is thereby bounded + by timeout plus the time required by one call to embedder. Returns: list: A list of disjoint embeddings. Each embedding follows the format @@ -154,6 +190,7 @@ def find_multiple_embeddings( map from the source to the target graph as a dictionary without reusing target variables. """ + timeout = perf_counter() + timeout embs = [] if embedder is None: embedder = find_subgraph @@ -183,7 +220,12 @@ def find_multiple_embeddings( ): emb = [] else: - emb = embedder(_S, _T, **embedder_kwargs) + if perf_counter() >= timeout: + emb = [] + else: + if timeout == 0: + raise ValueError() + emb = embedder(_S, _T, **embedder_kwargs) if len(emb) == 0: break @@ -219,6 +261,21 @@ def lattice_size(T: Optional[nx.Graph] = None) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) +def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): + """Special handling of 1:1 mappings""" + if one_to_iterable: + return is_valid_embedding(emb, S, T) + else: + return is_valid_embedding({k: (v,) for k, v in emb.items()}, S, T) + + +def _mapped_proposal(emb: dict, f: Callable, one_to_iterable: bool = True): + if one_to_iterable: + return {k: tuple(f(n) for n in c) for k, c in emb.items()} + else: + return {k: f(n) for k, n in emb.items()} + + def find_sublattice_embeddings( S: nx.Graph, T: nx.Graph, @@ -226,13 +283,16 @@ def find_sublattice_embeddings( tile: nx.Graph = None, sublattice_size: int = None, max_num_emb: Optional[int] = 1, - use_filter: bool = False, + use_filter: bool = True, + use_tile_embedding: Optional[bool] = None, + tile_embedding: Optional[dict] = None, seed: Union[int, np.random.RandomState, np.random.Generator] = None, embedder: callable = None, embedder_kwargs: dict = None, one_to_iterable: bool = False, shuffle_all_graphs: bool = False, shuffle_sublattice_order: bool = False, + timeout: float = float("Inf"), ) -> list: """Searches for embeddings on sublattices of the target graph. @@ -271,13 +331,22 @@ def find_sublattice_embeddings( Defaults to 1, set to None for unbounded (try unlimited search on all lattice offsets). use_filter: Specifies whether to check feasibility of arguments for - embedding independently of the embedder routine. Defaults to False. + embedding independently of the embedder routine. In some + easy to embed cases use of a filter can slow down operation. + use_tile_embedding: A single embedding for a tile is generated, and then + reused for a maximum number of sublattices. If, due to defects or + overlap with an existing assignment, the embedding fails at a particular + offset embedder is not invoked. + tile_embedding: When provided, this should be an embedding from the source + to the tile. If not provided it is generated by the embedder method. Note + that `one_to_iterable` should be adjusted to match the value type. embedder: Specifies the embedding search method, a callable taking ``S``, ``T`` as - the first two arguments. Defaults to minorminer.subgraph.find_subgraph. + the first two arguments. Defaults to minorminer.subgraph.find_subgraph. Note + that if `one_to_iterable` should be adjusted to match the return type. embedder_kwargs: Dictionary specifying arguments for the embedder other than ``S``, ``T``. - one_to_iterable: Specifies whether the embedder returns a dict with - iterable values. Defaults to False to match find_subgraph. + one_to_iterable: Specifies whether the embedder returns (and/or tile_embedding is) + a dict with iterable values (True), or a 1:1 mapping (False). shuffle_all_graphs: If True, the tile-masked target graph and source graph are shuffled on each embedding attempt. Note that, if the embedder supports randomization this should be preferred by use of embedder_kwargs. If an @@ -288,13 +357,20 @@ def find_sublattice_embeddings( for diversification of the embeddings found. seed: seed for the `numpy.random.Generator` controlling shuffling (if invoked). + timeout: total time allowed across all embeddings in seconds. Time elapsed is + checked before each sublattice search begins, and before each call of the + embedder function. The total runtime is thereby bounded by timeout plus the + time required by one call to embedder. Raises: ValueError: If the target graph ``T`` is not of type zephyr, pegasus, or chimera. + If the tile_embedding is incompatible with the source graph and tile. Returns: list: A list of disjoint embeddings. """ + + timeout = perf_counter() + timeout if sublattice_size is None and tile is None: return find_multiple_embeddings( S=S, @@ -356,11 +432,52 @@ def find_sublattice_embeddings( "source graphs must a graph constructed by " "dwave_networkx as chimera, pegasus or zephyr type" ) + if tile_embedding is not None: + if not _is_valid_embedding(tile_embedding, S, tile, one_to_iterable): + raise ValueError("tile_embedding is invalid for S and tile") + else: + use_filter = False # Unnecessary + + if use_tile_embedding is None: + use_tile_embedding = tile == S # Trivial 1:1 + if use_tile_embedding and tile_embedding is None: + tile_embedding = {i: i for i in tile.nodes} + use_filter = False # Unnecessary + + if use_filter or (use_tile_embedding and tile_embedding is None): + # Check defect-free tile embedding is viable + + # With respect to filter: + # * This assumes that an embedder that returns on a graph G + # will also return an embedding for any graph where G is + # a subgraph. Can be violated given heuristic search + defect_free_embs = find_multiple_embeddings( + S, + tile, + max_num_emb=1, + use_filter=False, + seed=seed, + embedder=embedder, + embedder_kwargs=embedder_kwargs, + timeout=timeout - perf_counter(), + ) + if len(defect_free_embs) == 0: + # If embedding is infeasible on the tile*, it will be infeasible + # on all subgraphs thereof (assuming sufficient time was provided + # in embedder_kwargs) + return [] + if use_tile_embedding: + tile_embedding = defect_free_embs[0] + # Apply sufficient restriction on the tile + if one_to_iterable: + tile = tile.subgraph({v for c in tile_embedding.values() for v in c}) + else: + tile = tile.subgraph({v for v in tile_embedding.values()}) if max_num_emb is None: max_num_emb = T.number_of_nodes() // S.number_of_nodes() else: max_num_emb = min(T.number_of_nodes() // S.number_of_nodes(), max_num_emb) - tiling = tile == S + embs = [] if max_num_emb == 1 and seed is None: _T = T @@ -376,22 +493,25 @@ def find_sublattice_embeddings( sublattice_iter = sublattice_mappings(tile, _T) for f in sublattice_iter: - Tr = _T.subgraph([f(n) for n in tile]) - if tiling: - if Tr.number_of_edges() == S.number_of_edges(): - sub_embs = [{k: v for k, v in zip(S.nodes, Tr.nodes)}] + if perf_counter() > timeout: + break + if use_tile_embedding: + proposal = _mapped_proposal(tile_embedding, f, one_to_iterable) + if _is_valid_embedding(proposal, S, T, one_to_iterable): + sub_embs = [proposal] else: sub_embs = [] - else: + Tr = _T.subgraph([f(n) for n in tile]) sub_embs = find_multiple_embeddings( S, Tr, - max_num_emb=max_num_emb, + max_num_emb=max_num_emb - len(embs), use_filter=use_filter, seed=seed, embedder=embedder, embedder_kwargs=embedder_kwargs, + timeout=timeout - perf_counter(), ) embs += sub_embs if len(embs) >= max_num_emb: @@ -405,5 +525,4 @@ def find_sublattice_embeddings( _T.remove_nodes_from([v for c in emb.values() for v in c]) else: _T.remove_nodes_from(emb.values()) - return embs From 898d16fc20513fcce4420fc98329488a9c112037 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:31:31 -0700 Subject: [PATCH 085/127] Add test of timeout --- tests/utils/test_parallel_embeddings.py | 106 +++++++++++++++++++++++- 1 file changed, 105 insertions(+), 1 deletion(-) diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index f94fad5c..2aaff3c4 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -193,6 +193,14 @@ def test_find_multiple_embeddings_advanced(self): embs[i - 1][idx] == emb[idx] for idx, emb in enumerate(embss[i]) ) + # timeout + embs = find_multiple_embeddings( + S, T, timeout=float("Inf") + ) # Inf is current default .. + self.assertLess(0, len(embs), "embeddings with large timeout") + embs = find_multiple_embeddings(S, T, timeout=0) + self.assertEqual(0, len(embs), "no embeddings with timeout of 0") + # randomization seed = 42 embs_run1 = find_multiple_embeddings( S, T, max_num_emb=4, seed=seed, shuffle_all_graphs=True @@ -304,6 +312,7 @@ def test_find_sublattice_embeddings_tile(self): sublattice_size=min_sublattice_size, max_num_emb=None, tile=tile5, + use_filter=False, # Easiest way to suppress warnings ) self.assertEqual(len(embs), 0, "Tile is too small") @@ -321,11 +330,11 @@ def test_find_sublattice_embeddings_tile(self): self.assertEqual(len(nodes_used), S.number_of_nodes() * len(embs)) invalid_T = nx.complete_graph(5) # Complete graph is not a valid topology + with self.assertRaises(ValueError): find_sublattice_embeddings( S, invalid_T, sublattice_size=min_sublattice_size, tile=tile ) - small_T = dnx.chimera_graph(m=2, n=2) small_S = dnx.chimera_graph(m=2, n=1) sublattice_size = 1 # Too small @@ -337,6 +346,101 @@ def test_find_sublattice_embeddings_tile(self): with self.assertWarns(Warning): find_sublattice_embeddings(small_S, small_T, tile=tile, use_filter=True) + def test_find_sublattice_embeddings_tile_embedding(self): + # SUBGRAPHS # + S = nx.from_edgelist({(i, i + 1) for i in range(3)}) # 4 node path + T = dnx.chimera_graph(2, t=2) + tile_embedding = {0: 0, 1: 2, 2: 1, 3: 3} + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=False, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + self.assertEqual(len(embs), 1) + + T = dnx.chimera_graph(2, t=2, edge_list=[(0, 2), (1, 2), (1, 3)]) # Valid + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=False, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + self.assertEqual(len(embs), 1) + T = dnx.chimera_graph(2, t=2, edge_list=[(0, 2), (1, 2), (0, 3)]) + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=False, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + self.assertEqual(len(embs), 0) + with self.assertRaises(ValueError): + # No connection (0,1) or (2,3), invalid tile_embedding on target tile. + tile_embedding = {i: i for i in range(4)} + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=False, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + + # MINORS (CHAIN LENGTH <=2) # + S = nx.from_edgelist([(0, 1)]) + T = dnx.chimera_graph(2, t=2) + tile_embedding = {0: (0,), 1: (1, 3)} + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=True, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + self.assertEqual(len(embs), 1) + T = dnx.chimera_graph(2, t=2, edge_list=[(0, 3), (1, 3)]) + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=True, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + self.assertEqual(len(embs), 1, "Embedding succeeds") + T = dnx.chimera_graph(2, t=2, edge_list=[(1, 3), (1, 2)]) + for use_tile_embedding in [True, False]: + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=True, + use_tile_embedding=use_tile_embedding, + tile_embedding=tile_embedding, + ) + self.assertEqual( + len(embs), + 1 - int(use_tile_embedding), + "Embedding should fail due to use_tile_embedding=True", + ) + + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=1, + one_to_iterable=True, + use_tile_embedding=True, + tile_embedding=tile_embedding, + ) + if __name__ == "__main__": unittest.main() From fdafc0abcd171bf31be68ba2ddec3981f7f53425 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Mon, 3 Mar 2025 15:03:00 -0800 Subject: [PATCH 086/127] Restructure docs (minimal changes) --- README.rst | 22 ++--- docs/source/LICENSE | 1 - docs/source/index.rst | 89 +++++++++++-------- docs/source/installation.rst | 16 ---- docs/source/intro.rst | 44 --------- docs/source/license.rst | 4 - .../reference/{index.rst => api_ref.rst} | 0 docs/source/reference/general_embedding.rst | 4 +- docs/source/sdk_index.rst | 15 ---- 9 files changed, 64 insertions(+), 131 deletions(-) delete mode 120000 docs/source/LICENSE delete mode 100644 docs/source/installation.rst delete mode 100644 docs/source/intro.rst delete mode 100644 docs/source/license.rst rename docs/source/reference/{index.rst => api_ref.rst} (100%) delete mode 100644 docs/source/sdk_index.rst diff --git a/README.rst b/README.rst index 24d2e051..f28f7c50 100644 --- a/README.rst +++ b/README.rst @@ -13,18 +13,17 @@ .. image:: https://img.shields.io/badge/arXiv-1507.04774-b31b1b.svg :target: https://arxiv.org/abs/1507.04774 -.. index-start-marker ========== minorminer ========== +.. start_minorminer_about + `minorminer` is a heuristic tool for minor embedding: given a minor and target graph, it tries to find a mapping that embeds the minor into the target. -.. index-end-marker - -.. general-embedding-start-marker +.. start_minorminer_about_general_embedding The primary utility function, ``find_embedding()``, is an implementation of the heuristic algorithm described in [1]. It accepts various optional parameters @@ -43,7 +42,7 @@ biclique embeddings as well. [2] https://arxiv.org/abs/1507.04774 -.. general-embedding-end-marker +.. end_minorminer_about Python ====== @@ -51,8 +50,6 @@ Python Installation ------------ -.. install-python-start - pip installation is recommended for platforms with precompiled wheels posted to pypi. Source distributions are provided as well. @@ -77,11 +74,11 @@ and then run the `setuptools` script. python -m pytest . -.. install-python-end - Examples -------- +.. start_minorminer_examples_python + .. code-block:: python from minorminer import find_embedding @@ -142,6 +139,7 @@ A more fleshed out example can be found under `examples/fourcolor.py` pip install -r requirements.txt python fourcolor.py +.. end_minorminer_examples_python C++ === @@ -149,8 +147,6 @@ C++ Installation ------------ -.. install-c-start - The `CMakeLists.txt` in the root of this repo will build the library and optionally run a series of tests. On Linux, the commands would be something like this: @@ -179,8 +175,6 @@ to add the following lines to your `CMakeLists.txt` # After your target is defined target_link_libraries(your_target minorminer pthread) -.. install-c-end - Examples -------- @@ -205,6 +199,8 @@ Released under the Apache License 2.0. See ``_ file. Contributing ============ +.. todo:: update links + Ocean's `contributing guide `_ has guidelines for contributing to Ocean packages. diff --git a/docs/source/LICENSE b/docs/source/LICENSE deleted file mode 120000 index 30cff740..00000000 --- a/docs/source/LICENSE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index f498398a..6f49c3ea 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,52 +1,69 @@ +.. index_minorminer: + +========== +minorminer +========== + +.. toctree:: + :caption: Reference documentation for minorminer: + :maxdepth: 1 + + reference/api_ref + +About minorminer +================ .. include:: README.rst - :start-after: index-start-marker - :end-before: index-end-marker + :start-after: start_minorminer_about + :end-before: end_minorminer_about -Documentation -------------- +Examples +-------- -.. only:: html +.. include:: README.rst + :start-after: start_minorminer_examples_python + :end-before: end_minorminer_examples_python - :Release: |version| - :Date: |today| +.. todo:: update this entire section (taken from the intro) -.. note:: This documentation is for the latest version of - `minorminer `_. - Documentation for the version currently installed by - `dwave-ocean-sdk `_ - is here: :std:doc:`minorminer `. +Introduction +------------ -.. sdk-start-marker +.. automodule:: minorminer -.. toctree:: - :maxdepth: 2 +`minorminer` is a library of tools for finding graph minor embeddings, developed +to embed Ising problems onto quantum annealers (QA). While this library can be +used to find minors in arbitrary graphs, it is particularly geared towards +state-of-the-art QA: problem graphs of a few to a few hundred variables, and +hardware graphs of a few thousand qubits. - intro - reference/index +`minorminer` has both a Python and C++ API, and includes implementations of +multiple embedding algorithms to best fit different problems. -.. sdk-end-marker +Minor-Embedding and QPU Topology +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. toctree:: - :caption: Code - :maxdepth: 1 +For an introduction to minor-embedding, see :std:doc:`Minor-Embedding `. - Source - installation - license +For an introduction to the topologies of D-Wave hardware graphs, see +:std:doc:`QPU Topology `. Leap users also have access +to the Exploring Pegasus Jupyter Notebook that explains the architecture of +D-Wave's quantum computer, Advantage, in further detail. -.. toctree:: - :caption: D-Wave's Ocean Software - :maxdepth: 1 +Minor-embedding can be done manually, though typically for very small problems +only. For a walkthrough of the manual minor-embedding process, see the +`Constraints Example: Minor-Embedding `_. - Ocean Home - Ocean Documentation - Ocean Glossary +Minor-Embedding in Ocean +======================== -.. toctree:: - :caption: D-Wave - :maxdepth: 1 +Minor-embedding can also be automated through Ocean. `minorminer` is used by several +:std:doc:`Ocean embedding composites ` +for this purpose. For details on automated (and manual) minor-embedding through +Ocean, see how the `EmbeddingComposite` and `FixedEmbeddingComposite` are used +in this :std:doc:`Boolean AND Gate example `. - D-Wave - Leap - D-Wave System Documentation +Once an embedding has been found, D-Wave's Problem Inspector tool can be used to +evaluate its quality. See +:std:doc:`Using the Problem Inspector ` +for more information. \ No newline at end of file diff --git a/docs/source/installation.rst b/docs/source/installation.rst deleted file mode 100644 index 47589605..00000000 --- a/docs/source/installation.rst +++ /dev/null @@ -1,16 +0,0 @@ -Installation -============ - -Python ------- - -.. include:: README.rst - :start-after: install-python-start - :end-before: install-python-end - -C++ ---- - -.. include:: README.rst - :start-after: install-c-start - :end-before: install-c-end diff --git a/docs/source/intro.rst b/docs/source/intro.rst deleted file mode 100644 index 0c54930d..00000000 --- a/docs/source/intro.rst +++ /dev/null @@ -1,44 +0,0 @@ -.. intro_minorminer: - -============ -Introduction -============ - -.. automodule:: minorminer - -`minorminer` is a library of tools for finding graph minor embeddings, developed -to embed Ising problems onto quantum annealers (QA). While this library can be -used to find minors in arbitrary graphs, it is particularly geared towards -state-of-the-art QA: problem graphs of a few to a few hundred variables, and -hardware graphs of a few thousand qubits. - -`minorminer` has both a Python and C++ API, and includes implementations of -multiple embedding algorithms to best fit different problems. - -Minor-Embedding and QPU Topology -================================ - -For an introduction to minor-embedding, see :std:doc:`Minor-Embedding `. - -For an introduction to the topologies of D-Wave hardware graphs, see -:std:doc:`QPU Topology `. Leap users also have access -to the Exploring Pegasus Jupyter Notebook that explains the architecture of -D-Wave's quantum computer, Advantage, in further detail. - -Minor-embedding can be done manually, though typically for very small problems -only. For a walkthrough of the manual minor-embedding process, see the -`Constraints Example: Minor-Embedding `_. - -Minor-Embedding in Ocean -======================== - -Minor-embedding can also be automated through Ocean. `minorminer` is used by several -:std:doc:`Ocean embedding composites ` -for this purpose. For details on automated (and manual) minor-embedding through -Ocean, see how the `EmbeddingComposite` and `FixedEmbeddingComposite` are used -in this :std:doc:`Boolean AND Gate example `. - -Once an embedding has been found, D-Wave's Problem Inspector tool can be used to -evaluate its quality. See -:std:doc:`Using the Problem Inspector ` -for more information. diff --git a/docs/source/license.rst b/docs/source/license.rst deleted file mode 100644 index 71e7788c..00000000 --- a/docs/source/license.rst +++ /dev/null @@ -1,4 +0,0 @@ -License -======= - -.. include:: LICENSE diff --git a/docs/source/reference/index.rst b/docs/source/reference/api_ref.rst similarity index 100% rename from docs/source/reference/index.rst rename to docs/source/reference/api_ref.rst diff --git a/docs/source/reference/general_embedding.rst b/docs/source/reference/general_embedding.rst index b5c92517..217222c4 100644 --- a/docs/source/reference/general_embedding.rst +++ b/docs/source/reference/general_embedding.rst @@ -7,8 +7,8 @@ General Embedding General embedding refers to embedding that may be useful for any type of graph. .. include:: ../README.rst - :start-after: general-embedding-start-marker - :end-before: general-embedding-end-marker + :start-after: start_minorminer_about_general_embedding + :end-before: end_minorminer_about .. autofunction:: minorminer.find_embedding diff --git a/docs/source/sdk_index.rst b/docs/source/sdk_index.rst deleted file mode 100644 index 151a67c6..00000000 --- a/docs/source/sdk_index.rst +++ /dev/null @@ -1,15 +0,0 @@ -.. _sdk_index_minorminer: - -.. include:: README.rst - :start-after: index-start-marker - :end-before: index-end-marker - -.. include:: index.rst - :start-after: sdk-start-marker - :end-before: sdk-end-marker - -.. toctree:: - :caption: Code - :maxdepth: 1 - - Source From 89387196fc773c469b0403a8059ae1678722a05f Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Mon, 3 Mar 2025 15:39:04 -0800 Subject: [PATCH 087/127] Update header and conf.py fixes --- docs/source/conf.py | 16 ++++++++-------- docs/source/reference/api_ref.rst | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 9094f628..d5c109dd 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,12 +26,12 @@ _PY2 = sys.version_info.major == 2 # Add __version__, __author__, __authoremail__, __description__ to this namespace -path_to_package_info = os.path.join( - '..', '..', 'minorminer', 'package_info.py') -if _PY2: - execfile(path_to_package_info) -else: - exec(open(path_to_package_info).read()) +# path_to_package_info = os.path.join( +# '..', '..', 'minorminer', 'package_info.py') +# if _PY2: +# execfile(path_to_package_info) +# else: +# exec(open(path_to_package_info).read()) # -- General configuration ------------------------------------------------ @@ -80,9 +80,9 @@ # built documents. # # The short X.Y version. -version = __version__ +# version = __version__ # The full version, including alpha/beta/rc tags. -release = __version__ +# release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/docs/source/reference/api_ref.rst b/docs/source/reference/api_ref.rst index 4a2a4f31..a1de449c 100644 --- a/docs/source/reference/api_ref.rst +++ b/docs/source/reference/api_ref.rst @@ -1,8 +1,8 @@ .. _reference_minorminer: -======================= -Reference Documentation -======================= +============= +API Reference +============= .. toctree:: :maxdepth: 2 From 8fc86ec2b2ad3a14dc39f401d2cbbf9ea74dfc34 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Mon, 3 Mar 2025 17:09:14 -0800 Subject: [PATCH 088/127] Add Usage Info section, fix api_ref anchor --- docs/source/index.rst | 14 +++++++++++++- docs/source/reference/api_ref.rst | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 6f49c3ea..7507e43e 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -66,4 +66,16 @@ in this :std:doc:`Boolean AND Gate example `. Once an embedding has been found, D-Wave's Problem Inspector tool can be used to evaluate its quality. See :std:doc:`Using the Problem Inspector ` -for more information. \ No newline at end of file +for more information. + +Usage Information +================= + +.. todo:: add the intersphinx prefix to enable these links for self builds + +* :ref:`index_concepts` for terminology +* :ref:`qpu_embedding_intro` for an introduction to minor embedding +* :ref:`qpu_embedding_guidance` provides advanced guidance +* Examples in the :ref:`qpu_example_and`, :ref:`qpu_example_multigate`, + and :ref:`qpu_example_inspector_graph_partitioning` sections show through + some simple examples how to embed and set chain strength. \ No newline at end of file diff --git a/docs/source/reference/api_ref.rst b/docs/source/reference/api_ref.rst index a1de449c..21374312 100644 --- a/docs/source/reference/api_ref.rst +++ b/docs/source/reference/api_ref.rst @@ -1,4 +1,4 @@ -.. _reference_minorminer: +.. _minorminer_api_ref: ============= API Reference From 6884f0206349a010cb9714fba083eae25394e8e0 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Tue, 4 Mar 2025 10:26:58 -0800 Subject: [PATCH 089/127] Fix SDK build warning for repo (minimal until last rebase) --- docs/source/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index 7507e43e..17b47385 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,4 +1,4 @@ -.. index_minorminer: +.. _index_minorminer: ========== minorminer From 9bf15d146758b81fce14021fdc9866908fc05df9 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Tue, 18 Mar 2025 12:19:49 -0700 Subject: [PATCH 090/127] Update theme and xrefs --- .gitignore | 2 + README.rst | 10 +-- docs/requirements.txt | 5 +- docs/source/conf.py | 160 ++++-------------------------------------- docs/source/index.rst | 35 +++++---- 5 files changed, 40 insertions(+), 172 deletions(-) diff --git a/.gitignore b/.gitignore index ef72516e..be044c46 100644 --- a/.gitignore +++ b/.gitignore @@ -139,6 +139,8 @@ instance/ # Sphinx documentation docs/_build/ +docs/generated/ +docs/*/generated/* # PyBuilder target/ diff --git a/README.rst b/README.rst index f28f7c50..e8bd7ea5 100644 --- a/README.rst +++ b/README.rst @@ -35,10 +35,10 @@ block in research. [1] https://arxiv.org/abs/1406.2741 -Another function, ``find_clique_embedding()``, can be used to find clique embeddings -for Chimera, Pegasus, and Zephyr graphs in polynomial time. It is an implementation -of the algorithm described in [2]. There are additional utilities for finding -biclique embeddings as well. +Another function, ``find_clique_embedding()``, can be used to find clique +embeddings for Chimera, Pegasus, and Zephyr graphs in polynomial time. It is an +implementation of the algorithm described in [2]. There are additional utilities +for finding biclique embeddings as well. [2] https://arxiv.org/abs/1507.04774 @@ -201,7 +201,7 @@ Contributing .. todo:: update links -Ocean's `contributing guide `_ +Ocean's `contributing guide `_ has guidelines for contributing to Ocean packages. If you're interested in adding or modifying parameters of the ``find_embedding`` diff --git a/docs/requirements.txt b/docs/requirements.txt index 651cc8d8..67465f59 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,4 +1,5 @@ breathe>4.25.0 -sphinx-rtd-theme -sphinx>=4.0.0,<5.0.0 cython>=0.28.5 + +pydata-sphinx-theme +sphinx \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index d5c109dd..eede479b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,21 +1,9 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# minorminer documentation build configuration file, created by -# sphinx-quickstart on Wed Nov 15 11:24:18 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# import subprocess import os import sys @@ -23,25 +11,8 @@ sys.path.insert(0, os.path.abspath('.')) sys.path.insert(0, os.path.dirname(config_directory)) -_PY2 = sys.version_info.major == 2 - -# Add __version__, __author__, __authoremail__, __description__ to this namespace -# path_to_package_info = os.path.join( -# '..', '..', 'minorminer', 'package_info.py') -# if _PY2: -# execfile(path_to_package_info) -# else: -# exec(open(path_to_package_info).read()) - # -- General configuration ------------------------------------------------ -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = [ 'sphinx.ext.autosummary', 'sphinx.ext.autodoc', @@ -58,16 +29,8 @@ autosummary_generate = True -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] source_suffix = '.rst' -# The master toctree document. master_doc = 'index' # General information about the project. @@ -75,43 +38,21 @@ copyright = u'2017, D-Wave Systems' author = u'D-Wave Systems' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -# version = __version__ -# The full version, including alpha/beta/rc tags. -# release = __version__ - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None +language = 'en' -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'sdk_index.rst'] +exclude_patterns = ['_build', 'README.rst'] linkcheck_retries = 2 linkcheck_anchors = False linkcheck_ignore = [r'https://cloud.dwavesys.com/leap', # redirects, many checks ] -# The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Breathe configuration ------------------------------------------------ -# Pat to the breathe module itself -# sys.path.append() - # Path to the cpp xml files breathe_projects = {"minorminer": os.path.join( config_directory, '../build-cpp/xml/')} @@ -122,92 +63,19 @@ # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -# html_theme = 'alabaster' -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] - - -def setup(app): - app.add_css_file('cookie_notice.css') - app.add_js_file('cookie_notice.js') - app.add_config_value('target', 'repo', 'env') +html_theme = 'pydata_sphinx_theme' +html_theme_options = { + "collapse_navigation": True, + "show_prev_next": False, +} +html_sidebars = {"**": ["search-field", "sidebar-nav-bs"]} # remove ads # Configuration for intersphinx. intersphinx_mapping = {'python': ('https://docs.python.org/3', None), - 'qbsolv': ('https://docs.ocean.dwavesys.com/projects/qbsolv/en/latest/', None), - 'oceandocs': ('https://docs.ocean.dwavesys.com/en/stable/', None), - 'sysdocs_gettingstarted': ('https://docs.dwavesys.com/docs/latest/', None)} -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'minorminerdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'minorminer.tex', u'minorminer Documentation', - u'D-Wave Systems', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'minorminer', u'minorminer Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'minorminer', u'minorminer Documentation', - author, 'minorminer', 'One line description of project.', - 'Miscellaneous'), -] + 'dwave': ('https://docs.dwavequantum.com/en/latest/', None), + } read_the_docs_build = os.environ.get('READTHEDOCS', None) == 'True' - if read_the_docs_build: subprocess.call('cd ..; make cpp', shell=True) diff --git a/docs/source/index.rst b/docs/source/index.rst index 17b47385..7f746096 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -29,8 +29,6 @@ Examples Introduction ------------ -.. automodule:: minorminer - `minorminer` is a library of tools for finding graph minor embeddings, developed to embed Ising problems onto quantum annealers (QA). While this library can be used to find minors in arbitrary graphs, it is particularly geared towards @@ -43,36 +41,35 @@ multiple embedding algorithms to best fit different problems. Minor-Embedding and QPU Topology ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For an introduction to minor-embedding, see :std:doc:`Minor-Embedding `. +For an introduction to minor-embedding, see the :ref:`qpu_embedding_intro` +section. -For an introduction to the topologies of D-Wave hardware graphs, see -:std:doc:`QPU Topology `. Leap users also have access -to the Exploring Pegasus Jupyter Notebook that explains the architecture of -D-Wave's quantum computer, Advantage, in further detail. +For an introduction to the topologies of D-Wave hardware graphs, see the +:ref:`qpu_topologies` section and the +`Exploring Pegasus Jupyter Notebook `_ +that explains the :term:`Advantage` architecture in further detail. Minor-embedding can be done manually, though typically for very small problems only. For a walkthrough of the manual minor-embedding process, see the -`Constraints Example: Minor-Embedding `_. +:ref:`qpu_example_sat_constrained` section. Minor-Embedding in Ocean -======================== +~~~~~~~~~~~~~~~~~~~~~~~~ -Minor-embedding can also be automated through Ocean. `minorminer` is used by several -:std:doc:`Ocean embedding composites ` -for this purpose. For details on automated (and manual) minor-embedding through -Ocean, see how the `EmbeddingComposite` and `FixedEmbeddingComposite` are used -in this :std:doc:`Boolean AND Gate example `. +Minor-embedding can also be automated through Ocean. `minorminer` is used by +several :ref:`Ocean embedding composites ` for this purpose. +For details on automated (and manual) minor-embedding through +Ocean, see how the :class:`~dwave.system.composites.EmbeddingComposite` and +:class:`~dwave.system.composites.FixedEmbeddingComposite` classes are used +in this :ref:`Boolean AND Gate example `. Once an embedding has been found, D-Wave's Problem Inspector tool can be used to -evaluate its quality. See -:std:doc:`Using the Problem Inspector ` -for more information. +evaluate its quality. See the :ref:`qpu_example_inspector_graph_partitioning` +section for more information. Usage Information ================= -.. todo:: add the intersphinx prefix to enable these links for self builds - * :ref:`index_concepts` for terminology * :ref:`qpu_embedding_intro` for an introduction to minor embedding * :ref:`qpu_embedding_guidance` provides advanced guidance From b79fc8a26fc2d123d085c99b1c923cbe423ff027 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Tue, 18 Mar 2025 13:27:11 -0700 Subject: [PATCH 091/127] Flatten ref structure --- docs/source/api_ref.rst | 13 ++++++++++ .../{reference => }/clique_embedding.rst | 14 +++++------ docs/source/{reference => }/cppdocs.rst | 2 +- .../{reference => }/general_embedding.rst | 24 ++++++++++--------- docs/source/index.rst | 2 +- .../{reference => }/layout_embedding.rst | 22 +++++++++-------- docs/source/reference/api_ref.rst | 13 ---------- 7 files changed, 47 insertions(+), 43 deletions(-) create mode 100644 docs/source/api_ref.rst rename docs/source/{reference => }/clique_embedding.rst (82%) rename docs/source/{reference => }/cppdocs.rst (85%) rename docs/source/{reference => }/general_embedding.rst (82%) rename docs/source/{reference => }/layout_embedding.rst (69%) delete mode 100644 docs/source/reference/api_ref.rst diff --git a/docs/source/api_ref.rst b/docs/source/api_ref.rst new file mode 100644 index 00000000..fb63e9a0 --- /dev/null +++ b/docs/source/api_ref.rst @@ -0,0 +1,13 @@ +.. _minorminer_api_ref: + +============= +API Reference +============= + +.. toctree:: + :maxdepth: 2 + + general_embedding + clique_embedding + layout_embedding + cppdocs diff --git a/docs/source/reference/clique_embedding.rst b/docs/source/clique_embedding.rst similarity index 82% rename from docs/source/reference/clique_embedding.rst rename to docs/source/clique_embedding.rst index 064cc9e2..f6e156d4 100644 --- a/docs/source/reference/clique_embedding.rst +++ b/docs/source/clique_embedding.rst @@ -1,4 +1,4 @@ -.. _clique_embedding: +.. _minorminer_clique_embedding: ================ Clique Embedding @@ -17,10 +17,10 @@ better embeddings than the generic :meth:`~minorminer.find_embedding` method. Caching ======= -If multiple clique or biclique embeddings need to be computed for a single Chimera -or Pegasus graph, it may be more efficient to retrieve these embeddings through -the :class:`busgraph_cache`, which creates LRU file-caches for the target graph's -cliques and bicliques. +If multiple clique or biclique embeddings need to be computed for a single +Chimera or Pegasus graph, it may be more efficient to retrieve these embeddings +through the :class:`busgraph_cache`, which creates LRU file-caches for the +target graph's cliques and bicliques. Class ----- @@ -58,12 +58,12 @@ This example minor embeds a source clique of size 5 into a target Chimera graph. print(embedding) {0: (0, 16, 4), 1: (1, 17, 5), 2: (2, 18, 6), 3: (3, 19, 7), 4: (24, 20, 28)} -.. figure:: ../_images/k5_chimera.png +.. figure:: _images/k5_chimera.png :name: K5_Chimera :width: 800 :alt: Source K5 graph and target Chimera graph -.. figure:: ../_images/clique_embedding.png +.. figure:: _images/clique_embedding.png :name: Embedding_K5_Chimera :width: 800 :alt: Embedding source K5 graph onto target Chimera graph diff --git a/docs/source/reference/cppdocs.rst b/docs/source/cppdocs.rst similarity index 85% rename from docs/source/reference/cppdocs.rst rename to docs/source/cppdocs.rst index 4533cf2b..cd4d46c8 100644 --- a/docs/source/reference/cppdocs.rst +++ b/docs/source/cppdocs.rst @@ -1,4 +1,4 @@ -.. _cppdocs_minorminer: +.. _minorminer_cppdocs: C++ Library =========== diff --git a/docs/source/reference/general_embedding.rst b/docs/source/general_embedding.rst similarity index 82% rename from docs/source/reference/general_embedding.rst rename to docs/source/general_embedding.rst index 217222c4..8aa5d885 100644 --- a/docs/source/reference/general_embedding.rst +++ b/docs/source/general_embedding.rst @@ -1,4 +1,4 @@ -.. _general_embedding: +.. _minorminer_general_embedding: ================= General Embedding @@ -17,7 +17,8 @@ General embedding refers to embedding that may be useful for any type of graph. Examples ======== -This example minor embeds a triangular source K3 graph onto a square target graph. +This example minor embeds a triangular source K3 graph onto a square target +graph. .. code-block:: python @@ -36,13 +37,13 @@ This example minor embeds a triangular source K3 graph onto a square target grap # [[0, 1], [2], [3]] # [[3], [1, 0], [2]] -.. figure:: ../_images/Embedding_TriangularSquare.png +.. figure:: _images/Embedding_TriangularSquare.png :name: Embedding_TriangularSquare :scale: 60 % :alt: Embedding a triangular source graph into a square target graph - Embedding a :math:`K_3` source graph into a square target graph by chaining two - target nodes to represent one source node. + Embedding a :math:`K_3` source graph into a square target graph by chaining + two target nodes to represent one source node. .... @@ -95,13 +96,14 @@ regular graph of degree 3. 4: [11, 24, 13], 5: [2, 14, 26, 5, 3]} -.. figure:: ../_images/Embedding_K6Random3.png +.. figure:: _images/Embedding_K6Random3.png :name: Embedding_K6Random3 :scale: 80 % :alt: Embedding a K6 graph into a 30-node random graph - Embedding a :math:`K_6` source graph (upper left) into a 30-node random target graph of - degree 3 (upper right) by chaining several target nodes to represent one source node (bottom). - The graphic of the embedding clusters chains representing nodes in the source graph: the - cluster of red nodes is a chain of target nodes that represent source node 0, the orange - nodes represent source node 1, and so on. + Embedding a :math:`K_6` source graph (upper left) into a 30-node random + target graph of degree 3 (upper right) by chaining several target nodes to + represent one source node (bottom). The graphic of the embedding clusters + chains representing nodes in the source graph: the cluster of red nodes is a + chain of target nodes that represent source node 0, the orange nodes + represent source node 1, and so on. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 7f746096..98a6502f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,7 +8,7 @@ minorminer :caption: Reference documentation for minorminer: :maxdepth: 1 - reference/api_ref + api_ref About minorminer ================ diff --git a/docs/source/reference/layout_embedding.rst b/docs/source/layout_embedding.rst similarity index 69% rename from docs/source/reference/layout_embedding.rst rename to docs/source/layout_embedding.rst index 15bc206d..fddad167 100644 --- a/docs/source/reference/layout_embedding.rst +++ b/docs/source/layout_embedding.rst @@ -1,15 +1,16 @@ -.. _layout_embedding: +.. _minorminer_layout_embedding: ================ Layout Embedding ================ -:meth:`minorminer.layout.find_embedding()` offers a more specialized approach to -find an embedding through the use of the :class:`~minorminer.layout.layout.Layout` -and :class:`~minorminer.layout.placement.Placement` classes. This kind of -embedding may be useful when the underlying data of your source graph is `spatial -`_. It can also be useful for -embedding graphs with nodes of a low degree (i.e., a cubic graph). +the :meth:`minorminer.layout.find_embedding()` method offers a more specialized +approach to find an embedding through the use of the +:class:`~minorminer.layout.layout.Layout` and +:class:`~minorminer.layout.placement.Placement` classes. This kind of embedding +may be useful when the underlying data of your source graph is +`spatial `_. It can also be +useful for embedding graphs with nodes of a low degree (i.e., a cubic graph). .. autofunction:: minorminer.layout.find_embedding @@ -36,15 +37,16 @@ This example minor embeds a 3x3 grid graph onto a Chimera graph. # One run returned the following embedding: {(0, 0): [13], (1, 0): [0, 8], (0, 1): [9], (1, 1): [12], (0, 2): [14], (1, 2): [10], (2, 0): [7], (2, 1): [11, 3], (2, 2): [15]} -.. figure:: ../_images/grid_chimera.png +.. figure:: _images/grid_chimera.png :name: 2DGrid_Chimera :width: 800 :alt: Source 2-dimensional 3x3 grid graph and a target Chimera graph -.. figure:: ../_images/Layout_Embedding.png +.. figure:: _images/Layout_Embedding.png :name: Layout_Embedding_2DGrid_Chimera :width: 800 - :alt: Embedding a source 2-dimensional 3x3 grid graph onto a target Chimera graph + :alt: Embedding a source 2-dimensional 3x3 grid graph onto a target Chimera + graph .... diff --git a/docs/source/reference/api_ref.rst b/docs/source/reference/api_ref.rst deleted file mode 100644 index 21374312..00000000 --- a/docs/source/reference/api_ref.rst +++ /dev/null @@ -1,13 +0,0 @@ -.. _minorminer_api_ref: - -============= -API Reference -============= - -.. toctree:: - :maxdepth: 2 - - general_embedding - clique_embedding - layout_embedding - cppdocs From 422813c6336a2fd2ea8adcfa20d2767f486c6da5 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Tue, 18 Mar 2025 16:34:55 -0700 Subject: [PATCH 092/127] Fix missing C++ functions --- docs/source/cppdocs.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/source/cppdocs.rst b/docs/source/cppdocs.rst index cd4d46c8..e5d40e40 100644 --- a/docs/source/cppdocs.rst +++ b/docs/source/cppdocs.rst @@ -6,7 +6,7 @@ C++ Library .. toctree:: :maxdepth: 2 - ../cpp/namespacelist - ../cpp/filelist - ../cpp/classlist - ../cpp/structlist + cpp/namespacelist + cpp/filelist + cpp/classlist + cpp/structlist From 14d0c6034449e1d8e9c49db79c7f6e13c0f1fced Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Wed, 19 Mar 2025 06:44:45 -0700 Subject: [PATCH 093/127] Remove the ``find_embedding:189: ERROR: Unexpected indentation`` from multiple package builds --- minorminer/_minorminer.pyx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/minorminer/_minorminer.pyx b/minorminer/_minorminer.pyx index 2991fa83..53aad8fa 100644 --- a/minorminer/_minorminer.pyx +++ b/minorminer/_minorminer.pyx @@ -277,10 +277,11 @@ def find_embedding(S, T, **params): We accomplish this through the following problem transformation for each iterable `blob_j` in ``suspend_chains[i]``, - * Add an auxiliary node `Zij` to both source and target graphs - * Set `fixed_chains[Zij]` = `[Zij]` - * Add the edge `(i,Zij)` to the source graph - * Add the edges `(q,Zij)` to the target graph for each `q` in `blob_j` + + * Add an auxiliary node `Zij` to both source and target graphs + * Set `fixed_chains[Zij]` = `[Zij]` + * Add the edge `(i,Zij)` to the source graph + * Add the edges `(q,Zij)` to the target graph for each `q` in `blob_j` """ cdef _input_parser _in try: From 20b6ad6d89dc1bba13e8347586cdd8d5fcb000be Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Wed, 19 Mar 2025 06:57:05 -0700 Subject: [PATCH 094/127] Update missed path to readme --- docs/source/general_embedding.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/general_embedding.rst b/docs/source/general_embedding.rst index 8aa5d885..fa939840 100644 --- a/docs/source/general_embedding.rst +++ b/docs/source/general_embedding.rst @@ -6,7 +6,7 @@ General Embedding General embedding refers to embedding that may be useful for any type of graph. -.. include:: ../README.rst +.. include:: README.rst :start-after: start_minorminer_about_general_embedding :end-before: end_minorminer_about From eec527f219b551c4feda3d4f2f03603a1a7b71ba Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Thu, 20 Mar 2025 10:13:13 -0700 Subject: [PATCH 095/127] Turn off dot (graphviz) to prevent multiple build warnings --- docs/Doxyfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index 0bdd7841..fa11f754 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -2121,7 +2121,7 @@ PERL_PATH = /usr/bin/perl # powerful graphs. # The default value is: YES. -CLASS_DIAGRAMS = YES +CLASS_DIAGRAMS = NO # You can define message sequence charts within doxygen comments using the \msc # command. Doxygen will then run the mscgen tool (see: @@ -2152,7 +2152,7 @@ HIDE_UNDOC_RELATIONS = YES # set to NO # The default value is: YES. -HAVE_DOT = YES +HAVE_DOT = NO # The DOT_NUM_THREADS specifies the number of dot invocations doxygen is allowed # to run in parallel. When set to 0 doxygen will base this on the number of From 541f471823cd9017dadb92b7ac2137d0987c0833 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Thu, 20 Mar 2025 12:11:30 -0700 Subject: [PATCH 096/127] Remove leftover todo (thanks @thisac) --- README.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.rst b/README.rst index e8bd7ea5..39d21bad 100644 --- a/README.rst +++ b/README.rst @@ -199,8 +199,6 @@ Released under the Apache License 2.0. See ``_ file. Contributing ============ -.. todo:: update links - Ocean's `contributing guide `_ has guidelines for contributing to Ocean packages. From e94fcbc12d4eca59209e9e46b9df37698115b336 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 24 Mar 2025 20:57:00 -0700 Subject: [PATCH 097/127] Fiddle with test_parallel_embeddings to satisfy arbitrary circleCI complaints --- tests/utils/test_parallel_embeddings.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index 2aaff3c4..f85adf5a 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -440,7 +440,3 @@ def test_find_sublattice_embeddings_tile_embedding(self): use_tile_embedding=True, tile_embedding=tile_embedding, ) - - -if __name__ == "__main__": - unittest.main() From 2a099199a989a135f3f3a86efa4b6813df6966e4 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:12:27 -0700 Subject: [PATCH 098/127] Make dwave.embedding optional dependency --- minorminer/utils/parallel_embeddings.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 908bb37c..6b21e123 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -23,7 +23,6 @@ import networkx as nx import numpy as np from typing import Union, Optional, Callable -from dwave.embedding import is_valid_embedding from minorminer.subgraph import find_subgraph @@ -262,12 +261,18 @@ def lattice_size(T: Optional[nx.Graph] = None) -> int: def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): - """Special handling of 1:1 mappings""" + """If dwave.embedding module available check embedding validity. + + With special handling of 1:1 mappings.""" + try: + from dwave.embedding import is_valid_embedding + except ImportError: + return True if one_to_iterable: return is_valid_embedding(emb, S, T) else: return is_valid_embedding({k: (v,) for k, v in emb.items()}, S, T) - + def _mapped_proposal(emb: dict, f: Callable, one_to_iterable: bool = True): if one_to_iterable: From 630e0ee73fbbdd3bc3b6e6a3bc30136bd7c3bcda Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:48:20 -0700 Subject: [PATCH 099/127] basic version of imported function is_valid_embedding --- minorminer/utils/parallel_embeddings.py | 51 ++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 6b21e123..baef2fd3 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -259,6 +259,55 @@ def lattice_size(T: Optional[nx.Graph] = None) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) +def _is_valid_embedding_import_failover(emb: dict, S: dict, T: dict): + """Diagnose a minor embedding. + + Simplified version of dwave.embedding.is_valid_embedding for case that + import unavailable. + """ + + if not hasattr(source, 'edges'): + source = nx.Graph(source) + if not hasattr(target, 'edges'): + target = nx.Graph(target) + + labels = {} + embedded = set() + overlaps = set() + for x in source: + try: + embx = emb[x] + if len(embx) == 0: + return False + except KeyError: + return False + for q in embx: + if q not in target: + return False + elif x not in labels.setdefault(q, {x}): + labels[q].add(x) + overlaps.add(q) + + embedded.add(x) + if not nx.is_connected(target.subgraph(embx)): + return False + + for q in overlaps: + nodes = list(labels[q]) + root = nodes[0] + for x in nodes[1:]: + return False + + yielded = nx.Graph() + for p, q in target.subgraph(labels).edges(): + yielded.add_edges_from((x, y) for x in labels[p] for y in labels[q]) + + for x, y in source.edges(): + if x == y: + continue + if x in embedded and y in embedded and not yielded.has_edge(x, y): + return False + return True def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): """If dwave.embedding module available check embedding validity. @@ -267,7 +316,7 @@ def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = Tru try: from dwave.embedding import is_valid_embedding except ImportError: - return True + is_valid_embedding = _is_valid_embedding_import_failover if one_to_iterable: return is_valid_embedding(emb, S, T) else: From a35eefab81a86681977b7b0898e0b29a4893a1ed Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 24 Mar 2025 21:59:54 -0700 Subject: [PATCH 100/127] Correct variable names in fallover function --- minorminer/utils/parallel_embeddings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index baef2fd3..651aff45 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -259,7 +259,7 @@ def lattice_size(T: Optional[nx.Graph] = None) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) -def _is_valid_embedding_import_failover(emb: dict, S: dict, T: dict): +def _is_valid_embedding_import_failover(emb: dict, source: dict, target: dict): """Diagnose a minor embedding. Simplified version of dwave.embedding.is_valid_embedding for case that From 045ba050c1bbcbfa54025ea604198857e7c1b229 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:27:50 -0700 Subject: [PATCH 101/127] random review: Update minorminer/utils/parallel_embeddings.py Co-authored-by: Radomir Stevanovic --- minorminer/utils/parallel_embeddings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 651aff45..87794bfd 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -103,7 +103,7 @@ def embeddings_to_array(embs: list, node_order=None, as_ndarray=False): return [[emb[v] for v in node_order] for emb in embs] -def array_to_embeddings(embs: list, node_order=None): +def array_to_embeddings(embs: list, node_order: Optional[Iterable] = None) -> list[dict]: """Convert list of embedding lists (values) to dictionary Args: From 554fe4bacb2fa4359144e2daf971afea512ec901 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:31:17 -0700 Subject: [PATCH 102/127] Apply suggestions from code review randomir review implementation Co-authored-by: Radomir Stevanovic --- minorminer/utils/parallel_embeddings.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 87794bfd..28760089 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -118,7 +118,7 @@ def array_to_embeddings(embs: list, node_order: Optional[Iterable] = None) -> li Returns: An embedding dictionary """ - if len(embs) is None: + if not embs: return [] if node_order is None: @@ -240,7 +240,7 @@ def find_multiple_embeddings( return embs -def lattice_size(T: Optional[nx.Graph] = None) -> int: +def lattice_size(T: nx.Graph) -> int: """Determines the cellular (square) dimension of a dwave_networkx lattice The lattice size is the parameter ``m`` of a dwave_networkx graph, also @@ -313,10 +313,7 @@ def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = Tru """If dwave.embedding module available check embedding validity. With special handling of 1:1 mappings.""" - try: - from dwave.embedding import is_valid_embedding - except ImportError: - is_valid_embedding = _is_valid_embedding_import_failover + from minorminer.utils.diagnostic import is_valid_embedding if one_to_iterable: return is_valid_embedding(emb, S, T) else: @@ -424,7 +421,7 @@ def find_sublattice_embeddings( list: A list of disjoint embeddings. """ - timeout = perf_counter() + timeout + timeout_at = perf_counter() + timeout if sublattice_size is None and tile is None: return find_multiple_embeddings( S=S, From e3b8de0ac89e31bc68136e490b09c39b1391d252 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 12:44:36 -0700 Subject: [PATCH 103/127] Implement randomir review recommendations --- minorminer/utils/parallel_embeddings.py | 40 +++++++++++++++++-------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 28760089..6aa70069 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -103,7 +103,9 @@ def embeddings_to_array(embs: list, node_order=None, as_ndarray=False): return [[emb[v] for v in node_order] for emb in embs] -def array_to_embeddings(embs: list, node_order: Optional[Iterable] = None) -> list[dict]: +def array_to_embeddings( + embs: list, node_order: Optional[Iterable] = None +) -> list[dict]: """Convert list of embedding lists (values) to dictionary Args: @@ -116,7 +118,7 @@ def array_to_embeddings(embs: list, node_order: Optional[Iterable] = None) -> li keys). Returns: - An embedding dictionary + A list of embedding dictionaries """ if not embs: return [] @@ -135,7 +137,7 @@ def find_multiple_embeddings( S: nx.Graph, T: nx.Graph, *, - max_num_emb: Optional[int] = 1, + max_num_emb: Union[None, int, float] = 1, use_filter: bool = True, embedder: callable = None, embedder_kwargs: dict = None, @@ -160,7 +162,8 @@ def find_multiple_embeddings( T: The target graph in which to embed. max_num_emb: Maximum number of embeddings to find. Defaults to 1, set to None to find the maximum possible - number. + number. Use of float('Inf') is deprecated and gives equivalent + behaviour to None. use_filter: Specifies whether to check feasibility of embedding arguments independently of the embedder method. In some easy to embed cases use of a filter can slow down operation. @@ -202,10 +205,20 @@ def find_multiple_embeddings( _T = shuffle_graph(T, seed=prng) else: _T = T + if max_num_emb == float("inf"): + warnings.warn( + 'Use of Inf for "max_num_emb" has been deprecated in favor of "None".', + DeprecationWarning, + stacklevel=2, + ) + max_num_emb = None + if max_num_emb is None: max_num_emb = T.number_of_nodes() // S.number_of_nodes() else: - max_num_emb = min(T.number_of_nodes() // S.number_of_nodes(), max_num_emb) + max_num_emb = min( + T.number_of_nodes() // S.number_of_nodes(), round(max_num_emb) + ) if shuffle_all_graphs: _S = shuffle_graph(S, seed=prng) @@ -259,6 +272,7 @@ def lattice_size(T: nx.Graph) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) + def _is_valid_embedding_import_failover(emb: dict, source: dict, target: dict): """Diagnose a minor embedding. @@ -266,9 +280,9 @@ def _is_valid_embedding_import_failover(emb: dict, source: dict, target: dict): import unavailable. """ - if not hasattr(source, 'edges'): + if not hasattr(source, "edges"): source = nx.Graph(source) - if not hasattr(target, 'edges'): + if not hasattr(target, "edges"): target = nx.Graph(target) labels = {} @@ -309,16 +323,18 @@ def _is_valid_embedding_import_failover(emb: dict, source: dict, target: dict): return False return True + def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): - """If dwave.embedding module available check embedding validity. + """If dwave.embedding module available check embedding validity. With special handling of 1:1 mappings.""" from minorminer.utils.diagnostic import is_valid_embedding + if one_to_iterable: return is_valid_embedding(emb, S, T) else: return is_valid_embedding({k: (v,) for k, v in emb.items()}, S, T) - + def _mapped_proposal(emb: dict, f: Callable, one_to_iterable: bool = True): if one_to_iterable: @@ -510,7 +526,7 @@ def find_sublattice_embeddings( seed=seed, embedder=embedder, embedder_kwargs=embedder_kwargs, - timeout=timeout - perf_counter(), + timeout=timeout_at - perf_counter(), ) if len(defect_free_embs) == 0: # If embedding is infeasible on the tile*, it will be infeasible @@ -544,7 +560,7 @@ def find_sublattice_embeddings( sublattice_iter = sublattice_mappings(tile, _T) for f in sublattice_iter: - if perf_counter() > timeout: + if perf_counter() > timeout_at: break if use_tile_embedding: proposal = _mapped_proposal(tile_embedding, f, one_to_iterable) @@ -562,7 +578,7 @@ def find_sublattice_embeddings( seed=seed, embedder=embedder, embedder_kwargs=embedder_kwargs, - timeout=timeout - perf_counter(), + timeout=timeout_at - perf_counter(), ) embs += sub_embs if len(embs) >= max_num_emb: From 7eeaf45c27d71d0c817170cc4088497620194e9b Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 13:52:19 -0700 Subject: [PATCH 104/127] Add test of array_to_embeddings --- minorminer/utils/parallel_embeddings.py | 6 +++--- tests/utils/test_parallel_embeddings.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 6aa70069..f500dcc0 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -16,13 +16,13 @@ onto a target graph or provide supporting functionality. """ import warnings - -from time import perf_counter import time +from time import perf_counter +from typing import Union, Optional, Callable, Iterable, List + import dwave_networkx as dnx import networkx as nx import numpy as np -from typing import Union, Optional, Callable from minorminer.subgraph import find_subgraph diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index f85adf5a..8ec11ec5 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -23,6 +23,7 @@ lattice_size, shuffle_graph, embeddings_to_array, + array_to_embeddings, find_multiple_embeddings, find_sublattice_embeddings, ) @@ -116,6 +117,16 @@ def test_embeddings_to_array(self): with self.assertRaises(KeyError): embeddings_to_array(embs, node_order=node_order) + def test_array_to_embeddings(self): + embs = [{0: 0, 1: 1, "a": 2}, {0: 2, 1: 3, "a": 5}] + node_order = [1, 0, "a"] + arr = embeddings_to_array(embs, node_order=node_order) + embs_out = array_to_embeddings(arr, node_order) + self.assertTrue( + all(embs[i] == embs_out[i] for i in range(len(embs))), + "array_to_embeddings should invert embeddings_to_array", + ) + def test_find_multiple_embeddings_basic(self): square = { ((0, 0), (0, 1)), From 685a06818502106e0c04e542f1f053b2b86c84bd Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 14:05:14 -0700 Subject: [PATCH 105/127] Delete redundant failover code --- minorminer/utils/parallel_embeddings.py | 50 ------------------------- 1 file changed, 50 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index f500dcc0..ca4b2307 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -273,56 +273,6 @@ def lattice_size(T: nx.Graph) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) -def _is_valid_embedding_import_failover(emb: dict, source: dict, target: dict): - """Diagnose a minor embedding. - - Simplified version of dwave.embedding.is_valid_embedding for case that - import unavailable. - """ - - if not hasattr(source, "edges"): - source = nx.Graph(source) - if not hasattr(target, "edges"): - target = nx.Graph(target) - - labels = {} - embedded = set() - overlaps = set() - for x in source: - try: - embx = emb[x] - if len(embx) == 0: - return False - except KeyError: - return False - for q in embx: - if q not in target: - return False - elif x not in labels.setdefault(q, {x}): - labels[q].add(x) - overlaps.add(q) - - embedded.add(x) - if not nx.is_connected(target.subgraph(embx)): - return False - - for q in overlaps: - nodes = list(labels[q]) - root = nodes[0] - for x in nodes[1:]: - return False - - yielded = nx.Graph() - for p, q in target.subgraph(labels).edges(): - yielded.add_edges_from((x, y) for x in labels[p] for y in labels[q]) - - for x, y in source.edges(): - if x == y: - continue - if x in embedded and y in embedded and not yielded.has_edge(x, y): - return False - return True - def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): """If dwave.embedding module available check embedding validity. From 5c2104838a06409579a6bbc1826e9f84909b5f98 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:40:24 -0700 Subject: [PATCH 106/127] Apply suggestions from code review thisac review code suggestions Co-authored-by: Theodor Isacsson --- minorminer/utils/parallel_embeddings.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index ca4b2307..2b6c73f7 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -182,7 +182,7 @@ def find_multiple_embeddings( diversification of the embeddings found. seed: seed for the ``numpy.random.Generator`` controlling shuffling (if invoked). - timeout: total time allowed across all embeddings in seconds. Time elapsed is + timeout: Total time allowed across all embeddings in seconds. Time elapsed is checked before each call to embedder. The total runtime is thereby bounded by timeout plus the time required by one call to embedder. @@ -205,6 +205,7 @@ def find_multiple_embeddings( _T = shuffle_graph(T, seed=prng) else: _T = T + if max_num_emb == float("inf"): warnings.warn( 'Use of Inf for "max_num_emb" has been deprecated in favor of "None".', @@ -233,7 +234,7 @@ def find_multiple_embeddings( emb = [] else: if perf_counter() >= timeout: - emb = [] + break else: if timeout == 0: raise ValueError() @@ -264,7 +265,7 @@ def lattice_size(T: nx.Graph) -> int: T: The target graph in which to embed. The graph must be of type zephyr, pegasus or chimera and constructed by dwave_networkx. Returns: - int: The maximum possible size of a tile + int: The maximum possible size of a tile. """ # Possible feature enhancement, determine a stronger upper bound akin # to lattice_size_lower_bound, accounting for defects, the @@ -275,7 +276,7 @@ def lattice_size(T: nx.Graph) -> int: def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): - """If dwave.embedding module available check embedding validity. + """Check embedding validity. With special handling of 1:1 mappings.""" from minorminer.utils.diagnostic import is_valid_embedding @@ -374,7 +375,7 @@ def find_sublattice_embeddings( for diversification of the embeddings found. seed: seed for the `numpy.random.Generator` controlling shuffling (if invoked). - timeout: total time allowed across all embeddings in seconds. Time elapsed is + timeout: Total time allowed across all embeddings in seconds. Time elapsed is checked before each sublattice search begins, and before each call of the embedder function. The total runtime is thereby bounded by timeout plus the time required by one call to embedder. @@ -452,8 +453,8 @@ def find_sublattice_embeddings( if tile_embedding is not None: if not _is_valid_embedding(tile_embedding, S, tile, one_to_iterable): raise ValueError("tile_embedding is invalid for S and tile") - else: - use_filter = False # Unnecessary + + use_filter = False # Unnecessary if use_tile_embedding is None: use_tile_embedding = tile == S # Trivial 1:1 @@ -483,6 +484,7 @@ def find_sublattice_embeddings( # on all subgraphs thereof (assuming sufficient time was provided # in embedder_kwargs) return [] + if use_tile_embedding: tile_embedding = defect_free_embs[0] # Apply sufficient restriction on the tile From a62934c1cf6a7b4691eef3a7db3f4853978ce023 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 25 Mar 2025 16:03:26 -0700 Subject: [PATCH 107/127] implement thisac review --- minorminer/utils/parallel_embeddings.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 2b6c73f7..3b3ccb5d 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -169,7 +169,8 @@ def find_multiple_embeddings( easy to embed cases use of a filter can slow down operation. embedder: Specifies the embedding search method, a callable taking ``S``, ``T``, as the first two parameters. Defaults to - ``minorminer.subgraph.find_subgraph``. + ``minorminer.subgraph.find_subgraph``. When the embedder fails it is + expected to return an empty dictionary. embedder_kwargs: Additional arguments for the embedder beyond ``S`` and ``T``. one_to_iterable: Determines how embedding mappings are @@ -192,8 +193,12 @@ def find_multiple_embeddings( map from the source to the target graph as a dictionary without reusing target variables. """ - timeout = perf_counter() + timeout embs = [] + if timeout <= 0: + return embs + + timeout_at = perf_counter() + timeout + if embedder is None: embedder = find_subgraph if embedder_kwargs is None: @@ -231,13 +236,11 @@ def find_multiple_embeddings( use_filter and embedding_feasibility_filter(_S, _T, not one_to_iterable) is False ): - emb = [] + emb = {} else: - if perf_counter() >= timeout: + if perf_counter() >= timeout_at: break else: - if timeout == 0: - raise ValueError() emb = embedder(_S, _T, **embedder_kwargs) if len(emb) == 0: @@ -274,7 +277,6 @@ def lattice_size(T: nx.Graph) -> int: return max(T.graph.get("rows"), T.graph.get("columns")) - def _is_valid_embedding(emb: dict, S: dict, T: dict, one_to_iterable: bool = True): """Check embedding validity. @@ -361,6 +363,7 @@ def find_sublattice_embeddings( embedder: Specifies the embedding search method, a callable taking ``S``, ``T`` as the first two arguments. Defaults to minorminer.subgraph.find_subgraph. Note that if `one_to_iterable` should be adjusted to match the return type. + When the embedder fails it is expected to return an empty dictionary. embedder_kwargs: Dictionary specifying arguments for the embedder other than ``S``, ``T``. one_to_iterable: Specifies whether the embedder returns (and/or tile_embedding is) @@ -387,7 +390,8 @@ def find_sublattice_embeddings( Returns: list: A list of disjoint embeddings. """ - + if timeout <= 0: + return [] timeout_at = perf_counter() + timeout if sublattice_size is None and tile is None: return find_multiple_embeddings( From db8b95cd3e7aef18913bbe044b9e9c2734b49bd8 Mon Sep 17 00:00:00 2001 From: Joel Pasvolsky Date: Fri, 21 Mar 2025 12:53:16 -0700 Subject: [PATCH 108/127] Remove obsolete doxygen configuration --- docs/Doxyfile | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/Doxyfile b/docs/Doxyfile index fa11f754..c70acc56 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -234,7 +234,7 @@ ALIASES = # A mapping has the form "name=value". For example adding "class=itcl::class" # will allow you to use the command class in the itcl::class meaning. -TCL_SUBST = +# TCL_SUBST = # Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources # only. Doxygen will then generate output that is more tailored for C. For @@ -1043,7 +1043,7 @@ ALPHABETICAL_INDEX = YES # Minimum value: 1, maximum value: 20, default value: 5. # This tag requires that the tag ALPHABETICAL_INDEX is set to YES. -COLS_IN_ALPHA_INDEX = 5 +# COLS_IN_ALPHA_INDEX = 5 # In case all classes in a project start with a common prefix, all classes will # be put under the same header in the alphabetical index. The IGNORE_PREFIX tag @@ -2108,7 +2108,7 @@ EXTERNAL_PAGES = YES # interpreter (i.e. the result of 'which perl'). # The default file (with absolute path) is: /usr/bin/perl. -PERL_PATH = /usr/bin/perl +# PERL_PATH = /usr/bin/perl #--------------------------------------------------------------------------- # Configuration options related to the dot tool @@ -2130,7 +2130,7 @@ CLASS_DIAGRAMS = NO # the mscgen tool resides. If left empty the tool is assumed to be found in the # default search path. -MSCGEN_PATH = +# MSCGEN_PATH = # You can include diagrams made with dia in doxygen documentation. Doxygen will # then run dia to produce the diagram and insert it in the documentation. The From 219fcf43f8d0c5d9a54506e91f707500be30a705 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 26 Mar 2025 01:10:31 +0100 Subject: [PATCH 109/127] Release 0.2.18 --- minorminer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/__init__.py b/minorminer/__init__.py index 8af570cf..267feeea 100644 --- a/minorminer/__init__.py +++ b/minorminer/__init__.py @@ -16,4 +16,4 @@ from minorminer.minorminer import miner, VARORDER, find_embedding -__version__ = "0.2.17" +__version__ = "0.2.18" From 92e227a465fe0ed879e08ccadf4fb20216de04d6 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:11:32 -0700 Subject: [PATCH 110/127] Bugfix: replace full graph by local decimated graph for use tiling branch with testing derived from TilingComposite --- minorminer/utils/parallel_embeddings.py | 13 +++++++------ tests/utils/test_parallel_embeddings.py | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 3b3ccb5d..136801e4 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -334,11 +334,12 @@ def find_sublattice_embeddings( T: The target graph in which to embed. If raster_embedding is not None, the graph must be of type zephyr, pegasus, or chimera and constructed by dwave_networkx. - tile: A mask applied to the target graph ``T`` defining a restricted - space in which to search for embeddings; the tile family - should match that of ``T``. If the tile is not - provided, it is generated as a fully yielded square sublattice - of ``T``, with ``m=sublattice_size``. + tile: A dwave_networkx compatible mask applied to the target graph ``T`` + defining a sublattice in which to search for embeddings. + If the tile is not provided, it is generated as a fully yielded square + sublattice of ``T`` of the same family (chimera, pegasus or zephyr), with + ``m=sublattice_size``. Aside from same-family tiles, chimera tiles + can be embedded on pegasus and zephyr target graphs. If ``tile==S``, ``embedder`` can be ignored since a 1:1 mapping is necessary on each subgraph and easily verified by checking the mask is complete. @@ -520,7 +521,7 @@ def find_sublattice_embeddings( break if use_tile_embedding: proposal = _mapped_proposal(tile_embedding, f, one_to_iterable) - if _is_valid_embedding(proposal, S, T, one_to_iterable): + if _is_valid_embedding(proposal, S, _T, one_to_iterable): sub_embs = [proposal] else: sub_embs = [] diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index 8ec11ec5..120261d2 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -451,3 +451,26 @@ def test_find_sublattice_embeddings_tile_embedding(self): use_tile_embedding=True, tile_embedding=tile_embedding, ) + + def test_chimera_on_pegasus(self): + # see test_composite_multi_cell associated to the TilingComposite. + # I plan to replace this composite with a ParallelEmbeddingComposite shortly. + + # Embedding Chimera[m=2, n=3, t=4] chimera over a Pegasus[m=8] should be supported + # This is more challenging than other tests because candidate subgraphs are not disjointed + # and cannot make use of all target nodes. + m = 8 + T = dnx.pegasus_graph(m) + m_sub = 2 + n_sub = 3 + tile = S = dnx.chimera_graph(m=m_sub, n=n_sub) + embedder_kwargs = { + "tile": tile, + "max_num_emb": None, + "use_tile_embedding": True, + } + embeddings = find_sublattice_embeddings(S, T, **embedder_kwargs) + + m_nice = n_nice = m - 1 # A nice subgraph is relevant to Chimera sublattices: + expected_number_of_cells = (m_nice // m_sub) * (n_nice // 3) * 3 + self.assertEqual(len(embeddings), expected_number_of_cells) From 9ed506ab0af5634892f3516431a4e53ad8292cf5 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Mon, 28 Apr 2025 10:37:45 -0700 Subject: [PATCH 111/127] Minor changes to docstrings and defaulting for compatibility/comprehensibility --- minorminer/utils/parallel_embeddings.py | 31 ++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 136801e4..9b14c9b3 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -338,11 +338,10 @@ def find_sublattice_embeddings( defining a sublattice in which to search for embeddings. If the tile is not provided, it is generated as a fully yielded square sublattice of ``T`` of the same family (chimera, pegasus or zephyr), with - ``m=sublattice_size``. Aside from same-family tiles, chimera tiles - can be embedded on pegasus and zephyr target graphs. - If ``tile==S``, ``embedder`` can be ignored since a 1:1 mapping is - necessary on each subgraph and easily verified by checking the - mask is complete. + ``m=sublattice_size``. See minorminer + documentation for sublattice mappings (zephyr, pegasus, or chimera) for + details on compatible combinations. For example, chimera tiles can be used + in combination with pegasus and zephyr target graphs. sublattice_size: Parameterizes the tile when it is not provided as an input argument: defines the number of rows and columns of a square sublattice (parameter m of the dwave_networkx graph @@ -358,8 +357,11 @@ def find_sublattice_embeddings( reused for a maximum number of sublattices. If, due to defects or overlap with an existing assignment, the embedding fails at a particular offset embedder is not invoked. + By default the setting is False unless ``tile==S`` or a tile_embedding + is provided. tile_embedding: When provided, this should be an embedding from the source - to the tile. If not provided it is generated by the embedder method. Note + to the tile. If not provided it is generated by the embedder method, or + fixed as a 1:1 mapping (in the special case that S==tile). Note that `one_to_iterable` should be adjusted to match the value type. embedder: Specifies the embedding search method, a callable taking ``S``, ``T`` as the first two arguments. Defaults to minorminer.subgraph.find_subgraph. Note @@ -455,17 +457,23 @@ def find_sublattice_embeddings( "source graphs must a graph constructed by " "dwave_networkx as chimera, pegasus or zephyr type" ) + if tile_embedding is not None: if not _is_valid_embedding(tile_embedding, S, tile, one_to_iterable): raise ValueError("tile_embedding is invalid for S and tile") - use_filter = False # Unnecessary if use_tile_embedding is None: - use_tile_embedding = tile == S # Trivial 1:1 - if use_tile_embedding and tile_embedding is None: - tile_embedding = {i: i for i in tile.nodes} - use_filter = False # Unnecessary + use_tile_embedding = tile == S or tile_embedding is not None # Trivial 1:1 + + if use_tile_embedding and tile_embedding is None and S == tile: + if one_to_iterable: + tile_embedding = { + i: (i,) for i in tile.nodes + } # 1 to 1 mapping is sufficient + else: + tile_embedding = {i: i for i in tile.nodes} # 1 to 1 mapping is sufficient + use_filter = False # Unnecessary if use_filter or (use_tile_embedding and tile_embedding is None): # Check defect-free tile embedding is viable @@ -497,6 +505,7 @@ def find_sublattice_embeddings( tile = tile.subgraph({v for c in tile_embedding.values() for v in c}) else: tile = tile.subgraph({v for v in tile_embedding.values()}) + if max_num_emb is None: max_num_emb = T.number_of_nodes() // S.number_of_nodes() else: From 4c5515bb660c7cee0c63596db97bf2e139600531 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:06:16 -0700 Subject: [PATCH 112/127] implement review changes to docstrings --- minorminer/utils/parallel_embeddings.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 9b14c9b3..3e1281c4 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -334,14 +334,13 @@ def find_sublattice_embeddings( T: The target graph in which to embed. If raster_embedding is not None, the graph must be of type zephyr, pegasus, or chimera and constructed by dwave_networkx. - tile: A dwave_networkx compatible mask applied to the target graph ``T`` - defining a sublattice in which to search for embeddings. + tile: A graph constructed by dwave_networkx applied as a displaceable mask + to the target graph ``T``. The embedding search is limited to the mask. If the tile is not provided, it is generated as a fully yielded square - sublattice of ``T`` of the same family (chimera, pegasus or zephyr), with - ``m=sublattice_size``. See minorminer - documentation for sublattice mappings (zephyr, pegasus, or chimera) for - details on compatible combinations. For example, chimera tiles can be used - in combination with pegasus and zephyr target graphs. + sublattice of ``T`` of the same type (chimera, pegasus or zephyr), with + ``m=sublattice_size``. See minorminer sublattice mapping documentation + for details on compatible combinations. For example, chimera tiles + can be used in combination with pegasus and zephyr target graphs. sublattice_size: Parameterizes the tile when it is not provided as an input argument: defines the number of rows and columns of a square sublattice (parameter m of the dwave_networkx graph From 19dff7b696eabdaac8217a28bb142f673328202d Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Sun, 4 May 2025 00:03:38 -0700 Subject: [PATCH 113/127] Allow dwave_networkx to be passed as kwargs rather than in T --- minorminer/utils/parallel_embeddings.py | 71 ++++++++++++++++++------- tests/utils/test_parallel_embeddings.py | 68 ++++++++++++++++++++++- 2 files changed, 119 insertions(+), 20 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 3e1281c4..8dd2b6cc 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -296,9 +296,31 @@ def _mapped_proposal(emb: dict, f: Callable, one_to_iterable: bool = True): return {k: f(n) for k, n in emb.items()} +def _local_graph(T=None, T_family=None, T_kwargs=None): + if T_family is None: + if T is None: + raise ValueError( + "Either the target graph T, or its lattice specication T_family and T_kwargs, must be provided" + ) + return T.copy() + else: + if T_kwargs is None: + T_kwargs = {} + if T is not None: + T_kwargs.update({"edge_list": T.edges, "node_list": T.nodes}) + if T_family == "chimera": + return dnx.chimera_graph(**T_kwargs) + elif T_family == "pegasus": + return dnx.pegasus_graph(**T_kwargs) + elif T_family == "zephyr": + return dnx.zephyr_graph(**T_kwargs) + else: + raise ValueError("Unsupported dwave_networkx graph") + + def find_sublattice_embeddings( S: nx.Graph, - T: nx.Graph, + T: Optional[nx.Graph], *, tile: nx.Graph = None, sublattice_size: int = None, @@ -313,6 +335,8 @@ def find_sublattice_embeddings( shuffle_all_graphs: bool = False, shuffle_sublattice_order: bool = False, timeout: float = float("Inf"), + T_family: Optional[str] = None, + T_kwargs: Optional[dict] = None, ) -> list: """Searches for embeddings on sublattices of the target graph. @@ -331,9 +355,10 @@ def find_sublattice_embeddings( Args: S: The source graph to embed. - T: The target graph in which to embed. If - raster_embedding is not None, the graph must be of type zephyr, - pegasus, or chimera and constructed by dwave_networkx. + T: The target graph in which to embed. Either the graph should be of type + chimera, pegasus or zephyr and generated by dwave_networkx, or the + graph must be cast to one of these types using `T_family` and `T_kwargs`. + If `T` is None, then `T` is generated as a defect free graph. tile: A graph constructed by dwave_networkx applied as a displaceable mask to the target graph ``T``. The embedding search is limited to the mask. If the tile is not provided, it is generated as a fully yielded square @@ -345,7 +370,8 @@ def find_sublattice_embeddings( as an input argument: defines the number of rows and columns of a square sublattice (parameter m of the dwave_networkx graph family matching T). ``lattice_size_lower_bound()`` - provides a lower bound based on a fast feasibility filter. + provides a lower bound based on a fast feasibility filter. Note that + the tile is created with default graph parameters (integer labeled variables). max_num_emb: Maximum number of embeddings to find. Defaults to 1, set to None for unbounded (try unlimited search on all lattice offsets). @@ -384,9 +410,14 @@ def find_sublattice_embeddings( checked before each sublattice search begins, and before each call of the embedder function. The total runtime is thereby bounded by timeout plus the time required by one call to embedder. - + T_family: 'chimera', 'pegasus' or 'zephyr' arguments result in the target graph + being cast to the corresponding dwave_networkx graph family. + T_kwargs: The arguments passed to the graph generator. It is necessary to specify the + shape parameters m (rows) and perhaps n (columns) or t (tile). + If T is not None, `T_kwargs` is updated with the nodelist and edgelist of T. Raises: - ValueError: If the target graph ``T`` is not of type zephyr, pegasus, or chimera. + ValueError: If the target graph ``T`` is not of type zephyr, pegasus, or chimera and + T_family, T_kwargs are not specified. If the tile_embedding is incompatible with the source graph and tile. Returns: @@ -404,21 +435,27 @@ def find_sublattice_embeddings( embedder=embedder, embedder_kwargs=embedder_kwargs, ) + if T.graph.get("family", T_family) not in {"chimera", "pegasus", "zephyr"}: + raise ValueError( + "If T is not a dwave_networkx graph, T_family must be specified" + ) + if max_num_emb == 1 and seed is None and T_family is None: + _T = T # Graph is unmodified. + else: + _T = _local_graph(T, T_family, T_kwargs) if use_filter and tile is None: feasibility_bound = lattice_size_lower_bound( - S=S, T=T, one_to_one=not one_to_iterable + S=S, T=_T, one_to_one=not one_to_iterable ) if feasibility_bound is None or sublattice_size < feasibility_bound: warnings.warn("sublattice_size < lower bound: embeddings will be empty.") return [] - # A possible feature enhancement might allow for sublattice_size (m) to be - # replaced by shape: (m,t) [zephyr] or (m,n,t) [Chimera] - family = T.graph.get("family") + family = _T.graph.get("family") if family == "chimera": sublattice_mappings = dnx.chimera_sublattice_mappings - t = T.graph["tile"] + t = _T.graph["tile"] if tile is None: tile = dnx.chimera_graph(m=sublattice_size, n=sublattice_size, t=t) elif ( @@ -441,7 +478,7 @@ def find_sublattice_embeddings( elif family == "zephyr": sublattice_mappings = dnx.zephyr_sublattice_mappings - t = T.graph["tile"] + t = _T.graph["tile"] if tile is None: tile = dnx.zephyr_graph(m=sublattice_size, t=t) elif ( @@ -506,15 +543,11 @@ def find_sublattice_embeddings( tile = tile.subgraph({v for v in tile_embedding.values()}) if max_num_emb is None: - max_num_emb = T.number_of_nodes() // S.number_of_nodes() + max_num_emb = _T.number_of_nodes() // S.number_of_nodes() else: - max_num_emb = min(T.number_of_nodes() // S.number_of_nodes(), max_num_emb) + max_num_emb = min(_T.number_of_nodes() // S.number_of_nodes(), max_num_emb) embs = [] - if max_num_emb == 1 and seed is None: - _T = T - else: - _T = T.copy() if shuffle_all_graphs or shuffle_sublattice_order: prng = np.random.default_rng(seed) diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index 120261d2..b31ff35c 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -12,8 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import unittest -import numpy as np +from itertools import product +import numpy as np import networkx as nx import dwave_networkx as dnx @@ -260,6 +261,7 @@ def test_find_multiple_embeddings_advanced(self): def test_find_sublattice_embeddings_basic(self): # defaults and basic arguments + use_Ts = [True, False] for topology in ["chimera", "pegasus", "zephyr"]: if topology == "chimera": min_sublattice_size = 1 @@ -474,3 +476,67 @@ def test_chimera_on_pegasus(self): m_nice = n_nice = m - 1 # A nice subgraph is relevant to Chimera sublattices: expected_number_of_cells = (m_nice // m_sub) * (n_nice // 3) * 3 self.assertEqual(len(embeddings), expected_number_of_cells) + + def test_T_family_T_kwargs(self): + # Like test basic, but pass family information + with_Ts = [True, False] + with_Tfamily = [True, False] + for topology, with_T, with_Tfamily in product( + ["chimera", "pegasus", "zephyr"], with_Ts, with_Tfamily + ): + T_family = topology + if topology == "chimera": + min_sublattice_size = 1 + S = dnx.chimera_graph(min_sublattice_size) + T = dnx.chimera_graph(min_sublattice_size + 1) + num_emb = 4 + tile = None + T_kwargs = {} + elif topology == "pegasus": + min_sublattice_size = 2 + S = dnx.pegasus_graph(min_sublattice_size, nice_coordinates=True) + tile = S + T = dnx.pegasus_graph(min_sublattice_size + 1, nice_coordinates=True) + num_emb = 2 + T_kwargs = { + "nice_coordinates": True, + "edge_list": list(T.edges), + "node_list": list(T.nodes), + } + elif topology == "zephyr": + min_sublattice_size = 1 + S = dnx.zephyr_graph(min_sublattice_size, coordinates=True) + tile = S + T = dnx.zephyr_graph(min_sublattice_size + 1, coordinates=True) + num_emb = 2 + T_kwargs = {"coordinates": True} + if with_Tfamily is False: + T_family = None + T_kwargs = None # Ignored in any case. + else: + # Cast T as standard nx.Graph; make sure propagation is correct: + T_kwargs["m"] = T.graph.get("rows") + T = nx.from_edgelist(T.edges) + if with_Ts is False: + T = None + + if T is None and T_family is None: + with self.assertRaises(ValueError): + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=min_sublattice_size, + T_family=T_family, + T_kwargs=T_kwargs, + tile=tile, + ) + else: + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=min_sublattice_size, + T_family=T_family, + T_kwargs=T_kwargs, + tile=tile, + ) + self.assertEqual(len(embs), 1, "mismatched number of embeddings") From 8af68329f162c13820af846b7811000091ba0c4e Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Wed, 4 Jun 2025 14:01:54 -0700 Subject: [PATCH 114/127] Bugfix: remove None test for required List argument --- minorminer/utils/parallel_embeddings.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 8dd2b6cc..98a3fada 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -104,12 +104,12 @@ def embeddings_to_array(embs: list, node_order=None, as_ndarray=False): def array_to_embeddings( - embs: list, node_order: Optional[Iterable] = None + embs: Iterable, node_order: Optional[Iterable] = None ) -> list[dict]: """Convert list of embedding lists (values) to dictionary Args: - embs: A list of embeddings, each embedding is a list where values are + embs: An iterable of embeddings, each embedding is a list where values are chains in node order. node_order: An iterable giving the ordering of variables in each row. When not provided variables are ordered to @@ -120,8 +120,6 @@ def array_to_embeddings( Returns: A list of embedding dictionaries """ - if not embs: - return [] if node_order is None: node_order = range(len(embs[0])) From 6a0677656c610f18929806225881dfc52207edfe Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Wed, 4 Jun 2025 16:03:38 -0700 Subject: [PATCH 115/127] Apply suggestions from code review Co-authored-by: Radomir Stevanovic --- minorminer/utils/parallel_embeddings.py | 4 ++-- tests/utils/test_parallel_embeddings.py | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 98a3fada..6de9f05b 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -104,7 +104,7 @@ def embeddings_to_array(embs: list, node_order=None, as_ndarray=False): def array_to_embeddings( - embs: Iterable, node_order: Optional[Iterable] = None + embs: Iterable[list], node_order: Optional[Iterable] = None ) -> list[dict]: """Convert list of embedding lists (values) to dictionary @@ -318,7 +318,7 @@ def _local_graph(T=None, T_family=None, T_kwargs=None): def find_sublattice_embeddings( S: nx.Graph, - T: Optional[nx.Graph], + T: Optional[nx.Graph] = None, *, tile: nx.Graph = None, sublattice_size: int = None, diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index b31ff35c..457fb96b 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -477,13 +477,11 @@ def test_chimera_on_pegasus(self): expected_number_of_cells = (m_nice // m_sub) * (n_nice // 3) * 3 self.assertEqual(len(embeddings), expected_number_of_cells) - def test_T_family_T_kwargs(self): + @parameterized.expand( + product(["chimera", "pegasus", "zephyr"], [False, True], [False, True]) + ) + def test_T_family_T_kwargs(self, topology, with_T, with_Tfamily): # Like test basic, but pass family information - with_Ts = [True, False] - with_Tfamily = [True, False] - for topology, with_T, with_Tfamily in product( - ["chimera", "pegasus", "zephyr"], with_Ts, with_Tfamily - ): T_family = topology if topology == "chimera": min_sublattice_size = 1 @@ -517,7 +515,7 @@ def test_T_family_T_kwargs(self): # Cast T as standard nx.Graph; make sure propagation is correct: T_kwargs["m"] = T.graph.get("rows") T = nx.from_edgelist(T.edges) - if with_Ts is False: + if with_T is False: T = None if T is None and T_family is None: From 4db164eea720b16e7b2357b3f23e19c71eecf095 Mon Sep 17 00:00:00 2001 From: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Date: Thu, 5 Jun 2025 07:01:15 -0700 Subject: [PATCH 116/127] Fix Optional type hinting, fix handling of T=None code and tests --- minorminer/utils/parallel_embeddings.py | 29 +++++--- tests/utils/test_parallel_embeddings.py | 97 +++++++++++++------------ 2 files changed, 66 insertions(+), 60 deletions(-) diff --git a/minorminer/utils/parallel_embeddings.py b/minorminer/utils/parallel_embeddings.py index 6de9f05b..3b86649a 100644 --- a/minorminer/utils/parallel_embeddings.py +++ b/minorminer/utils/parallel_embeddings.py @@ -33,7 +33,8 @@ def shuffle_graph( - G: nx.Graph, seed: Union[int, np.random.RandomState, np.random.Generator] = None + G: nx.Graph, + seed: Optional[Union[int, np.random.RandomState, np.random.Generator]] = None, ) -> nx.Graph: """Shuffle the node and edge ordering of a networkx graph. @@ -137,11 +138,11 @@ def find_multiple_embeddings( *, max_num_emb: Union[None, int, float] = 1, use_filter: bool = True, - embedder: callable = None, - embedder_kwargs: dict = None, + embedder: Optional[callable] = None, + embedder_kwargs: Optional[dict] = None, one_to_iterable: bool = False, shuffle_all_graphs: bool = False, - seed: Union[int, np.random.RandomState, np.random.Generator] = None, + seed: Optional[Union[int, np.random.RandomState, np.random.Generator]] = None, timeout: float = float("Inf"), ) -> list: """Finds multiple disjoint embeddings of a source graph onto a target graph @@ -320,15 +321,15 @@ def find_sublattice_embeddings( S: nx.Graph, T: Optional[nx.Graph] = None, *, - tile: nx.Graph = None, - sublattice_size: int = None, - max_num_emb: Optional[int] = 1, + tile: Optional[nx.Graph] = None, + sublattice_size: Optional[int] = None, + max_num_emb: int = 1, use_filter: bool = True, use_tile_embedding: Optional[bool] = None, tile_embedding: Optional[dict] = None, - seed: Union[int, np.random.RandomState, np.random.Generator] = None, - embedder: callable = None, - embedder_kwargs: dict = None, + seed: Optional[Union[int, np.random.RandomState, np.random.Generator]] = None, + embedder: Optional[callable] = None, + embedder_kwargs: Optional[dict] = None, one_to_iterable: bool = False, shuffle_all_graphs: bool = False, shuffle_sublattice_order: bool = False, @@ -433,11 +434,15 @@ def find_sublattice_embeddings( embedder=embedder, embedder_kwargs=embedder_kwargs, ) - if T.graph.get("family", T_family) not in {"chimera", "pegasus", "zephyr"}: + if T is not None and T.graph.get("family", T_family) not in { + "chimera", + "pegasus", + "zephyr", + }: raise ValueError( "If T is not a dwave_networkx graph, T_family must be specified" ) - if max_num_emb == 1 and seed is None and T_family is None: + if T is not None and max_num_emb == 1 and seed is None and T_family is None: _T = T # Graph is unmodified. else: _T = _local_graph(T, T_family, T_kwargs) diff --git a/tests/utils/test_parallel_embeddings.py b/tests/utils/test_parallel_embeddings.py index 457fb96b..df3d84cb 100644 --- a/tests/utils/test_parallel_embeddings.py +++ b/tests/utils/test_parallel_embeddings.py @@ -13,6 +13,7 @@ # limitations under the License. import unittest from itertools import product +from parameterized import parameterized import numpy as np import networkx as nx @@ -482,53 +483,44 @@ def test_chimera_on_pegasus(self): ) def test_T_family_T_kwargs(self, topology, with_T, with_Tfamily): # Like test basic, but pass family information - T_family = topology - if topology == "chimera": - min_sublattice_size = 1 - S = dnx.chimera_graph(min_sublattice_size) - T = dnx.chimera_graph(min_sublattice_size + 1) - num_emb = 4 - tile = None - T_kwargs = {} - elif topology == "pegasus": - min_sublattice_size = 2 - S = dnx.pegasus_graph(min_sublattice_size, nice_coordinates=True) - tile = S - T = dnx.pegasus_graph(min_sublattice_size + 1, nice_coordinates=True) - num_emb = 2 - T_kwargs = { - "nice_coordinates": True, - "edge_list": list(T.edges), - "node_list": list(T.nodes), - } - elif topology == "zephyr": - min_sublattice_size = 1 - S = dnx.zephyr_graph(min_sublattice_size, coordinates=True) - tile = S - T = dnx.zephyr_graph(min_sublattice_size + 1, coordinates=True) - num_emb = 2 - T_kwargs = {"coordinates": True} - if with_Tfamily is False: - T_family = None - T_kwargs = None # Ignored in any case. - else: - # Cast T as standard nx.Graph; make sure propagation is correct: - T_kwargs["m"] = T.graph.get("rows") - T = nx.from_edgelist(T.edges) - if with_T is False: - T = None - - if T is None and T_family is None: - with self.assertRaises(ValueError): - embs = find_sublattice_embeddings( - S, - T, - sublattice_size=min_sublattice_size, - T_family=T_family, - T_kwargs=T_kwargs, - tile=tile, - ) - else: + T_family = topology + if topology == "chimera": + min_sublattice_size = 1 + S = dnx.chimera_graph(min_sublattice_size) + T = dnx.chimera_graph(min_sublattice_size + 1) + num_emb = 4 + tile = None + T_kwargs = {} + elif topology == "pegasus": + min_sublattice_size = 2 + S = dnx.pegasus_graph(min_sublattice_size, nice_coordinates=True) + tile = S + T = dnx.pegasus_graph(min_sublattice_size + 1, nice_coordinates=True) + num_emb = 2 + T_kwargs = { + "nice_coordinates": True, + "edge_list": list(T.edges), + "node_list": list(T.nodes), + } + elif topology == "zephyr": + min_sublattice_size = 1 + S = dnx.zephyr_graph(min_sublattice_size, coordinates=True) + tile = S + T = dnx.zephyr_graph(min_sublattice_size + 1, coordinates=True) + num_emb = 2 + T_kwargs = {"coordinates": True} + if with_Tfamily is False: + T_family = None + T_kwargs = None # Ignored in any case. + else: + # Cast T as standard nx.Graph; make sure propagation is correct: + T_kwargs["m"] = T.graph.get("rows") + T = nx.from_edgelist(T.edges) + if with_T is False: + T = None + + if T is None and T_family is None: + with self.assertRaises(ValueError): embs = find_sublattice_embeddings( S, T, @@ -537,4 +529,13 @@ def test_T_family_T_kwargs(self, topology, with_T, with_Tfamily): T_kwargs=T_kwargs, tile=tile, ) - self.assertEqual(len(embs), 1, "mismatched number of embeddings") + else: + embs = find_sublattice_embeddings( + S, + T, + sublattice_size=min_sublattice_size, + T_family=T_family, + T_kwargs=T_kwargs, + tile=tile, + ) + self.assertEqual(len(embs), 1, "mismatched number of embeddings") From 04171df0b4b2cecca97a611a7148a4ad6ede68fc Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 5 Jun 2025 21:17:16 +0200 Subject: [PATCH 117/127] Release 0.2.19 ### New Features - Allow lattice type and lattice dimensions to be passed as options for `find_sublattice_embeddings()`. See [\#266](https://github.com/dwavesystems/minorminer/pull/266). ### Bug Fixes - Fix `find_sublattice_embeddings()` to not produce non-disjoint embeddings when `use_tile_embedding=True` is set. See [\#265](https://github.com/dwavesystems/minorminer/pull/265). --- minorminer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/__init__.py b/minorminer/__init__.py index 267feeea..00404736 100644 --- a/minorminer/__init__.py +++ b/minorminer/__init__.py @@ -16,4 +16,4 @@ from minorminer.minorminer import miner, VARORDER, find_embedding -__version__ = "0.2.18" +__version__ = "0.2.19" From ac17efb4254631f6633d661e688d085646a54ee9 Mon Sep 17 00:00:00 2001 From: Kelly Boothby Date: Thu, 11 Sep 2025 11:03:37 -0700 Subject: [PATCH 118/127] Added support for colorings in Glasgow subgraph solver (#272) * Added support for colorings in Glasgow subgraph solver * updated Glasgow to latest version (with patches for c++17 support) * new parameters for subgraph.find_subgraph * as_embedding=True will produce a dict with tuple values * node_labels to be respected by GSS, node labels must match * edge_labels as with node_labels; directed edge labels match --- MANIFEST.in | 2 +- .../{boost/thread/barrier.hpp => barrier} | 8 +- external/boost/BOOST_LICENSE | 23 ---- external/boost/bimap.hpp | 44 ------- external/boost/bimap/unordered_set_of.hpp | 27 ---- external/boost/container/allocator.hpp | 25 ---- external/boost/functional/hash.hpp | 33 ----- external/boost/iostreams/device/file.hpp | 32 ----- external/boost/iostreams/filter/bzip2.hpp | 29 ---- external/boost/iostreams/filtering_stream.hpp | 36 ----- external/boost/iostreams/stream.hpp | 18 --- external/boost/multiprecision/cpp_int.hpp | 24 ---- external/glasgow-subgraph-solver | 2 +- minorminer/subgraph.pyx | 124 ++++++++++++------ pyproject.toml | 2 +- .../add-glasgow-colors-f139f9d91fce3839.yaml | 6 + setup.py | 37 +++--- tests/test_subgraph.py | 55 ++++++++ 18 files changed, 174 insertions(+), 353 deletions(-) rename external/{boost/thread/barrier.hpp => barrier} (90%) delete mode 100644 external/boost/BOOST_LICENSE delete mode 100644 external/boost/bimap.hpp delete mode 100644 external/boost/bimap/unordered_set_of.hpp delete mode 100644 external/boost/container/allocator.hpp delete mode 100644 external/boost/functional/hash.hpp delete mode 100644 external/boost/iostreams/device/file.hpp delete mode 100644 external/boost/iostreams/filter/bzip2.hpp delete mode 100644 external/boost/iostreams/filtering_stream.hpp delete mode 100644 external/boost/iostreams/stream.hpp delete mode 100644 external/boost/multiprecision/cpp_int.hpp create mode 100644 releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml diff --git a/MANIFEST.in b/MANIFEST.in index 3064a292..5ca6dec2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -2,5 +2,5 @@ include pyproject.toml global-include *.hpp *.pyx *.pxd *.pxi recursive-include external/glasgow-subgraph-solver *.hh LICENSE recursive-include external/portable-snippets builtin.h COPYING.md -recursive-include external/boost BOOST_LICENSE +recursive-include external barrier recursive-include minorminer/_extern LICENSE.md diff --git a/external/boost/thread/barrier.hpp b/external/barrier similarity index 90% rename from external/boost/thread/barrier.hpp rename to external/barrier index 55fb3c83..bf48fe81 100644 --- a/external/boost/thread/barrier.hpp +++ b/external/barrier @@ -1,4 +1,4 @@ -// Copyright 2022-2023 D-Wave Systems Inc. +// Copyright 2022-2025 D-Wave Systems Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,12 +14,14 @@ // // Excerpted from and/or inspired by implementations found in the Boost Library // see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt +// +// Modified to emulate c++20's barrier interface #pragma once #include #include -namespace boost { +namespace std { class barrier { std::mutex mtx; @@ -29,7 +31,7 @@ class barrier { unsigned int generation; public: barrier(unsigned int m) : waiters(m), waiting(m), generation(0) {} - bool wait() { + bool arrive_and_wait() { std::unique_lock lk{mtx}; unsigned int gen = generation; if (--waiting == 0) { diff --git a/external/boost/BOOST_LICENSE b/external/boost/BOOST_LICENSE deleted file mode 100644 index 36b7cd93..00000000 --- a/external/boost/BOOST_LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Boost Software License - Version 1.0 - August 17th, 2003 - -Permission is hereby granted, free of charge, to any person or organization -obtaining a copy of the software and accompanying documentation covered by -this license (the "Software") to use, reproduce, display, distribute, -execute, and transmit the Software, and to prepare derivative works of the -Software, and to permit third-parties to whom the Software is furnished to -do so, all subject to the following: - -The copyright notices in the Software and this entire statement, including -the above license grant, this restriction and the following disclaimer, -must be included in all copies of the Software, in whole or in part, and -all derivative works of the Software, unless such copies or derivative -works are solely in the form of machine-executable object code generated by -a source language processor. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT -SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE -FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, -ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. diff --git a/external/boost/bimap.hpp b/external/boost/bimap.hpp deleted file mode 100644 index bca4c013..00000000 --- a/external/boost/bimap.hpp +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -#include -#include -#include - -namespace boost { - -template -class bimap; - -template -class bimap, boost::bimaps::unordered_set_of, Alloc> { - public: - std::unordered_map left; - std::unordered_map right; - typedef typename std::pair value_type; - - void insert(const value_type v) { - left.insert(std::make_pair(v.first, v.second)); - right.insert(std::make_pair(v.second, v.first)); - } - - bimap() {} -}; - -} diff --git a/external/boost/bimap/unordered_set_of.hpp b/external/boost/bimap/unordered_set_of.hpp deleted file mode 100644 index 7abce1db..00000000 --- a/external/boost/bimap/unordered_set_of.hpp +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -namespace boost::bimaps { - -template -class unordered_set_of { - typedef T value_type; -}; - -} diff --git a/external/boost/container/allocator.hpp b/external/boost/container/allocator.hpp deleted file mode 100644 index d7088b25..00000000 --- a/external/boost/container/allocator.hpp +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -namespace boost::container { - -template class allocator {}; - -} - diff --git a/external/boost/functional/hash.hpp b/external/boost/functional/hash.hpp deleted file mode 100644 index 5dcc3892..00000000 --- a/external/boost/functional/hash.hpp +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -namespace boost { - -template -inline void hash_combine(std::size_t & seed, const T value) { - std::hash h; - hash_combine(seed, h(value)); -} - -template <> -inline void hash_combine(std::size_t & seed, const std::size_t value) { - seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2); -} - -} diff --git a/external/boost/iostreams/device/file.hpp b/external/boost/iostreams/device/file.hpp deleted file mode 100644 index 6f07eba1..00000000 --- a/external/boost/iostreams/device/file.hpp +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -#include -#include -#include - -namespace boost::iostreams { - -std::ofstream file_sink(const std::string &fn) { - std::ofstream outfile; - outfile.open(fn); - return outfile; -} - -} diff --git a/external/boost/iostreams/filter/bzip2.hpp b/external/boost/iostreams/filter/bzip2.hpp deleted file mode 100644 index ad04d240..00000000 --- a/external/boost/iostreams/filter/bzip2.hpp +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -#include - -namespace boost::iostreams { - -bool bzip2_compressor() { - throw std::runtime_error("filtering_ostream is not implemented"); - return false; -} - -} diff --git a/external/boost/iostreams/filtering_stream.hpp b/external/boost/iostreams/filtering_stream.hpp deleted file mode 100644 index dea63fb3..00000000 --- a/external/boost/iostreams/filtering_stream.hpp +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -#include -#include -namespace boost::iostreams { - -class filtering_ostream: public std::basic_ostream { - class fake_streambuf: public std::basic_streambuf { - public: - fake_streambuf() : std::basic_streambuf() {} - }; - fake_streambuf sb; - public: - filtering_ostream() : sb(), basic_ostream(&sb) {} - template - void push(T) { throw std::runtime_error("filtering_ostream is not implemented"); } -}; - -} diff --git a/external/boost/iostreams/stream.hpp b/external/boost/iostreams/stream.hpp deleted file mode 100644 index 77af2006..00000000 --- a/external/boost/iostreams/stream.hpp +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once diff --git a/external/boost/multiprecision/cpp_int.hpp b/external/boost/multiprecision/cpp_int.hpp deleted file mode 100644 index b285b3f3..00000000 --- a/external/boost/multiprecision/cpp_int.hpp +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2022-2023 D-Wave Systems Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// Excerpted from and/or inspired by implementations found in the Boost Library -// see external/boost/BOOST_LICENSE or https://www.boost.org/LICENSE_1_0.txt - -#pragma once - -namespace boost::multiprecision { - -typedef long long int cpp_int; - -} diff --git a/external/glasgow-subgraph-solver b/external/glasgow-subgraph-solver index 568c45f8..3535531f 160000 --- a/external/glasgow-subgraph-solver +++ b/external/glasgow-subgraph-solver @@ -1 +1 @@ -Subproject commit 568c45f85be2eb3472cf5133d2c55d0382c383e1 +Subproject commit 3535531f1ee88942faf5f18885794f7917bb6261 diff --git a/minorminer/subgraph.pyx b/minorminer/subgraph.pyx index 8432b0b9..358a8a5a 100644 --- a/minorminer/subgraph.pyx +++ b/minorminer/subgraph.pyx @@ -44,10 +44,10 @@ from libcpp.pair cimport pair from libc.stdint cimport uint8_t, uint64_t from libcpp.string cimport string -cdef class labeldict(dict): +cdef class _labeldict(dict): cdef list _label def __init__(self,*args,**kwargs): - super(labeldict,self).__init__(args,**kwargs) + super(_labeldict,self).__init__(args,**kwargs) self._label = [] def __missing__(self,l): self[l] = k = len(self._label) @@ -70,54 +70,56 @@ cdef extern from "" namespace "std::chrono" nogil: cdef milliseconds make_milliseconds "std::chrono::milliseconds"(int) cdef time_point[steady_clock] steady_clock_now "std::chrono::steady_clock::now"() -cdef extern from "glasgow-subgraph-solver/src/timeout.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/timeout.hh" namespace "gss" nogil: cppclass Timeout: Timeout(seconds) -cdef extern from "glasgow-subgraph-solver/src/formats/input_graph.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/formats/input_graph.hh" nogil: cdef cppclass InputGraph: InputGraph(int, bool, bool) void add_edge(int, int) + void add_directed_edge(int, int, string) void resize(int) + void set_vertex_label(int, string) -cdef extern from "glasgow-subgraph-solver/src/restarts.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/restarts.hh" namespace "gss" nogil: cdef cppclass RestartsSchedule: pass - cdef unsigned long long default_luby_multiplier "LubyRestartsSchedule::default_multiplier" - cdef milliseconds default_timed_duration "TimedRestartsSchedule::default_duration" - cdef unsigned long long default_timed_backtracks "TimedRestartsSchedule::default_minimum_backtracks" - cdef double default_geometric_constant "GeometricRestartsSchedule::default_initial_value" - cdef double default_geometric_multiplier "GeometricRestartsSchedule::default_multiplier" + cdef unsigned long long default_luby_multiplier "gss::LubyRestartsSchedule::default_multiplier" + cdef milliseconds default_timed_duration "gss::TimedRestartsSchedule::default_duration" + cdef unsigned long long default_timed_backtracks "gss::TimedRestartsSchedule::default_minimum_backtracks" + cdef double default_geometric_constant "gss::GeometricRestartsSchedule::default_initial_value" + cdef double default_geometric_multiplier "gss::GeometricRestartsSchedule::default_multiplier" cdef extern from "" namespace "std" nogil: cdef cppclass shared_ptr[T]: pass cdef cppclass unique_ptr[T]: pass - cdef shared_ptr[Timeout] make_shared_timeout "std::make_shared"(seconds) + cdef shared_ptr[Timeout] make_shared_timeout "std::make_shared"(seconds) cdef shared_ptr[InputGraph] make_shared[InputGraph](int, bool, bool) cdef InputGraph deref "*"(shared_ptr[InputGraph]) - cdef unique_ptr[RestartsSchedule] make_no_restarts_schedule "std::make_unique"() - cdef unique_ptr[RestartsSchedule] make_luby_restarts_schedule "std::make_unique"(unsigned long long) - cdef unique_ptr[RestartsSchedule] make_geometric_restarts_schedule "std::make_unique"(double, double) - cdef unique_ptr[RestartsSchedule] make_timed_restarts_schedule "std::make_unique"(milliseconds, unsigned long long) + cdef unique_ptr[RestartsSchedule] make_no_restarts_schedule "std::make_unique"() + cdef unique_ptr[RestartsSchedule] make_luby_restarts_schedule "std::make_unique"(unsigned long long) + cdef unique_ptr[RestartsSchedule] make_geometric_restarts_schedule "std::make_unique"(double, double) + cdef unique_ptr[RestartsSchedule] make_timed_restarts_schedule "std::make_unique"(milliseconds, unsigned long long) -cdef extern from "glasgow-subgraph-solver/src/vertex_to_vertex_mapping.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/vertex_to_vertex_mapping.hh" namespace "gss" nogil: cdef cppclass VertexToVertexMapping: pass -cdef extern from "glasgow-subgraph-solver/src/value_ordering.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/value_ordering.hh" namespace "gss" nogil: #this enum contains None - cdef enum ValueOrdering 'ValueOrdering': - _VO_None 'ValueOrdering::None' - _VO_Biased 'ValueOrdering::Biased' - _VO_Degree 'ValueOrdering::Degree' - _VO_AntiDegree 'ValueOrdering::AntiDegree' - _VO_Random 'ValueOrdering::Random' - -cdef extern from "glasgow-subgraph-solver/src/homomorphism.hh" nogil: + cdef enum ValueOrdering 'gss::ValueOrdering': + _VO_None 'gss::ValueOrdering::None' + _VO_Biased 'gss::ValueOrdering::Biased' + _VO_Degree 'gss::ValueOrdering::Degree' + _VO_AntiDegree 'gss::ValueOrdering::AntiDegree' + _VO_Random 'gss::ValueOrdering::Random' + +cdef extern from "glasgow-subgraph-solver/gss/homomorphism.hh" namespace "gss" nogil: cppclass HomomorphismParams: shared_ptr[Timeout] timeout time_point[steady_clock] start_time @@ -138,7 +140,7 @@ cdef extern from "glasgow-subgraph-solver/src/homomorphism.hh" nogil: cppclass HomomorphismResult: map[int, int] mapping -cdef extern from "glasgow-subgraph-solver/src/sip_decomposer.hh" nogil: +cdef extern from "glasgow-subgraph-solver/gss/sip_decomposer.hh" namespace "gss" nogil: cdef HomomorphismResult solve_sip_by_decomposition(InputGraph, InputGraph, HomomorphismParams) except+ def find_subgraph(source, target, **kwargs): @@ -163,24 +165,38 @@ def find_subgraph(source, target, **kwargs): Args: S (iterable/NetworkX Graph): - The source graph as an iterable of label pairs representing the + The source graph as an iterable of node pairs representing the edges, or a NetworkX Graph. T (iterable/NetworkX Graph): - The target graph as an iterable of label pairs representing the + The target graph as an iterable of node pairs representing the edges, or a NetworkX Graph. **params (optional): See below. Returns: - A dict that maps labels in S to labels in T. If no isomorphism - is found, and empty dictionary is returned. + A dict that maps nodes in S to nodes in T. If no isomorphism is found, + an empty dictionary is returned. Optional Parameters: timeout (int, optional, default=0) Abort after this many seconds parallel (bool, optional, default=False): Use auto-configured parallel search (highly nondeterministic runtimes) + node_labels (tuple, optional, default=None): + If not ``None``, a pair of dicts (``S_labels``, ``T_labels``) whose keys are + nodes and values are strings. Unlabeled nodes are labeled with + the empty string "". + edge_labels (tuple, optional, default=None): + If not ``None``, a pair of dicts (``S_labels``, ``T_labels``) whose keys are + (source, dest) pairs of nodes corresponding to directed edges, and + values are strings. Unlabeled directed edges are labeled with the + empty string "". If the label on an edge (u, v) is intended to be + undirected, you must provide the same label for both directions + (u, v) and (v, u). + as_embedding (bool, optional, default=False): + If ``True``, the values of the returned dictionary will be singleton + tuples similar to the return type of ``find_embedding``. Advanced Parallelism Options threads (int, optional, default=1): @@ -227,10 +243,14 @@ def find_subgraph(source, target, **kwargs): Use clique size constraints on supplemental graphs too """ - cdef shared_ptr[InputGraph] source_g = make_shared[InputGraph](0, False, False) - cdef shared_ptr[InputGraph] target_g = make_shared[InputGraph](0, False, False) - cdef labeldict source_labels = _read_graph(deref(source_g), source) - cdef labeldict target_labels = _read_graph(deref(target_g), target) + node_labels = kwargs.pop("node_labels", (None, None)) + edge_labels = kwargs.pop("edge_labels", (None, None)) + + cdef shared_ptr[InputGraph] source_g = make_shared[InputGraph](0, node_labels[0], edge_labels[0]) + cdef shared_ptr[InputGraph] target_g = make_shared[InputGraph](0, node_labels[1], edge_labels[1]) + + cdef _labeldict source_labels = _read_graph(deref(source_g), source, node_labels[0], edge_labels[0]) + cdef _labeldict target_labels = _read_graph(deref(target_g), target, node_labels[1], edge_labels[1]) cdef HomomorphismParams params cdef bool parallel = kwargs.pop('parallel', False) @@ -297,23 +317,47 @@ def find_subgraph(source, target, **kwargs): params.timeout = make_shared_timeout(make_seconds(kwargs.pop('timeout', 0))) params.start_time = steady_clock_now() + as_embedding = kwargs.pop("as_embedding", False) + if kwargs: raise ValueError("unknown/unused parameters: {list(kwargs.keys())}") cdef HomomorphismResult result = solve_sip_by_decomposition(deref(source_g), deref(target_g), params) - return dict((source_labels.label(s), target_labels.label(t)) for s, t in result.mapping) + if as_embedding: + return dict((source_labels.label(s), (target_labels.label(t),)) for s, t in result.mapping) + else: + return dict((source_labels.label(s), target_labels.label(t)) for s, t in result.mapping) -cdef _read_graph(InputGraph &g, E): - cdef labeldict L = labeldict() +cdef _read_graph(InputGraph &g, E, node_labels, edge_labels): + cdef _labeldict L = _labeldict() + cdef str label if hasattr(E, 'edges'): G = E E = E.edges() for a in G.nodes(): L[a] - - for a, b in E: - g.add_edge(L[a],L[b]) + + if edge_labels is None: + for a, b in E: + g.add_edge(L[a],L[b]) + else: + for a, b in E: + label = edge_labels.get((a, b), "") + g.add_directed_edge(L[a], L[b], bytes(label, "utf8")) + label = edge_labels.get((b, a), "") + g.add_directed_edge(L[b], L[a], bytes(label, "utf8")) + + if node_labels is not None: + # performance note: we really wanna do this in order because as of + # writing, the Glasgow implementation of set_vertex_label uses vector + # erase/insert which can result in accidentally-quadratic runtime if + # we don't write at the end + for i, a in enumerate(L._label): + label = node_labels.get(a) + if label is not None: + g.resize(i+1) + g.set_vertex_label(i, bytes(label, "utf8")) g.resize(len(L)) return L diff --git a/pyproject.toml b/pyproject.toml index c4e4408b..664cef1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ authors = [ maintainers = [ {name = "D-Wave Inc.", email = "tools@dwavesys.com"}, ] -license = {file = "LICENSE"} +license-files = ["LICENSE", "external/glasgow-subgraph-solver/LICENCE", "minorminer/_extern/rpack/LICENSE.md"] classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX", diff --git a/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml b/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml new file mode 100644 index 00000000..82bb6672 --- /dev/null +++ b/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml @@ -0,0 +1,6 @@ +--- +features: + - new coloring features for subgraph matching (nodes or directed edges can be assigned labels with the `node_labels` and `edge_labels` arguments to `subgraph.find_subgraph`) + - `subgraph.find_subgraph` accepts an additional new argument `as_embedding` to return a dict with iterable values similar to other embedding tools +issues: + - the `timeout` argument in `subgraph.find_subgraph` seems to be broken diff --git a/setup.py b/setup.py index cf3366f1..48d03168 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ extra_compile_args_glasgow.extend([ '/external:W4', - '/external:I external', + '/external:I', 'external', + '/external:I', 'external/glasgow-subgraph-solver', '/DUSE_PORTABLE_SNIPPETS_BUILTIN', ]) @@ -61,7 +62,8 @@ ]) extra_compile_args_glasgow.extend([ - '-isystemexternal', + '-isystem', 'external', + '-isystem', 'external/glasgow-subgraph-solver', '-DUSE_PORTABLE_SNIPPETS_BUILTIN', ]) @@ -77,25 +79,25 @@ # this is a subset of the total source files, so we can't just use glob or similar glasgow_cc = [ - '/'.join(['external/glasgow-subgraph-solver/src', f]) + '/'.join(['external/glasgow-subgraph-solver/gss', f]) for f in [ - 'cheap_all_different.cc', 'clique.cc', 'configuration.cc', - 'graph_traits.cc', 'homomorphism.cc', - 'homomorphism_domain.cc', - 'homomorphism_model.cc', - 'homomorphism_searcher.cc', - 'homomorphism_traits.cc', - 'lackey.cc', - 'proof.cc', + 'innards/proof.cc', 'restarts.cc', 'sip_decomposer.cc', - 'svo_bitset.cc', 'timeout.cc', - 'thread_utils.cc', - 'watches.cc', + 'innards/cheap_all_different.cc', + 'innards/graph_traits.cc', + 'innards/homomorphism_domain.cc', + 'innards/homomorphism_model.cc', + 'innards/homomorphism_searcher.cc', + 'innards/homomorphism_traits.cc', + 'innards/lackey.cc', + 'innards/svo_bitset.cc', + 'innards/thread_utils.cc', + 'innards/watches.cc', 'formats/input_graph.cc', 'formats/graph_file_error.cc', ] @@ -120,8 +122,10 @@ name="minorminer.subgraph", sources=["./minorminer/subgraph.pyx"] + glasgow_cc, include_dirs=['', './include', './external', - './external/glasgow-subgraph-solver/src'], - library_dirs=['./include'], + './external/glasgow-subgraph-solver' + './external/glasgow-subgraph-solver/src' + './external/glasgow-subgraph-solver/gss'], + library_dirs=['./include', './external', './external/glasgow-subgraph-solver/gss/formats'], language='c++', extra_compile_args=extra_compile_args + extra_compile_args_glasgow, ), @@ -139,6 +143,7 @@ packages=['minorminer', 'minorminer.layout', 'minorminer.utils', + 'minorminer._extern', 'minorminer._extern.rpack', ], include_package_data=True, diff --git a/tests/test_subgraph.py b/tests/test_subgraph.py index d3b62807..4b47b920 100644 --- a/tests/test_subgraph.py +++ b/tests/test_subgraph.py @@ -1,5 +1,6 @@ from minorminer import subgraph from minorminer.utils import verify_embedding +from dwave_networkx import chimera_graph import unittest, random, itertools, dwave_networkx as dnx, networkx as nx, os @@ -11,3 +12,57 @@ def test_smoketest(self): emb = {k: [v] for k, v in emb.items()} verify_embedding(emb, c, b) + def test_as_embedding(self): + b = nx.complete_bipartite_graph(4, 4) + c = nx.cubical_graph() + emb = subgraph.find_subgraph(c, b, as_embedding=True) + verify_embedding(emb, c, b) + + def test_node_colors(self): + path_5 = nx.path_graph(5) + path_10 = nx.path_graph(10) + node_labels = {0: "start", 4:"end"}, {5: "start", 1: "end"} + emb = subgraph.find_subgraph(path_5, path_10, node_labels=node_labels) + for v in path_5: + self.assertEqual(node_labels[0].get(v), node_labels[1].get(emb[v])) + + node_labels = {0: "start", 4:"end"}, {5: "start", 0: "end"} #impossible + emb = subgraph.find_subgraph(path_5, path_10, node_labels=node_labels) + self.assertEqual(emb, {}) + + def test_edge_colors(self): + path_5 = nx.path_graph(5) + path_10 = nx.path_graph(10) + + #this is a directed edge thing + edge_labels = {(0,1): "start"}, {(1, 0): "start"} #impossible + emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) + self.assertEqual(emb, {}) + + #try that again but undirected + edge_labels = {(0,1): "start", (1, 0): "start"}, {(0,1): "start", (1, 0): "start"} + emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) + for uv in path_5.edges: + u, v = uv + pq = emb[u], emb[v] + self.assertEqual(edge_labels[0].get(uv), edge_labels[1].get(pq)) + self.assertEqual(edge_labels[0].get(uv[::-1]), edge_labels[1].get(pq[::-1])) + + #now with a solvable directed problem + edge_labels = {(0,1): "start"}, {(7, 6): "start"} + emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) + for uv in path_5.edges: + u, v = uv + pq = emb[u], emb[v] + self.assertEqual(edge_labels[0].get(uv), edge_labels[1].get(pq)) + self.assertEqual(edge_labels[0].get(uv[::-1]), edge_labels[1].get(pq[::-1])) + + def test_timeout(self): + source = chimera_graph(8) + target = chimera_graph(15, coordinates=True) + #pop out a vertex from the central tile to make a minimally-impossible + #problem (no fully-yielded 8x8s) that GSS know how to reason about + target.remove_node((7,7,0,0)) + emb = subgraph.find_subgraph(source, target, timeout=2) + self.assertEqual(emb, {}) + From 7697976d0765d94ae024be15d6e1bc836493553a Mon Sep 17 00:00:00 2001 From: Kelly Boothby Date: Sat, 13 Sep 2025 08:14:03 -0700 Subject: [PATCH 119/127] Glasgow Miscellany (#273) * removed kwargs handling in favor for explicit arguments (issue #255) * added injectivity parameter to support noninjective and locally-injective homomorphisms * fix isolated nodes issue #254 and add injectivity options * support random seeds (issue #243) --- minorminer/subgraph.pyx | 238 +++++++++++++----- .../glasgow-miscellany-49ac8d45bb83fc78.yaml | 7 + tests/test_subgraph.py | 189 ++++++++++++-- 3 files changed, 354 insertions(+), 80 deletions(-) create mode 100644 releasenotes/notes/glasgow-miscellany-49ac8d45bb83fc78.yaml diff --git a/minorminer/subgraph.pyx b/minorminer/subgraph.pyx index 358a8a5a..dc7c7ee3 100644 --- a/minorminer/subgraph.pyx +++ b/minorminer/subgraph.pyx @@ -43,6 +43,7 @@ from libcpp.map cimport map from libcpp.pair cimport pair from libc.stdint cimport uint8_t, uint64_t from libcpp.string cimport string +import random as _random cdef class _labeldict(dict): cdef list _label @@ -53,6 +54,10 @@ cdef class _labeldict(dict): self[l] = k = len(self._label) self._label.append(l) return k + def shuffle(self, random): + random.shuffle(self._label) + for i, l in enumerate(self._label): + self[l] = i def label(self,k): return self._label[k] @@ -120,6 +125,11 @@ cdef extern from "glasgow-subgraph-solver/gss/value_ordering.hh" namespace "gss" _VO_Random 'gss::ValueOrdering::Random' cdef extern from "glasgow-subgraph-solver/gss/homomorphism.hh" namespace "gss" nogil: + cdef enum Injectivity 'gss:Injectivity': + _I_Injective 'gss::Injectivity::Injective' + _I_LocallyInjective 'gss::Injectivity::LocallyInjective' + _I_NonInjective 'gss::Injectivity::NonInjective' + cppclass HomomorphismParams: shared_ptr[Timeout] timeout time_point[steady_clock] start_time @@ -128,6 +138,7 @@ cdef extern from "glasgow-subgraph-solver/gss/homomorphism.hh" namespace "gss" n bool triggered_restarts bool delay_thread_creation ValueOrdering value_ordering_heuristic + Injectivity injectivity bool clique_detection bool distance3 bool k4 @@ -143,7 +154,40 @@ cdef extern from "glasgow-subgraph-solver/gss/homomorphism.hh" namespace "gss" n cdef extern from "glasgow-subgraph-solver/gss/sip_decomposer.hh" namespace "gss" nogil: cdef HomomorphismResult solve_sip_by_decomposition(InputGraph, InputGraph, HomomorphismParams) except+ -def find_subgraph(source, target, **kwargs): +_default_kwarg = object() +def _check_kwarg(kwargs, name, default): + value = kwargs.pop(name) + return default if value is _default_kwarg else value + +def find_subgraph( + source, + target, + timeout=0, + parallel=False, + node_labels=None, + edge_labels=None, + as_embedding=False, + injectivity='injective', + seed=None, + threads=1, + triggered_restarts=_default_kwarg, + delay_thread_creation=_default_kwarg, + restarts_policy=_default_kwarg, + luby_constant=_default_kwarg, + geometric_constant=_default_kwarg, + geometric_multiplier=_default_kwarg, + restart_interval=_default_kwarg, + restart_minimum=_default_kwarg, + value_ordering='biased', + clique_detection=True, + no_supplementals=False, + no_nds=False, + distance3=False, + k4=False, + n_exact_path_graphs=4, + cliques=False, + cliques_on_supplementals=False, + ): """ Use the Glasgow Subgraph Solver to find a subgraph isomorphism from source to target. @@ -197,18 +241,39 @@ def find_subgraph(source, target, **kwargs): as_embedding (bool, optional, default=False): If ``True``, the values of the returned dictionary will be singleton tuples similar to the return type of ``find_embedding``. + injectivity (string, optional, default='injective'): + Must be one of ('injective', 'locally injective', 'noninjective'). + By default, this function searches for subgraphs by finding injective + homomorphisms. That is, nodes of the target graph can only occur + once in the output of the mapping. By providing the default value + 'injective' to the `injectivity` parameter, that is true. A mapping + can be said to be 'locally injective' if the mapping is injective + on the neighborhood of every node. + seed (int/object, optional, default=None): + If ``seed`` is an int, it will be used as a seed to a random number + generator to randomize the algorithm. This randomization is + accomplished by shuffling the nodes and edges of the source and + target graphs. If ``seed`` is an object with an attribute named + ``shuffle``, then that function will be called, with the expectation + that it is equivalent to ``random.shuffle``. + + Note that the placement of nodes without incident edges is not + subject to explicit randomization. Advanced Parallelism Options threads (int, optional, default=1): Use threaded search, with this many threads (0 to auto-detect) + (this value is overridden to zero if ``paralell` is ``True``) triggered_restarts (bool, optional, default=False): Have one thread trigger restarts (more nondeterminism, better performance) delay_thread_creation (bool, optional, default=False): Do not create threads until after the first restart + (default is changed to ``True`` if ``parallel`` is ``True`` Advanced Search Configuration Options: restarts_policy (string, optional, default='luby'): Specify restart policy ('luby', 'geometric', 'timed' or 'none') + (default policy is 'timed' with default parameters if ``parallel`` is ``True``) luby_constant (int, optional, default=666): Specify the starting constant / multiplier for Luby restarts geometric_constant (double, optional, 5400): @@ -227,7 +292,7 @@ def find_subgraph(source, target, **kwargs): Enable clique / independent set detection no_supplementals (bool, optional, default=False): Do not use supplemental graphs - no_nds (bool, optional, default=false): + no_nds (bool, optional, default=False): Do not use neighbourhood degree sequences Hidden Options: @@ -243,38 +308,54 @@ def find_subgraph(source, target, **kwargs): Use clique size constraints on supplemental graphs too """ - node_labels = kwargs.pop("node_labels", (None, None)) - edge_labels = kwargs.pop("edge_labels", (None, None)) + if node_labels is None: + node_labels = (None, None) + if edge_labels is None: + edge_labels = (None, None) cdef shared_ptr[InputGraph] source_g = make_shared[InputGraph](0, node_labels[0], edge_labels[0]) cdef shared_ptr[InputGraph] target_g = make_shared[InputGraph](0, node_labels[1], edge_labels[1]) - cdef _labeldict source_labels = _read_graph(deref(source_g), source, node_labels[0], edge_labels[0]) - cdef _labeldict target_labels = _read_graph(deref(target_g), target, node_labels[1], edge_labels[1]) - cdef HomomorphismParams params + if seed is None: + random = None + elif not hasattr(seed, 'shuffle'): + random = _random.Random(seed) + else: + random = seed - cdef bool parallel = kwargs.pop('parallel', False) + cdef _labeldict source_labels + cdef _labeldict target_labels + source_labels, source_isolated = _read_graph(deref(source_g), source, node_labels[0], edge_labels[0], random) + target_labels, target_isolated = _read_graph(deref(target_g), target, node_labels[1], edge_labels[1], random) + cdef HomomorphismParams params - params.triggered_restarts = kwargs.pop('triggered_restarts', parallel) + if triggered_restarts is _default_kwarg: + triggered_restarts = parallel - restarts_policy = kwargs.pop('restarts_policy', None) + check_kwargs = { + 'luby_constant': luby_constant, + 'geometric_constant': geometric_constant, + 'geometric_multiplier': geometric_multiplier, + 'restart_interval': restart_interval, + 'restart_minimum': restart_minimum, + } if restarts_policy == 'luby': - multiplier = kwargs.pop('luby_constant', default_luby_multiplier) + multiplier = _check_kwarg(check_kwargs, 'luby_constant', default_luby_multiplier) params.restarts_schedule = make_luby_restarts_schedule(multiplier) elif restarts_policy == 'geometric': - constant = kwargs.pop('geometric_constant', default_geometric_constant) - multiplier = kwargs.pop('geometric_multiplier', default_geometric_multiplier) + constant = _check_kwarg(check_kwargs, 'geometric_constant', default_geometric_constant) + multiplier = _check_kwarg(check_kwargs, 'geometric_multiplier', default_geometric_multiplier) params.restarts_schedule = make_geometric_restarts_schedule(constant, multiplier) elif restarts_policy == 'timed': - interval = kwargs.pop('restart_interval', None) - backtracks = kwargs.pop('restart_minimum', default_timed_backtracks) + interval = _check_kwarg(check_kwargs, 'restart_interval', None) + backtracks = _check_kwarg(check_kwargs, 'restart_minimum', default_timed_backtracks) params.restarts_schedule = make_timed_restarts_schedule( default_timed_duration if interval is None else make_milliseconds(interval), backtracks ) elif restarts_policy == 'none': params.restarts_schedule = make_no_restarts_schedule() - elif restarts_policy is None: + elif restarts_policy is _default_kwarg: if parallel: params.restarts_schedule = make_timed_restarts_schedule(default_timed_duration, default_timed_backtracks) else: @@ -282,13 +363,12 @@ def find_subgraph(source, target, **kwargs): else: raise ValueError(f"restarts_policy {restarts_policy} not recognized") - params.n_threads = kwargs.pop('threads', 1) - if parallel: - params.n_threads = 0 + params.n_threads = 0 if parallel else threads - params.delay_thread_creation = kwargs.pop('delay_thread_creation', parallel) + if delay_thread_creation is _default_kwarg: + delay_thread_creation = parallel + params.delay_thread_creation = delay_thread_creation - value_ordering = kwargs.pop('value_ordering', 'biased') if value_ordering == 'none': params.value_ordering_heuristic = _VO_None elif value_ordering == 'biased': @@ -302,51 +382,87 @@ def find_subgraph(source, target, **kwargs): else: raise RuntimeError("unknown value ordering heuristic") - params.clique_detection = kwargs.pop('clique_detection', params.clique_detection) - params.distance3 = kwargs.pop('distance3', params.distance3) - params.k4 = kwargs.pop('k4', params.k4) - params.number_of_exact_path_graphs = kwargs.pop('n_exact_path_graphs', params.number_of_exact_path_graphs) - params.no_supplementals = kwargs.pop('no_supplementals', params.no_supplementals) - params.no_nds = kwargs.pop('no_nds', params.no_nds) - params.clique_size_constraints = kwargs.pop('cliques', params.clique_size_constraints) - params.clique_size_constraints_on_supplementals = kwargs.pop( - 'cliques_on_supplementals', - params.clique_size_constraints_on_supplementals - ) - - params.timeout = make_shared_timeout(make_seconds(kwargs.pop('timeout', 0))) + if injectivity == 'injective': + params.injectivity = _I_Injective + elif injectivity == 'locally injective': + params.injectivity = _I_LocallyInjective + elif injectivity == 'noninjective': + params.injectivity = _I_NonInjective + else: + raise RuntimeError("unrecognized injectivity option") + + if clique_detection is not _default_kwarg: + params.clique_detection = clique_detection + if distance3 is not _default_kwarg: + params.distance3 = distance3 + if k4 is not _default_kwarg: + params.k4 = k4 + if n_exact_path_graphs is not _default_kwarg: + params.number_of_exact_path_graphs = n_exact_path_graphs + if n_exact_path_graphs is not _default_kwarg: + params.no_supplementals = no_supplementals + if no_nds is not _default_kwarg: + params.no_nds = no_nds + if cliques is not _default_kwarg: + params.clique_size_constraints = cliques + if cliques_on_supplementals is not _default_kwarg: + params.clique_size_constraints_on_supplementals = cliques_on_supplementals + + params.timeout = make_shared_timeout(make_seconds(timeout)) params.start_time = steady_clock_now() - as_embedding = kwargs.pop("as_embedding", False) - - if kwargs: - raise ValueError("unknown/unused parameters: {list(kwargs.keys())}") + check_kwargs = {k: v for k, v in check_kwargs.items() if v is not _default_kwarg} + if check_kwargs: + raise ValueError(f"unused parameters: {list(check_kwargs.keys())}") - cdef HomomorphismResult result = solve_sip_by_decomposition(deref(source_g), deref(target_g), params) + cdef HomomorphismResult result; + if len(source_labels) + len(source_isolated) <= len(target_labels) + len(target_isolated) or injectivity != 'injective': + result = solve_sip_by_decomposition(deref(source_g), deref(target_g), params) + emb = dict((source_labels.label(s), target_labels.label(t)) for s, t in result.mapping) + else: + emb = {} + + if source_isolated and (len(emb) == len(source_labels)): + if injectivity == 'injective': + target_isolated.extend(set(target_labels)-set(emb.values())) + if len(source_isolated) <= len(target_isolated): + for s, t in zip(source_isolated, target_isolated): + emb[s] = t + elif target_isolated or target_labels: + t = next(iter(target_isolated or target_labels)) + for s in source_isolated: + emb[s] = t if as_embedding: - return dict((source_labels.label(s), (target_labels.label(t),)) for s, t in result.mapping) - else: - return dict((source_labels.label(s), target_labels.label(t)) for s, t in result.mapping) + emb = {k: (v,) for k, v in emb.items()} + + return emb -cdef _read_graph(InputGraph &g, E, node_labels, edge_labels): +cdef _read_graph(InputGraph &g, E, node_labels, edge_labels, random): cdef _labeldict L = _labeldict() cdef str label + isolated_nodes = [] if hasattr(E, 'edges'): G = E - E = E.edges() - for a in G.nodes(): - L[a] - - if edge_labels is None: - for a, b in E: - g.add_edge(L[a],L[b]) + E = list(E.edges()) + for a, d in G.degree(): + if d: + L[a] + else: + isolated_nodes.append(a) else: - for a, b in E: - label = edge_labels.get((a, b), "") - g.add_directed_edge(L[a], L[b], bytes(label, "utf8")) - label = edge_labels.get((b, a), "") - g.add_directed_edge(L[b], L[a], bytes(label, "utf8")) + G = None + + if random is not None or node_labels is not None: + if G is None: + # E might be a generator... this is a silly-looking line but it + # walks over the edge-list, puts every node into L, and leaves E + # functionally unchanged + E = [(L[a], L[b]) and (a, b) for a, b in E] + + if random is not None: + L.shuffle(random) + random.shuffle(E) if node_labels is not None: # performance note: we really wanna do this in order because as of @@ -359,5 +475,15 @@ cdef _read_graph(InputGraph &g, E, node_labels, edge_labels): g.resize(i+1) g.set_vertex_label(i, bytes(label, "utf8")) + if edge_labels is None: + for a, b in E: + g.add_edge(L[a],L[b]) + else: + for a, b in E: + label = edge_labels.get((a, b), "") + g.add_directed_edge(L[a], L[b], bytes(label, "utf8")) + label = edge_labels.get((b, a), "") + g.add_directed_edge(L[b], L[a], bytes(label, "utf8")) + g.resize(len(L)) - return L + return L, isolated_nodes diff --git a/releasenotes/notes/glasgow-miscellany-49ac8d45bb83fc78.yaml b/releasenotes/notes/glasgow-miscellany-49ac8d45bb83fc78.yaml new file mode 100644 index 00000000..c8d4574d --- /dev/null +++ b/releasenotes/notes/glasgow-miscellany-49ac8d45bb83fc78.yaml @@ -0,0 +1,7 @@ +--- +features: + - add ``seed`` argument to ``subgraph.find_subgraph`` to randomize algorithm (issue #243) + - add support for non-injective homomorphisms in ``subgraph.find_embedding`` through new ``injectivity`` argument +fixes: + - argument list made explicit rather than over-reliance on ``kwargs`` (issue #255) + - proper support for nodes without incident edges (issue #254) diff --git a/tests/test_subgraph.py b/tests/test_subgraph.py index 4b47b920..678dd6dc 100644 --- a/tests/test_subgraph.py +++ b/tests/test_subgraph.py @@ -1,16 +1,39 @@ from minorminer import subgraph from minorminer.utils import verify_embedding -from dwave_networkx import chimera_graph import unittest, random, itertools, dwave_networkx as dnx, networkx as nx, os +def verify_homomorphism(emb, source, target, locallyinjective = False): + for s in source: + if s not in emb: + raise RuntimeError("homomorphism node fail") + + nbrs = {emb[v] for v in source[s]} + if not nbrs.issubset(target[emb[s]]): + raise RuntimeError("homomorphism edge fail") + + if locallyinjective and len(source[s]) + 1 > len(nbrs | {emb[s]}): + raise RuntimeError("homomorphism locality fail") + +def as_embedding(emb): + return {k: (v,) for k, v in emb.items()} class TestSubgraph(unittest.TestCase): + def verify_node_coloring(self, emb, source, node_labels): + for v in source: + self.assertEqual(node_labels[0].get(v), node_labels[1].get(emb[v])) + + def verify_edge_coloring(self, emb, source, edge_labels): + for uv in source.edges: + u, v = uv + pq = emb[u], emb[v] + self.assertEqual(edge_labels[0].get(uv), edge_labels[1].get(pq)) + self.assertEqual(edge_labels[0].get(uv[::-1]), edge_labels[1].get(pq[::-1])) + def test_smoketest(self): b = nx.complete_bipartite_graph(4, 4) c = nx.cubical_graph() emb = subgraph.find_subgraph(c, b) - emb = {k: [v] for k, v in emb.items()} - verify_embedding(emb, c, b) + verify_embedding(as_embedding(emb), c, b) def test_as_embedding(self): b = nx.complete_bipartite_graph(4, 4) @@ -20,49 +43,167 @@ def test_as_embedding(self): def test_node_colors(self): path_5 = nx.path_graph(5) - path_10 = nx.path_graph(10) - node_labels = {0: "start", 4:"end"}, {5: "start", 1: "end"} + path_10 = nx.path_graph('qrstuvwxyz') + node_labels = {0: "start", 4:"end"}, {'v': "start", 'r': "end"} emb = subgraph.find_subgraph(path_5, path_10, node_labels=node_labels) - for v in path_5: - self.assertEqual(node_labels[0].get(v), node_labels[1].get(emb[v])) + self.verify_node_coloring(emb, path_5, node_labels) + verify_embedding(as_embedding(emb), path_5, path_10) - node_labels = {0: "start", 4:"end"}, {5: "start", 0: "end"} #impossible + #do it again with generators instead of graphs + emb = subgraph.find_subgraph(iter(path_5.edges), iter(path_10.edges), node_labels=node_labels) + self.verify_node_coloring(emb, path_5, node_labels) + verify_embedding(as_embedding(emb), path_5, path_10) + + #and again with edge lists + emb = subgraph.find_subgraph(list(path_5.edges), list(path_10.edges), node_labels=node_labels) + self.verify_node_coloring(emb, path_5, node_labels) + verify_embedding(as_embedding(emb), path_5, path_10) + + node_labels = {0: "start", 4:"end"}, {'v': "start", 'q': "end"} #impossible emb = subgraph.find_subgraph(path_5, path_10, node_labels=node_labels) self.assertEqual(emb, {}) def test_edge_colors(self): - path_5 = nx.path_graph(5) + path_5 = nx.path_graph('vwxyz') path_10 = nx.path_graph(10) #this is a directed edge thing - edge_labels = {(0,1): "start"}, {(1, 0): "start"} #impossible + edge_labels = {('v','w'): "start"}, {(1, 0): "start"} #impossible emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) self.assertEqual(emb, {}) #try that again but undirected - edge_labels = {(0,1): "start", (1, 0): "start"}, {(0,1): "start", (1, 0): "start"} + edge_labels = {('v','w'): "start", ('w', 'v'): "start"}, {(0,1): "start", (1, 0): "start"} emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) - for uv in path_5.edges: - u, v = uv - pq = emb[u], emb[v] - self.assertEqual(edge_labels[0].get(uv), edge_labels[1].get(pq)) - self.assertEqual(edge_labels[0].get(uv[::-1]), edge_labels[1].get(pq[::-1])) + self.verify_edge_coloring(emb, path_5, edge_labels) + verify_embedding(as_embedding(emb), path_5, path_10) #now with a solvable directed problem - edge_labels = {(0,1): "start"}, {(7, 6): "start"} + edge_labels = {('v','w'): "start"}, {(7, 6): "start"} emb = subgraph.find_subgraph(path_5, path_10, edge_labels=edge_labels) - for uv in path_5.edges: - u, v = uv - pq = emb[u], emb[v] - self.assertEqual(edge_labels[0].get(uv), edge_labels[1].get(pq)) - self.assertEqual(edge_labels[0].get(uv[::-1]), edge_labels[1].get(pq[::-1])) + self.verify_edge_coloring(emb, path_5, edge_labels) + verify_embedding(as_embedding(emb), path_5, path_10) def test_timeout(self): - source = chimera_graph(8) - target = chimera_graph(15, coordinates=True) + source = dnx.chimera_graph(8) + target = dnx.chimera_graph(15, coordinates=True) #pop out a vertex from the central tile to make a minimally-impossible #problem (no fully-yielded 8x8s) that GSS know how to reason about target.remove_node((7,7,0,0)) emb = subgraph.find_subgraph(source, target, timeout=2) self.assertEqual(emb, {}) + def test_noninjective(self): + # find a 2-coloring of Chimera! + source = dnx.chimera_graph(4) + target = nx.path_graph(2) + emb = subgraph.find_subgraph(source, target, injectivity='noninjective') + verify_homomorphism(emb, source, target) + + # find a 4-coloring of zephyr! + source = dnx.zephyr_graph(4, t=1) + target = nx.complete_graph(4) + emb = subgraph.find_subgraph(source, target, injectivity='noninjective') + verify_homomorphism(emb, source, target) + + # but not a 3-coloring! + target = nx.complete_graph(3) + emb = subgraph.find_subgraph(source, target, injectivity='noninjective') + self.assertEqual(emb, {}) + + def test_locally_injective(self): + # find a triple-cover of a 3-cycle by a 9-cycle + source = nx.cycle_graph('rstuvwxyz') + target = nx.cycle_graph(3) + emb = subgraph.find_subgraph(source, target, injectivity='locally injective') + verify_homomorphism(emb, source, target, locallyinjective=True) + + # can't find a locally injective homomorphism for a 10-cycle into a 3-cycle + source = nx.cycle_graph(10) + emb = subgraph.find_subgraph(source, target, injectivity='locally injective') + self.assertEqual(emb, {}) + + def test_isolated_nodes(self): + source = nx.cycle_graph('rstuvwxyz') + target = nx.cycle_graph(9) + source.add_node('a') + + emb = subgraph.find_subgraph(source, target) + self.assertEqual(emb, {}) + + emb = subgraph.find_subgraph(source, target, injectivity='locally injective') + verify_homomorphism(emb, source, target, locallyinjective=True) + + target.add_node('a') + emb = subgraph.find_subgraph(source, target, as_embedding=True) + verify_embedding(emb, source, target) + + target.add_edge('a', 'x') + emb = subgraph.find_subgraph(source, target, as_embedding=True) + verify_embedding(emb, source, target) + + #annoying edge case: the source graph is edgeless :eyeroll: + source = nx.empty_graph(5) + emb = subgraph.find_subgraph(source, target, as_embedding=True) + verify_embedding(emb, source, target) + + target = nx.path_graph('ab') + target.add_node(0) + emb = subgraph.find_subgraph(source, target, injectivity='locally injective') + verify_homomorphism(emb, source, target, locallyinjective=True) + + def test_seed(self): + g = dnx.chimera_graph(3, 3, 1, coordinates=True) + emb = subgraph.find_subgraph(g, g, seed=54321, as_embedding=True) + verify_embedding(emb, g, g) + + emb = subgraph.find_subgraph(g, g, seed=random, as_embedding=True) + verify_embedding(emb, g, g) + + emb = subgraph.find_subgraph(g, g, seed=random.Random(12345), as_embedding=True) + verify_embedding(emb, g, g) + + #again, with iterable of edges + emb = subgraph.find_subgraph(iter(g.edges), g, seed=random.Random(12345), as_embedding=True) + verify_embedding(emb, g, g) + + #again, with list of edges + emb = subgraph.find_subgraph(list(g.edges), g, seed=random.Random(12345), as_embedding=True) + verify_embedding(emb, g, g) + + def test_seed_and_colors(self): + g = nx.petersen_graph() + label_nodes = random.sample(list(g), 2) + e0, e1 = random.sample(list(g.edges), 2) + node_labels = {x: "hello" for x in label_nodes} + edge_labels = {x: "hello" for x in (e0, e1, e1[::-1])} + node_labels = node_labels, node_labels + edge_labels = edge_labels, edge_labels + + emb = subgraph.find_subgraph(g, g, seed=random, node_labels=node_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_node_coloring(emb, g, node_labels) + + emb = subgraph.find_subgraph(g, g, seed=random, edge_labels=edge_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_edge_coloring(emb, g, node_labels) + + emb = subgraph.find_subgraph(g, g, seed=random, node_labels=node_labels, edge_labels=edge_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_node_coloring(emb, g, node_labels) + self.verify_edge_coloring(emb, g, node_labels) + + #again with list / iterable of edges + emb = subgraph.find_subgraph(list(g.edges), iter(g.edges), seed=random, node_labels=node_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_node_coloring(emb, g, node_labels) + + emb = subgraph.find_subgraph(list(g.edges), iter(g.edges), seed=random, edge_labels=edge_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_edge_coloring(emb, g, node_labels) + + emb = subgraph.find_subgraph(list(g.edges), iter(g.edges), seed=random, node_labels=node_labels, edge_labels=edge_labels) + verify_embedding(as_embedding(emb), g, g) + self.verify_node_coloring(emb, g, node_labels) + self.verify_edge_coloring(emb, g, node_labels) + From 72067f242df77ccb7a7004e39d0ee52e391772de Mon Sep 17 00:00:00 2001 From: Theodor Isacsson Date: Thu, 16 Oct 2025 14:25:59 -0700 Subject: [PATCH 120/127] Add Python 3.14 and remove 3.9 support --- .circleci/config.yml | 19 ++++++++----------- pyproject.toml | 4 ++-- requirements.txt | 6 +++--- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e9610d01..d51e873b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,19 +9,14 @@ commands: parameters: cibw-version: type: string - default: 2.21.2 + default: 3.2.1 steps: - run: name: run cibuildwheel shell: bash -eo pipefail command: | - if [[ $OS == Windows_NT ]]; then - python -m pip install --user cibuildwheel==<< parameters.cibw-version >> - python -m cibuildwheel --output-dir dist - else - python3 -m pip install --user cibuildwheel==<< parameters.cibw-version >> - python3 -m cibuildwheel --output-dir dist - fi + python -m pip install --user cibuildwheel==<< parameters.cibw-version >> + python -m cibuildwheel --output-dir dist - store_artifacts: &store-artifacts path: ./dist @@ -84,7 +79,7 @@ jobs: type: string macos: - xcode: 15.3.0 + xcode: 16.4.0 resource_class: macos.m1.medium.gen1 environment: @@ -210,7 +205,7 @@ workflows: - build-and-test-linux: &build matrix: parameters: - python-version: &python-versions [3.9.4, 3.10.0, 3.11.0, 3.12.0, 3.13.0] + python-version: &python-versions [3.10.0, 3.11.0, 3.12.0, 3.13.0, 3.14.0] - build-and-test-linux-aarch64: matrix: parameters: @@ -235,13 +230,15 @@ workflows: "dwave-networkx fasteners homebase networkx numpy scipy", # latest ] exclude: - # SciPy 1.7.3 doesn't support Python 3.11 + # SciPy 1.7.3 doesn't support Python 3.11+ - python-version: 3.11.0 dependency-versions: "dwave-networkx==0.8.13 fasteners==0.15 homebase==1.0.1 networkx==2.4 oldest-supported-numpy scipy==1.7.3" - python-version: 3.12.0 dependency-versions: "dwave-networkx==0.8.13 fasteners==0.15 homebase==1.0.1 networkx==2.4 oldest-supported-numpy scipy==1.7.3" - python-version: 3.13.0 dependency-versions: "dwave-networkx==0.8.13 fasteners==0.15 homebase==1.0.1 networkx==2.4 oldest-supported-numpy scipy==1.7.3" + - python-version: 3.14.0 + dependency-versions: "dwave-networkx==0.8.13 fasteners==0.15 homebase==1.0.1 networkx==2.4 oldest-supported-numpy scipy==1.7.3" deploy: jobs: diff --git a/pyproject.toml b/pyproject.toml index 664cef1c..2b0a25a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,14 +23,14 @@ classifiers = [ "Operating System :: MacOS", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">= 3.9" +requires-python = ">= 3.10" dependencies = [ "dwave-networkx>=0.8.13", "fasteners>=0.15", diff --git a/requirements.txt b/requirements.txt index 7d2bd900..fefa5766 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ -# working environment for Python>=3.8, <=3.12 -numpy==2.0.2 -scipy==1.13.1 +numpy==2.1.3 +scipy==1.15.3; python_version < '3.11' +scipy==1.16.2; python_version >= '3.11' networkx==3.2.1 dwave-networkx==0.8.15 fasteners==0.19 From 449c067219587186990fc7b92bda74af686f556b Mon Sep 17 00:00:00 2001 From: Theodor Isacsson Date: Fri, 17 Oct 2025 16:09:46 -0700 Subject: [PATCH 121/127] Set default start method for multiprocessing to 'fork' --- .../notes/add-py314-remove-py39-8d94fb3c5d57789a.yaml | 7 +++++++ tests/test_lib.py | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-py314-remove-py39-8d94fb3c5d57789a.yaml diff --git a/releasenotes/notes/add-py314-remove-py39-8d94fb3c5d57789a.yaml b/releasenotes/notes/add-py314-remove-py39-8d94fb3c5d57789a.yaml new file mode 100644 index 00000000..627c45b2 --- /dev/null +++ b/releasenotes/notes/add-py314-remove-py39-8d94fb3c5d57789a.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Add support for Python 3.14. +upgrade: + - | + Drop support for Python 3.9. diff --git a/tests/test_lib.py b/tests/test_lib.py index c69ae0cd..c966be19 100644 --- a/tests/test_lib.py +++ b/tests/test_lib.py @@ -25,9 +25,14 @@ import sys import time import signal -import multiprocessing import unittest +import multiprocessing +# Default changed from 'fork' to 'forkserver' for POSIX systems in Python 3.14. +# See https://docs.python.org/3.14/whatsnew/3.14.html#deprecated for details. +if os.name == "posix": + multiprocessing.set_start_method("fork") + # Given that this test is in the tests directory, the calibration data should be # in a sub directory. Use the path of this source file to find the calibration calibration_dir = os.path.join(os.path.dirname( From 52a897a1195c5d52502fc6edd43d1385529bdeef Mon Sep 17 00:00:00 2001 From: Theodor Isacsson Date: Tue, 28 Oct 2025 16:09:52 -0700 Subject: [PATCH 122/127] Bump release to 0.2.20 --- minorminer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/__init__.py b/minorminer/__init__.py index 00404736..2534009b 100644 --- a/minorminer/__init__.py +++ b/minorminer/__init__.py @@ -16,4 +16,4 @@ from minorminer.minorminer import miner, VARORDER, find_embedding -__version__ = "0.2.19" +__version__ = "0.2.20" From 4860b344de76d4f6afd4fba74833909b43dd3016 Mon Sep 17 00:00:00 2001 From: Theodor Isacsson Date: Tue, 28 Oct 2025 16:11:51 -0700 Subject: [PATCH 123/127] Update releasenote entry to fix reno parsing --- releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml b/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml index 82bb6672..8769705a 100644 --- a/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml +++ b/releasenotes/notes/add-glasgow-colors-f139f9d91fce3839.yaml @@ -1,6 +1,6 @@ --- features: - new coloring features for subgraph matching (nodes or directed edges can be assigned labels with the `node_labels` and `edge_labels` arguments to `subgraph.find_subgraph`) - - `subgraph.find_subgraph` accepts an additional new argument `as_embedding` to return a dict with iterable values similar to other embedding tools + - add additional new argument `as_embedding` to `subgraph.find_subgraph` to return a dict with iterable values similar to other embedding tools issues: - the `timeout` argument in `subgraph.find_subgraph` seems to be broken From 1c088d66df453cdc3e49db3db34852a612ff260a Mon Sep 17 00:00:00 2001 From: Kelly Boothby Date: Wed, 29 Oct 2025 09:34:30 -0700 Subject: [PATCH 124/127] Add keyboard interrupts for subgraph.find_subgraph (#275) * added ability to interrupt find_subgraph via ctrl-c --- external/glasgow-subgraph-solver | 2 +- minorminer/subgraph.pyx | 28 ++++++++++++++++--- .../glasgow-cancel-737ef7ebd8312635.yaml | 3 ++ 3 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/glasgow-cancel-737ef7ebd8312635.yaml diff --git a/external/glasgow-subgraph-solver b/external/glasgow-subgraph-solver index 3535531f..e544b549 160000 --- a/external/glasgow-subgraph-solver +++ b/external/glasgow-subgraph-solver @@ -1 +1 @@ -Subproject commit 3535531f1ee88942faf5f18885794f7917bb6261 +Subproject commit e544b549c0e669248832c3c50fc4a0bf8857e33a diff --git a/minorminer/subgraph.pyx b/minorminer/subgraph.pyx index dc7c7ee3..f25649a8 100644 --- a/minorminer/subgraph.pyx +++ b/minorminer/subgraph.pyx @@ -62,6 +62,20 @@ cdef class _labeldict(dict): return self._label[k] +cdef extern from "" namespace "std" nogil: + cdef cppclass atomic_bool "std::atomic": + void store(bool) + +ctypedef void(*sig_handler)(int) + +cdef extern from "" nogil: + cdef int SIGINT + cdef sig_handler signal(int, sig_handler) + +cdef atomic_bool _interrupt_find_subgraph_q +cdef void _interrupt_find_subgraph(int _): + _interrupt_find_subgraph_q.store(True) + cdef extern from "" namespace "std::chrono" nogil: cdef cppclass time_point[T]: pass @@ -102,7 +116,7 @@ cdef extern from "" namespace "std" nogil: pass cdef cppclass unique_ptr[T]: pass - cdef shared_ptr[Timeout] make_shared_timeout "std::make_shared"(seconds) + cdef shared_ptr[Timeout] make_shared_timeout "std::make_shared"(seconds, atomic_bool) cdef shared_ptr[InputGraph] make_shared[InputGraph](int, bool, bool) cdef InputGraph deref "*"(shared_ptr[InputGraph]) cdef unique_ptr[RestartsSchedule] make_no_restarts_schedule "std::make_unique"() @@ -408,16 +422,22 @@ def find_subgraph( if cliques_on_supplementals is not _default_kwarg: params.clique_size_constraints_on_supplementals = cliques_on_supplementals - params.timeout = make_shared_timeout(make_seconds(timeout)) + params.timeout = make_shared_timeout(make_seconds(timeout), _interrupt_find_subgraph_q) params.start_time = steady_clock_now() check_kwargs = {k: v for k, v in check_kwargs.items() if v is not _default_kwarg} if check_kwargs: raise ValueError(f"unused parameters: {list(check_kwargs.keys())}") - cdef HomomorphismResult result; + cdef HomomorphismResult result + cdef sig_handler prev_handler if len(source_labels) + len(source_isolated) <= len(target_labels) + len(target_isolated) or injectivity != 'injective': - result = solve_sip_by_decomposition(deref(source_g), deref(target_g), params) + _interrupt_find_subgraph_q.store(False) + prev_handler = signal(SIGINT, _interrupt_find_subgraph) + try: + result = solve_sip_by_decomposition(deref(source_g), deref(target_g), params) + finally: + signal(SIGINT, prev_handler) emb = dict((source_labels.label(s), target_labels.label(t)) for s, t in result.mapping) else: emb = {} diff --git a/releasenotes/notes/glasgow-cancel-737ef7ebd8312635.yaml b/releasenotes/notes/glasgow-cancel-737ef7ebd8312635.yaml new file mode 100644 index 00000000..47731bbc --- /dev/null +++ b/releasenotes/notes/glasgow-cancel-737ef7ebd8312635.yaml @@ -0,0 +1,3 @@ +--- +features: + - add the ability to interrupt subgraph.find_subgraph with ctrl-c From 82233015e96495a7b6d45fed90d8a1c80038fa0a Mon Sep 17 00:00:00 2001 From: Theodor Isacsson Date: Wed, 29 Oct 2025 09:55:19 -0700 Subject: [PATCH 125/127] Increment version for release --- minorminer/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/minorminer/__init__.py b/minorminer/__init__.py index 2534009b..57121dbd 100644 --- a/minorminer/__init__.py +++ b/minorminer/__init__.py @@ -16,4 +16,4 @@ from minorminer.minorminer import miner, VARORDER, find_embedding -__version__ = "0.2.20" +__version__ = "0.2.21" From c14b7da13534e5fa119d336bfd5e751a388ccc85 Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Fri, 14 Nov 2025 11:35:47 -0800 Subject: [PATCH 126/127] Include utils.zephyr in packages --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 48d03168..00ca87ae 100644 --- a/setup.py +++ b/setup.py @@ -145,6 +145,7 @@ 'minorminer.utils', 'minorminer._extern', 'minorminer._extern.rpack', + 'minorminer.utils.zephyr', ], include_package_data=True, ) From d8c979050485936ff4bf44293dbc00e1b938a61f Mon Sep 17 00:00:00 2001 From: Mahdieh Malekian Date: Wed, 19 Nov 2025 10:36:57 -0800 Subject: [PATCH 127/127] Apply suggestions from code review --- minorminer/utils/zephyr/__init__.py | 3 + minorminer/utils/zephyr/coordinate_systems.py | 9 +- minorminer/utils/zephyr/node_edge.py | 31 +- minorminer/utils/zephyr/plane_shift.py | 29 +- tests/utils/zephyr/test_coordinate_systems.py | 16 +- tests/utils/zephyr/test_node_edge.py | 538 +++++++++--------- tests/utils/zephyr/test_plane_shift.py | 38 +- 7 files changed, 333 insertions(+), 331 deletions(-) diff --git a/minorminer/utils/zephyr/__init__.py b/minorminer/utils/zephyr/__init__.py index b7b0a2b8..0ce76ef7 100644 --- a/minorminer/utils/zephyr/__init__.py +++ b/minorminer/utils/zephyr/__init__.py @@ -15,3 +15,6 @@ # ================================================================================================ from minorminer.utils.zephyr.zephyr import * +from minorminer.utils.zephyr.coordinate_systems import * +from minorminer.utils.zephyr.node_edge import * +from minorminer.utils.zephyr.plane_shift import * diff --git a/minorminer/utils/zephyr/coordinate_systems.py b/minorminer/utils/zephyr/coordinate_systems.py index ccf84965..2d1e0068 100644 --- a/minorminer/utils/zephyr/coordinate_systems.py +++ b/minorminer/utils/zephyr/coordinate_systems.py @@ -19,6 +19,9 @@ from collections import namedtuple +__all__ = ["cartesian_to_zephyr", "zephyr_to_cartesian", "CartesianCoord", "ZephyrCoord"] + + zephyr_fields = ["u", "w", "k", "j", "z"] ZephyrCoord = namedtuple("ZephyrCoord", zephyr_fields, defaults=(None,) * len(zephyr_fields)) cartesian_fields = ["x", "y", "k"] @@ -30,7 +33,8 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: """Converts a :class:`CartesianCoord` to its corresponding :class:`ZephyrCoord`. - Note: It assumes the given :class:`CartesianCoord` is valid. + ..note:: + It assumes the given :class:`CartesianCoord` is valid. Args: ccoord (CartesianCoord): The coodinate in Cartesian system to be converted. @@ -55,7 +59,8 @@ def cartesian_to_zephyr(ccoord: CartesianCoord) -> ZephyrCoord: def zephyr_to_cartesian(zcoord: ZephyrCoord) -> CartesianCoord: """Converts a :class:`ZephyrCoord` to its corresponding :class:`CartesianCoord`. - Note: It assumes the given ``zcoord`` is a valid Zephyr coordinate. + ..note:: + It assumes the given ``zcoord`` is a valid Zephyr coordinate. Args: zcoord (ZephyrCoord): The coodinate in Zephyr system to be converted. diff --git a/minorminer/utils/zephyr/node_edge.py b/minorminer/utils/zephyr/node_edge.py index 91872647..3426b874 100644 --- a/minorminer/utils/zephyr/node_edge.py +++ b/minorminer/utils/zephyr/node_edge.py @@ -20,11 +20,16 @@ from collections import namedtuple from enum import Enum from itertools import product -from typing import Callable, Generator, Iterable +from typing import Callable, Generator, Iterable, Any from minorminer.utils.zephyr.coordinate_systems import (CartesianCoord, ZephyrCoord, cartesian_to_zephyr, zephyr_to_cartesian) from minorminer.utils.zephyr.plane_shift import ZPlaneShift + + +__all__ = ["ZShape", "EdgeKind", "NodeKind", "Edge", "ZEdge", "ZNode"] + + ZShape = namedtuple("ZShape", ["m", "t"], defaults=(None, None)) @@ -41,28 +46,26 @@ class NodeKind(Enum): class Edge: - """Initializes an Edge with nodes x, y. + """Represents an edge of a graph in a canonical order. Args: - x : One endpoint of edge. - y : Another endpoint of edge. + x (Any): One endpoint of edge. + y (Any): Another endpoint of edge. + + ..note:: ``x`` and ``y`` must be mutually comparable. """ - def __init__( - self, - x, - y, - ) -> None: + def __init__(self, x: Any, y: Any) -> None: self._edge = self._set_edge(x, y) - def _set_edge(self, x, y): + def _set_edge(self, x: Any, y: Any): """Returns ordered tuple corresponding to the set {x, y}.""" if x < y: return (x, y) else: return (y, x) - def __hash__(self): + def __hash__(self) -> int: return hash(self._edge) def __getitem__(self, index: int) -> int: @@ -79,7 +82,7 @@ def __repr__(self) -> str: class ZEdge(Edge): - """Initializes a ZEdge with 'ZNode' nodes x, y. + """Represents an edge in a graph with Zephyr topology. Args: x (ZNode): Endpoint of edge. Must have same shape as ``y``. @@ -88,7 +91,7 @@ class ZEdge(Edge): Defaults to True. Raises: - TypeError: If either of x or y is not 'ZNode'. + TypeError: If either of x or y is not :class:`ZNode`. ValueError: If x, y do not have the same shape. ValueError: If x, y are not neighbors in a perfect yield (quotient) Zephyr graph. @@ -141,7 +144,7 @@ def edge_kind(self) -> EdgeKind: class ZNode: - """Initializes 'ZNode' with coord and optional shape. + """Represents a node of a graph with Zephyr topology with coordinate and optional shape. Args: coord (CartesianCoord | ZephyrCoord | tuple[int]): Coordinate in (quotient) Zephyr or (quotient) Cartesian. diff --git a/minorminer/utils/zephyr/plane_shift.py b/minorminer/utils/zephyr/plane_shift.py index f279d502..0e002a82 100644 --- a/minorminer/utils/zephyr/plane_shift.py +++ b/minorminer/utils/zephyr/plane_shift.py @@ -20,8 +20,10 @@ from typing import Iterator +__all__ = ["PlaneShift", "ZPlaneShift"] + class PlaneShift: - """Initializes PlaneShift with an x, y. + """Represents a displacement in a Cartesian plane. Args: x (int): The displacement in the x-direction of a Cartesian coordinate. @@ -32,7 +34,10 @@ class PlaneShift: >>> from minorminer.utils.zephyr.plane_shift import PlaneShift >>> ps1 = PlaneShift(1, 3) >>> ps2 = PlaneShift(2, -4) - >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") + >>> print(f"{ps1 + ps2 = }") + >>> print(f"{2 * ps1 = }") + ps1 + ps2 = PlaneShift(3, -1) + 2 * ps1 = PlaneShift(2, 6) """ def __init__(self, x: int, y: int) -> None: self._xy = (x, y) @@ -104,31 +109,17 @@ def __eq__(self, other: PlaneShift) -> bool: class ZPlaneShift(PlaneShift): - """Initializes ZPlaneShift with an x, y. + """Represents a displacement in the Zephyr quotient plane (expressed in Cartesian coordinates). Args: x (int): The displacement in the x-direction of a Cartesian coordinate. y (int): The displacement in the y-direction of a Cartesian coordinate. Raises: - TypeError: If x or y is not 'int'. - ValueError: If x and y have different parity. - - Example: - >>> from minorminer.utils.zephyr.plane_shift import ZPlaneShift - >>> ps1 = ZPlaneShift(1, 3) - >>> ps2 = ZPlaneShift(2, -4) - >>> print(f"{ps1 + ps2 = }, {2*ps1 = }") + ValueError: If ``x`` and ``y`` have different parity. """ - def __init__( - self, - x: int, - y: int, - ) -> None: - for shift in [x, y]: - if not isinstance(shift, int): - raise TypeError(f"Expected {shift} to be 'int', got {type(shift)}") + def __init__(self, x: int, y: int) -> None: if x % 2 != y % 2: raise ValueError( f"Expected x, y to have the same parity, got {x, y}" diff --git a/tests/utils/zephyr/test_coordinate_systems.py b/tests/utils/zephyr/test_coordinate_systems.py index bfff3e3d..6c008994 100644 --- a/tests/utils/zephyr/test_coordinate_systems.py +++ b/tests/utils/zephyr/test_coordinate_systems.py @@ -17,6 +17,8 @@ import unittest +from parameterized import parameterized + from minorminer.utils.zephyr.coordinate_systems import ( CartesianCoord, ZephyrCoord, @@ -33,14 +35,18 @@ def test_cartesian_to_zephyr_runs(self): for ccoord in ccoords: cartesian_to_zephyr(ccoord=ccoord) - def test_cartesian_to_zephyr(self): - self.assertEqual( - ZephyrCoord(0, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(0, 1, None)) + @parameterized.expand( + [ + ((0, 0, None, 0, 0), (0, 1, None)), + ((1, 0, None, 0, 0), (1, 0, None)), + ((1, 6, 3, 0, 1), (5, 12, 3)), + ] ) + def test_cartesian_to_zephyr(self, zcoord, ccoord): self.assertEqual( - ZephyrCoord(1, 0, None, 0, 0), cartesian_to_zephyr(CartesianCoord(1, 0, None)) + ZephyrCoord(*zcoord), cartesian_to_zephyr(CartesianCoord(*ccoord)) ) - self.assertEqual(CartesianCoord(5, 12, 3), zephyr_to_cartesian(ZephyrCoord(1, 6, 3, 0, 1))) + self.assertEqual(CartesianCoord(*ccoord), zephyr_to_cartesian(ZephyrCoord(*zcoord))) def test_zephyr_to_cartesian_runs(self): uwkjzs = [(0, 2, 4, 1, 5), (1, 3, 3, 0, 0), (1, 2, None, 1, 5)] diff --git a/tests/utils/zephyr/test_node_edge.py b/tests/utils/zephyr/test_node_edge.py index 627c86ef..91bc8ecb 100644 --- a/tests/utils/zephyr/test_node_edge.py +++ b/tests/utils/zephyr/test_node_edge.py @@ -14,9 +14,9 @@ # # ================================================================================================ - import unittest -from itertools import product + +from parameterized import parameterized from minorminer.utils.zephyr.coordinate_systems import CartesianCoord, ZephyrCoord from minorminer.utils.zephyr.node_edge import Edge, EdgeKind, NodeKind, ZEdge, ZNode, ZShape @@ -38,28 +38,34 @@ def test_equal(self) -> None: class TestZEdge(unittest.TestCase): - def test_valid_input_runs(self) -> None: - valid_edges = [ - ( - ZNode(coord=ZephyrCoord(0, 10, 3, 1, 3), shape=ZShape(6, 4)), - ZNode(coord=ZephyrCoord(0, 10, 3, 1, 2), shape=ZShape(6, 4)), - ), - ( - ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), - ZNode(coord=CartesianCoord(4, 1, 2), shape=(6, 3)), - ), - (ZNode(coord=(1, 6)), ZNode(coord=(5, 6))), - ] - for x, y in valid_edges: - ZEdge(x, y) - - def test_invalid_input_raises_error(self): - with self.assertRaises((TypeError, ValueError)): - ZEdge(2, 4) - ZEdge( - ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), - ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), - ) + @parameterized.expand( + [ + (ZephyrCoord(0, 10, 3, 1, 3), ZephyrCoord(0, 10, 3, 1, 2), ZShape(6, 4)), + (CartesianCoord(4, 3, 2), CartesianCoord(4, 1, 2), (6, 3)), + ((1, 6), (5, 6), None) + ] + ) + def test_valid_input_runs(self, x, y, shape) -> None: + zn_x = ZNode(coord=x, shape=shape) + zn_y = ZNode(coord=y, shape=shape) + + ZEdge(zn_x, zn_y) + + @parameterized.expand( + [ + ((2, 4), TypeError), + ((ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3)), ZNode(coord=CartesianCoord(4, 3, 2), shape=(6, 3))), ValueError), + ] + ) + def test_invalid_input_raises_error(self, invalid_edge, expected_err): + with self.assertRaises(expected_err): + ZEdge(*invalid_edge) + + +U_VALS = [0, 1] +J_VALS = [0, 1] +M_VALS = [1, 6, 12] +T_VALS = [1, 2, 4, 6] class TestZNode(unittest.TestCase): @@ -89,25 +95,42 @@ def setUp(self): self.right_down_xyms = [((1, 12), 3), ((16, 1), 4)] self.midgrid_xyms = [((5, 2), 6), ((5, 4), 6), ((6, 7), 5)] - def test_zephyr_node_runs(self) -> None: - for xy, m in self.xyms: - ZNode(xy, ZShape(m=m)) - ZNode(xy, ZShape(m=m), convert_to_z=True) - - for u in self.u_vals: - for j in self.j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - z_vals = range(m) - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - def test_zephyr_node_invalid_args_raises_error(self) -> None: - invalid_xyms = [ + @parameterized.expand( + [ + ((u, w, k, j, z), (m, t)) + for u in U_VALS + for j in J_VALS + for m in M_VALS + for t in T_VALS + for w in range(2 * m + 1) + for k in range(t) + for z in range(m) + ][::100] + ) + def test_znode_zcoord_runs(self, uwkjz, mt): + ZNode(coord=uwkjz, shape=mt) + + @parameterized.expand( + [ + ((0, 3), 1), + ((5, 2), 6), + ((16, 1), 4), + ((1, 12), 3), + ((3, 0), 4), + ((5, 4), 6), + ((0, 3), 6), + ((6, 3), 5), + ((3, 0), None), + ((5, 4), None), + ((0, 3), None), + ] + ) + def test_znode_ccoord_runs(self, xy, m): + ZNode(xy, ZShape(m=m)) + ZNode(xy, ZShape(m=m), convert_to_z=True) + + @parameterized.expand( + [ ((-1, 2), 6), ((6, -1), 4), ((1, 3), -5), @@ -118,249 +141,210 @@ def test_zephyr_node_invalid_args_raises_error(self) -> None: ((0, 0, 0), 1), ((-1, 2), None), ] - for xy, m in invalid_xyms: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=xy, shape=ZShape(m=m)) - # All good except u_vals - for u in self.invalid_u_vals: - for j in self.j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - z_vals = range(m) - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - # All good except w_vals - for u in self.u_vals: - for j in self.j_vals: - for m in self.m_vals: - z_vals = range(m) - for w in self.invalid_w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - # All good except k_vals - for u in self.u_vals: - for j in self.j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - z_vals = range(m) - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - invalid_k_vals = [-1, t, 2.5, None] - for k in invalid_k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - # All good except j_vals - for u in self.u_vals: - for j in self.invalid_j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - z_vals = range(m) - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - # All good except z_vals - for u in self.u_vals: - for j in self.j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - invalid_z_vals = [None, -1, m, 1.5] - for w in w_vals: - for z in invalid_z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - # All good except m_vals - for u in self.u_vals: - for j in self.j_vals: - for m in self.invalid_m_vals: - w_vals = [0] - z_vals = [0] - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - # All good except t_vals - for u in self.u_vals: - for j in self.j_vals: - for m in self.invalid_m_vals: - w_vals = [0] - z_vals = [0] - for w in w_vals: - for z in z_vals: - for t in self.invalid_t_vals: - k_vals = [0, 1, 3] - for k in k_vals: - with self.assertRaises((ValueError, TypeError)): - ZNode(coord=(u, w, k, j, z), shape=(m, t)) - - def test_add_sub_runs(self) -> None: - left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] - lu_qps = ZPlaneShift(-1, -1) - right_down_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.right_down_xyms] - rd_qps = ZPlaneShift(1, 1) - midgrid_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.midgrid_xyms] - for pc in midgrid_pcs: - for s1, s2 in product((-2, 2), (-2, 2)): - pc + ZPlaneShift(s1, s2) - for pc in left_up_pcs: - with self.assertRaises(ValueError): - pc + lu_qps - for pc in right_down_pcs: - with self.assertRaises(ValueError): - pc + rd_qps - - def test_add_sub(self) -> None: - midgrid_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.midgrid_xyms] - right_down_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.right_down_xyms] - left_up_pcs = [ZNode(xy, ZShape(m=m)) for xy, m in self.left_up_xyms] - - for pc in midgrid_pcs + right_down_pcs + left_up_pcs: - self.assertEqual(pc + ZPlaneShift(0, 0), pc) - - def test_neighbors_generator_runs(self) -> None: - for _ in ZNode((1, 12, 4), ZShape(t=6)).neighbors_generator(): - _ + ) + def test_bad_args_raises_error_ccoord(self, xy, m): + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=xy, shape=ZShape(m=m)) + + @parameterized.expand( + [ + ((2, 0, 0, 0, 0), (6, 4)), # All good except u_val + ((None, 3, 0, 0, 4), (6, 1)), # All good except u_val + ((3.5, 8, 2, 0, 0), (8, 4)), # All good except u_val + ((0, -1, 1, 1, 3), (6, 4)), # All good except w_val + ((0, 10, 2.5, 0, 5), (6, 4)), # All good except k_val + ((1, 23, -1, 1, 5), (12, 2)), # All good except k_val + ((1, 24, 1, 3.5, 9), (12, 4)), # All good except j_val + ((1, 24, 0, 1, None), (12, 2)), # All good except z_val + ((1, 20, 3, 1, 12), (12, 6)), # All good except z_val + ((0, 0, 0, 0, 0), (0, 0)), # All good except m_val + ((0, 0, 0, 0, 0), (1, -1)), # All good except t_val + ] + ) + def test_bad_args_raises_error_ccoord(self, uwkjz, mt): + with self.assertRaises((ValueError, TypeError)): + ZNode(coord=uwkjz, shape=mt) + + @parameterized.expand( + [ + ((0, 3), 6, ZPlaneShift(-1, -1)), + ((0, 3), None, ZPlaneShift(-1, -1)), + ((11, 0), None, ZPlaneShift(-1, -1)), + ((0, 5), 8, ZPlaneShift(-1, -1)), + ((1, 12), 3, ZPlaneShift(1, 1)), + ((16, 1), 4, ZPlaneShift(1, 1)), + ] + ) + def test_add_sub_raises_error_invalid(self, xy, m, zps) -> None: + zn = ZNode(xy, ZShape(m=m)) + with self.assertRaises(ValueError): + zn + zps + + @parameterized.expand( + [ + (ZNode((5, 2), ZShape(6)), ZPlaneShift(-2, -2), ZNode((3, 0), ZShape(6))), + (ZNode((5, 2), ZShape(6)), ZPlaneShift(2, -2), ZNode((7, 0), ZShape(6))), + (ZNode((5, 2), ZShape(6)), ZPlaneShift(2, 2), ZNode((7, 4), ZShape(6))), + (ZNode((5, 2), ZShape(6)), ZPlaneShift(-2, 2), ZNode((3, 4), ZShape(6))), + (ZNode((5, 4), ZShape(4)), ZPlaneShift(-4, -4), ZNode((1, 0), ZShape(4))), + (ZNode((6, 7), ZShape(5)), ZPlaneShift(1, 11), ZNode((7, 18), ZShape(5))), + (ZNode((0, 3), ZShape(6)), ZPlaneShift(0, 0), ZNode((0, 3), ZShape(6))), + (ZNode((1, 12), ZShape(3)), ZPlaneShift(1, -1), ZNode((2, 11), ZShape(3))), + ] + ) + def test_add_sub(self, zn, zps, expected) -> None: + self.assertEqual(zn + zps, expected) + + def test_neighbors_boundary(self) -> None: + x, y, k, t = 1, 12, 4, 6 + expected_nbrs = { + ZNode((x + i, y + j, kp), ZShape(t=t)) + for i in (-1, 1) + for j in (-1, 1) + for kp in range(t) + } + expected_nbrs |= {ZNode((x + 2, y, k), ZShape(t=t)), ZNode((x + 4, y, k), ZShape(t=t))} + self.assertEqual(set(ZNode((x, y, k), ZShape(t=t)).neighbors()), expected_nbrs) + + def test_neighbors_mid(self): + x, y, k, t = 10, 5, 3, 6 + expected_nbrs = { + ZNode((x + i, y + j, kp), ZShape(t=t)) + for i in (-1, 1) + for j in (-1, 1) + for kp in range(t) + } + expected_nbrs |= {ZNode((x, y + 4, k), ZShape(t=t)), ZNode((x, y - 4, k), ZShape(t=t))} + expected_nbrs |= {ZNode((x, y + 2, k), ZShape(t=t)), ZNode((x, y - 2, k), ZShape(t=t))} + self.assertEqual(set(ZNode((x, y, k), ZShape(t=t)).neighbors()), expected_nbrs) def test_zcoord(self) -> None: ZNode((11, 12, 4), ZShape(t=6)).zcoord == ZephyrCoord(1, 0, 4, 0, 2) ZNode((1, 0)).zcoord == ZephyrCoord(1, 0, None, 0, 0) ZNode((0, 1)).zcoord == ZephyrCoord(0, 0, None, 0, 0) - def test_direction_node_kind(self) -> None: - for u in self.u_vals: - for j in self.j_vals: - for m in self.m_vals: - w_vals = range(2 * m + 1) - z_vals = range(m) - for w in w_vals: - for z in z_vals: - for t in self.t_vals: - k_vals = range(t) - for k in k_vals: - zn = ZNode(coord=(u, w, k, j, z), shape=(m, t)) - self.assertEqual(zn.direction, u) - if u == 0: - self.assertTrue(zn.is_vertical()) - self.assertEqual(zn.node_kind, NodeKind.VERTICAL) - else: - self.assertTrue(zn.is_horizontal()) - self.assertEqual(zn.node_kind, NodeKind.HORIZONTAL) - - def test_neighbor_kind(self) -> None: - zn = ZNode((0, 1)) - self.assertTrue(zn.neighbor_kind(ZNode((1, 0))) is EdgeKind.INTERNAL) - self.assertTrue(zn.neighbor_kind(ZNode((1, 2))) is EdgeKind.INTERNAL) - self.assertTrue(zn.neighbor_kind(ZNode((0, 3))) is EdgeKind.ODD) - self.assertTrue(zn.neighbor_kind(ZNode((0, 5))) is EdgeKind.EXTERNAL) - self.assertTrue(zn.neighbor_kind(ZNode((0, 7))) is None) - self.assertTrue(zn.neighbor_kind(ZNode((1, 6))) is None) - - def test_internal_generator(self) -> None: - zn1 = ZNode((0, 1)) - set_internal1 = {x for x in zn1.internal_neighbors_generator()} - expected1 = {ZNode((1, 0)), ZNode((1, 2))} - self.assertEqual(set_internal1, expected1) - - zn2 = ZNode((0, 1, 0), ZShape(t=4)) - set_internal2 = {x for x in zn2.internal_neighbors_generator()} - expected2 = {ZNode((1, 0, k), ZShape(t=4)) for k in range(4)} | { - ZNode((1, 2, k), ZShape(t=4)) for k in range(4) - } - self.assertEqual(set_internal2, expected2) - - def test_external_generator(self) -> None: - zn = ZNode((0, 1)) + @parameterized.expand( + [ + ((u, w, k, j, z), (m, t)) + for u in U_VALS + for j in J_VALS + for m in M_VALS + for t in T_VALS + for w in range(2 * m + 1) + for k in range(t) + for z in range(m) + ][::300] + ) + def test_direction(self, uwkjz, mt) -> None: + zn = ZNode(coord=uwkjz, shape=mt) + self.assertEqual(zn.direction, uwkjz[0]) + + @parameterized.expand( + [ + ((u, w, k, j, z), (m, t)) + for u in U_VALS + for j in J_VALS + for m in M_VALS + for t in T_VALS + for w in range(2 * m + 1) + for k in range(t) + for z in range(m) + ][::200] + ) + def test_node_kind(self, uwkjz, mt) -> None: + zn = ZNode(coord=uwkjz, shape=mt) + if uwkjz[0] == 0: + self.assertTrue(zn.is_vertical()) + self.assertEqual(zn.node_kind, NodeKind.VERTICAL) + else: + self.assertTrue(zn.is_horizontal()) + self.assertEqual(zn.node_kind, NodeKind.HORIZONTAL) + + @parameterized.expand( + [ + (ZNode((1, 0)), EdgeKind.INTERNAL), + (ZNode((1, 2)), EdgeKind.INTERNAL), + (ZNode((0, 3)), EdgeKind.ODD), + (ZNode((0, 5)), EdgeKind.EXTERNAL), + (ZNode((0, 7)), None), + (ZNode((1, 6)), None), + ] + ) + def test_neighbor_kind(self, zn1, nbr_kind) -> None: + zn0 = ZNode((0, 1)) + self.assertIs(zn0.neighbor_kind(zn1), nbr_kind) + + @parameterized.expand( + [ + (ZNode((0, 1)), {ZNode((1, 0)), ZNode((1, 2))}), + ( + ZNode((0, 1, 0), ZShape(t=4)), + {ZNode((1, 0, k), ZShape(t=4)) for k in range(4)} + | {ZNode((1, 2, k), ZShape(t=4)) for k in range(4)}, + ), + ] + ) + def test_internal_generator(self, zn, expected) -> None: + set_internal = {x for x in zn.internal_neighbors_generator()} + self.assertEqual(set_internal, expected) + + @parameterized.expand( + [ + (ZNode((0, 1)), {ZNode((0, 5))}), + (ZNode((0, 1, 2), ZShape(t=4)), {ZNode((0, 5, 2), ZShape(t=4))}), + ( + ZNode((11, 6, 3), ZShape(t=4)), + {ZNode((7, 6, 3), ZShape(t=4)), ZNode((15, 6, 3), ZShape(t=4))}, + ), + ] + ) + def test_external_generator(self, zn, expected) -> None: set_external = {x for x in zn.external_neighbors_generator()} - expected = {ZNode((0, 5))} self.assertEqual(set_external, expected) - zn2 = ZNode((0, 1, 2), ZShape(t=4)) - set_external2 = {x for x in zn2.external_neighbors_generator()} - expected2 = {ZNode((0, 5, 2), ZShape(t=4))} - self.assertEqual(set_external2, expected2) - - def test_odd_generator(self) -> None: - zn = ZNode((0, 1)) + @parameterized.expand( + [ + (ZNode((0, 1)), {ZNode((0, 3))}), + (ZNode((0, 1, 2), ZShape(t=4)), {ZNode((0, 3, 2), ZShape(t=4))}), + (ZNode((15, 8)), {ZNode((13, 8)), ZNode((17, 8))}), + ] + ) + def test_odd_generator(self, zn, expected) -> None: set_odd = {x for x in zn.odd_neighbors_generator()} - expected = {ZNode((0, 3))} self.assertEqual(set_odd, expected) - zn2 = ZNode((0, 1, 2), ZShape(t=4)) - set_odd2 = {x for x in zn2.odd_neighbors_generator()} - expected2 = {ZNode((0, 3, 2), ZShape(t=4))} - self.assertEqual(set_odd2, expected2) - - def test_degree(self) -> None: - for (x, y), m in self.midgrid_xyms: - qzn1 = ZNode(coord=(x, y), shape=ZShape(m=m)) - self.assertEqual(len(qzn1.neighbors()), 8) - self.assertEqual(qzn1.degree(), 8) - self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.INTERNAL), 4) - self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(qzn1.degree(nbr_kind=EdgeKind.ODD), 2) - for t in [1, 4, 6]: - zn1 = ZNode(coord=(x, y, 0), shape=ZShape(m=m, t=t)) - self.assertEqual(zn1.degree(), 4 * t + 4) - self.assertEqual(zn1.degree(nbr_kind=EdgeKind.INTERNAL), 4 * t) - self.assertEqual(zn1.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(zn1.degree(nbr_kind=EdgeKind.ODD), 2) - - qzn2 = ZNode(coord=(0, 1)) - self.assertEqual(qzn2.degree(), 4) - self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.INTERNAL), 2) - self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.EXTERNAL), 1) - self.assertEqual(qzn2.degree(nbr_kind=EdgeKind.ODD), 1) - for t in [1, 4, 6]: - zn2 = ZNode(coord=(0, 1, 0), shape=ZShape(t=t)) - self.assertEqual(zn2.degree(), 2 * t + 2) - self.assertEqual(zn2.degree(nbr_kind=EdgeKind.INTERNAL), 2 * t) - self.assertEqual(zn2.degree(nbr_kind=EdgeKind.EXTERNAL), 1) - self.assertEqual(zn2.degree(nbr_kind=EdgeKind.ODD), 1) - - qzn3 = ZNode((24, 5), ZShape(m=6)) - self.assertEqual(qzn3.degree(), 6) - self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.INTERNAL), 2) - self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(qzn3.degree(nbr_kind=EdgeKind.ODD), 2) - for t in [1, 5, 6]: - zn3 = ZNode((24, 5, 0), ZShape(m=6, t=t)) - self.assertEqual(zn3.degree(), 2 * t + 4) - self.assertEqual(zn3.degree(nbr_kind=EdgeKind.INTERNAL), 2 * t) - self.assertEqual(zn3.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(zn3.degree(nbr_kind=EdgeKind.ODD), 2) - - qzn4 = ZNode((24, 5)) - self.assertEqual(qzn4.degree(), 8) - self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.INTERNAL), 4) - self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(qzn4.degree(nbr_kind=EdgeKind.ODD), 2) - for t in [1, 5, 6]: - zn4 = ZNode((24, 5, 0), ZShape(t=t)) - self.assertEqual(zn4.degree(), 4 * t + 4) - self.assertEqual(zn4.degree(nbr_kind=EdgeKind.INTERNAL), 4 * t) - self.assertEqual(zn4.degree(nbr_kind=EdgeKind.EXTERNAL), 2) - self.assertEqual(zn4.degree(nbr_kind=EdgeKind.ODD), 2) + @parameterized.expand( + [ + ((5, 2), 6, None, 4, 4), + ((5, 2), 6, EdgeKind.INTERNAL, 4, 0), + ((5, 2), 10, EdgeKind.EXTERNAL, 0, 2), + ((5, 2), 3, EdgeKind.ODD, 0, 2), + ((6, 7), 12, None, 4, 4), + ((6, 7), 8, EdgeKind.INTERNAL, 4, 0), + ((6, 7), 12, EdgeKind.EXTERNAL, 0, 2), + ((6, 7), 12, EdgeKind.ODD, 0, 2), + ((0, 1), 4, EdgeKind.INTERNAL, 2, 0), + ((0, 1), 2, EdgeKind.ODD, 0, 1), + ((0, 1), 4, EdgeKind.EXTERNAL, 0, 1), + ((0, 1), 4, None, 2, 2), + ((24, 5), None, EdgeKind.INTERNAL, 4, 0), + ((24, 5), None, EdgeKind.ODD, 0, 2), + ((24, 5), None, EdgeKind.EXTERNAL, 0, 2), + ((24, 5), None, None, 4, 4), + ((24, 5), 6, EdgeKind.INTERNAL, 2, 0), + ((24, 5), 8, EdgeKind.ODD, 0, 2), + ((24, 5), 6, EdgeKind.EXTERNAL, 0, 2), + ((24, 5), 8, None, 4, 4), + ] + ) + def test_degree(self, xy, m, nbr_kind, a, b) -> None: + for t in [None, 1, 4, 6]: + if t is None: + coord, t_p = xy, 1 + else: + coord, t_p = xy + (0, ), t + zn = ZNode(coord=coord, shape=ZShape(m=m, t=t)) + + with self.subTest(case=t): + self.assertEqual(zn.degree(nbr_kind=nbr_kind), a * t_p + b) + diff --git a/tests/utils/zephyr/test_plane_shift.py b/tests/utils/zephyr/test_plane_shift.py index 03f285ad..1958cc82 100644 --- a/tests/utils/zephyr/test_plane_shift.py +++ b/tests/utils/zephyr/test_plane_shift.py @@ -18,6 +18,8 @@ import unittest from itertools import combinations +from parameterized import parameterized + from minorminer.utils.zephyr.plane_shift import PlaneShift, ZPlaneShift @@ -52,13 +54,18 @@ def test_add(self) -> None: PlaneShift(*s0) + PlaneShift(*s1), PlaneShift(s0[0] + s1[0], s0[1] + s1[1]) ) - def test_mul(self) -> None: - self.assertEqual(-1 * PlaneShift(0, -4), PlaneShift(0, 4)) - self.assertEqual(3 * PlaneShift(2, 10), PlaneShift(6, 30)) + @parameterized.expand( + [ + (-1, PlaneShift(0, -4), PlaneShift(0, 4)), + (3, PlaneShift(2, 10), PlaneShift(6, 30)), + ] + ) + def test_mul(self, c, ps, expected) -> None: + self.assertEqual(c * ps, expected) -class TestZPlaneShift(TestPlaneShift): +class TestZPlaneShift(unittest.TestCase): def setUp(self) -> None: self.shifts = [ (0, 2), @@ -75,16 +82,19 @@ def test_valid_input_runs(self) -> None: for shift in self.shifts: ZPlaneShift(*shift) - def test_invalid_input_gives_error(self) -> None: - invalid_input_types = [5, "NE", (0, 2, None), (2, 0.5), (-4, 6.0)] - with self.assertRaises(TypeError): - for invalid_type_ in invalid_input_types: - ZPlaneShift(*invalid_type_) - - invalid_input_vals = [(4, 1), (0, 1)] - with self.assertRaises(ValueError): - for invalid_val_ in invalid_input_vals: - ZPlaneShift(*invalid_val_) + @parameterized.expand( + [ + (5, TypeError), + ("NE", TypeError), + ((0, 2, None), TypeError), + ((2, 0.5), ValueError), + ((4, 1), ValueError), + ((0, 1), ValueError), + ] + ) + def test_invalid_input_gives_error(self, invalid, expected_err) -> None: + with self.assertRaises(expected_err): + ZPlaneShift(*invalid) def test_eq(self): self.assertNotEqual(PlaneShift(0, 0), ZPlaneShift(0, 0))