Source code for mindquantum.device.topology

# Copyright 2023 Huawei Technologies Co., Ltd
#
# 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.
# ============================================================================
"""Qubit node an topology of quantum chip."""

import copy
import numbers
import sys
import typing
from typing import Optional

# pylint: disable=wrong-import-position
if sys.version_info < (3, 8):
    from typing_extensions import Literal, get_args
else:
    from typing import Literal, get_args

from rich.console import Console

from mindquantum.utils.type_value_check import _check_input_type, _check_int_type

from ..mqbackend.device import QubitNode as QubitNode_
from ..mqbackend.device import QubitsTopology as QubitsTopology_

AVA_SHOW_METHOD = Literal['mpl', 'svg']  # pylint: disable=invalid-name


[docs]class QubitNode: """ Qubit node. A qubit node has a id, a position and a color (if you want to draw it). You can connect two qubits with '>>' or '<<' and disconnect two qubits with '<' or '>'. Args: qubit_id (int): the identity number of this qubit. color (str): the face color of this qubit. poi_x (float): x position in canvas. poi_y (float): y position in canvas. Examples: >>> from mindquantum.device import QubitNode >>> q0 = QubitNode(0) >>> q1 = QubitNode(1) >>> q = q0 << q1 >>> q0.qubit_id == q.qubit_id True """ def __init__(self, qubit_id: int, color: str = '#000000', poi_x: float = 0.0, poi_y: float = 0.0) -> None: """Initialize a qubit node.""" _check_int_type("qubit_id", qubit_id) _check_input_type("color", str, color) _check_input_type("poi_x", numbers.Real, poi_x) _check_input_type("poi_y", numbers.Real, poi_y) self.id_ = qubit_id self.color_ = color self.poi_x_ = poi_x self.poi_y_ = poi_y self.neighbor: typing.Set[int] = set() def __get_cpp_obj__(self): """Get cpp obj.""" return QubitNode_(self.id_, self.color_, self.poi_x_, self.poi_y_, self.neighbor) def __gt__(self, other: "QubitNode") -> "QubitNode": """ Disconnect with other qubit node and return rhs. Args: other (:class:`~.device.QubitNode`): qubit node you want to disconnect with. Returns: :class:`~.device.QubitNode`, the right hand size qubit. Examples: >>> from mindquantum.device import QubitNode >>> from mindquantum.device import QubitsTopology >>> q0 = QubitNode(0) >>> q1 = QubitNode(1) >>> topology = QubitsTopology([q0, q1]) >>> q0 << q1 >>> topology.is_coupled_with(0, 1) True >>> q0 > q1 >>> topology.is_coupled_with(0, 1) False """ if self.qubit_id == other.qubit_id: raise RuntimeError("Cannot disconnect itself.") if other.qubit_id in self.neighbor: self.neighbor.remove(other.qubit_id) if self.qubit_id in other.neighbor: other.neighbor.remove(self.qubit_id) return other def __int__(self) -> int: """ Convert the qubit to int. Returns: int, the id of qubit. Examples: >>> from mindquantum.device import QubitNode >>> q0 = QubitNode(0) >>> int(q0) 0 """ return self.qubit_id def __lshift__(self, other: "QubitNode") -> "QubitNode": """ Connect with other qubit node and return lhs. Args: other (:class:`~.device.QubitNode`): qubit node you want to connect with. Returns: :class:`~.device.QubitNode`, the left hand size qubit. Examples: >>> from mindquantum.device import QubitNode >>> from mindquantum.device import QubitsTopology >>> q0 = QubitNode(0) >>> q1 = QubitNode(1) >>> topology = QubitsTopology([q0, q1]) >>> q = q0 << q1 >>> q.qubit_id == q0.qubit_id True >>> topology.is_coupled_with(0, 1) True """ _check_input_type("other", QubitNode, other) if self.qubit_id == other.qubit_id: raise RuntimeError("Cannot disconnect itself.") self.neighbor.add(other.qubit_id) other.neighbor.add(self.qubit_id) return self def __lt__(self, other: "QubitNode") -> "QubitNode": """ Disconnect with other qubit node and return lhs. Args: other (:class:`~.device.QubitNode`): qubit node you want to connect with. Returns: :class:`~.device.QubitNode`, the left hand size qubit. Examples: >>> from mindquantum.device import QubitNode >>> from mindquantum.device import QubitsTopology >>> q0 = QubitNode(0) >>> q1 = QubitNode(1) >>> topology = QubitsTopology([q0, q1]) >>> q0 << q1 >>> topology.is_coupled_with(0, 1) True >>> q = q0 < q1 >>> topology.is_coupled_with(0, 1) False >>> q.qubit_id == q0.qubit_id True """ _check_input_type("other", QubitNode, other) if self.qubit_id == other.qubit_id: raise RuntimeError("Cannot disconnect itself.") if other.qubit_id in self.neighbor: self.neighbor.remove(other.qubit_id) if self.qubit_id in other.neighbor: other.neighbor.remove(self.qubit_id) return self def __rshift__(self, other: "QubitNode") -> "QubitNode": """ Connect with other qubit node and return rhs. Args: other (:class:`~.device.QubitNode`): qubit node you want to connect with. Returns: :class:`~.device.QubitNode`, the right hand side qubit. Examples: >>> from mindquantum.device import QubitNode >>> from mindquantum.device import QubitsTopology >>> q0 = QubitNode(0) >>> q1 = QubitNode(1) >>> topology = QubitsTopology([q0, q1]) >>> q = q0 >> q1 >>> q.qubit_id == q1.qubit_id True >>> topology.is_coupled_with(0, 1) True """ _check_input_type("other", QubitNode, other) if self.qubit_id == other.qubit_id: raise RuntimeError("Cannot connect itself.") self.neighbor.add(other.qubit_id) other.neighbor.add(self.qubit_id) return other
[docs] def set_color(self, color: str) -> None: """ Set the color of this qubit. Args: color (str): The new color. Examples: >>> from mindquantum.device import QubitNode >>> q0 = QubitNode(1) >>> q0.set_color('#ababab') >>> q0.color '#ababab' """ _check_input_type("color", str, color) self.color_ = color
[docs] def set_poi(self, poi_x: float, poi_y: float) -> None: """ Set the position of this qubit. Args: poi_x (float): x position of qubit in canvas. poi_y (float): y position of qubit in canvas. Examples: >>> from mindquantum.device import QubitNode >>> q0 = QubitNode(1, poi_x=0, poi_y=1) >>> q0.set_poi(1, 0) >>> print(q0.poi_x, q0.poi_y) 1 0 """ _check_input_type("poi_x", numbers.Real, poi_x) _check_input_type("poi_y", numbers.Real, poi_y) self.poi_x_, self.poi_y_ = poi_x, poi_y
@property def color(self) -> str: """ Get the color of this qubit. Returns: str, the color of qubit node. """ return self.color_ @property def qubit_id(self) -> int: """ Get the id of this qubit. Returns: int, the id of this qubit. """ return self.id_ @property def poi_x(self) -> float: """ X position of this qubit. Returns: float, the x position. """ return self.poi_x_ @property def poi_y(self) -> float: """ Y position of this qubit. Returns: float, the y position. """ return self.poi_y_
[docs]class QubitsTopology: """ Topology of qubit in physical device. Topology is construct by different :class:`~.device.QubitNode`, and you can set the property of each qubit node directly. Args: qubits (List[:class:`~.device.QubitNode`]): All qubit nodes in this topology. Examples: >>> from mindquantum.device import QubitsTopology >>> from mindquantum.device import QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[0] >> topology[1] >>> topology.is_coupled_with(0, 1) True >>> topology.set_color(0, "#121212") >>> topology[0].color '#121212' """ def __init__(self, qubits: typing.List[QubitNode]) -> None: """Initialize a physical qubit topology.""" _check_input_type("qubits", list, qubits) for qubit in qubits: _check_input_type("Element of qubits", QubitNode, qubit) self.qubits: typing.Dict[int, QubitNode] = {} for node in qubits: if node.qubit_id in self.qubits: raise ValueError(f"Qubit with id {node.qubit_id} already exists.") self.qubits[node.qubit_id] = node self.cached_edge_color: typing.Dict[typing.Tuple[int, int], str] = {} def __getitem__(self, qubit_id: int) -> QubitNode: """ Get qubit node base on qubit id. Args: qubit_id (int), the identity number of qubit you want. Returns: :class:`~.device.QubitNode`, the qubit node with given id. Examples: >>> from mindquantum.device import QubitsTopology >>> from mindquantum.device import QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[2].qubit_id 2 """ _check_int_type("qubit_id", qubit_id) return self.qubits[qubit_id] def __get_cpp_obj__(self): """Get cpp object.""" return QubitsTopology_([node.__get_cpp_obj__() for node in self.qubits.values()])
[docs] def add_qubit_node(self, qubit: QubitNode) -> None: """ Add a qubit node into this topology. Args: qubit (:class:`~.device.QubitNode`): the qubit you want to add into this topology. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(2)]) >>> topology.add_qubit_node(QubitNode(2)); >>> topology.all_qubit_id() {0, 1, 2} """ _check_input_type("qubit", QubitNode, qubit) if qubit.qubit_id in self.qubits: raise ValueError(f"Qubit with id {qubit.qubit_id} already exists.") self.qubits[qubit.qubit_id] = qubit
[docs] def all_qubit_id(self) -> typing.Set[int]: """ Get all qubit id. Returns: Set[int], all qubit id in this qubit topology. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(2)]) >>> topology.add_qubit_node(QubitNode(2)); >>> topology.all_qubit_id() {0, 1, 2} """ return set(self.qubits.keys())
[docs] def compress(self) -> typing.Tuple["QubitsTopology", typing.Dict[int, int]]: """ Relabeling the qubit id so that the qubit id in new topology will start from 0. Returns: Tuple[QubitsTopology, Dict[int, int]], the first element of return is the new compressed topology, and the second element of return is the qubit id map with key be the qubit id in old topology and value be the qubit id in new topology. Examples: >>> from mindquantum.device import LinearQubits >>> topo1 = LinearQubits(5) >>> topo1.remove_qubit_node(0) >>> topo1.remove_qubit_node(2) >>> topo2, qubit_map = topo1.compress() >>> print(topo2.edges_with_id()) {(1, 2)} >>> print(qubit_map) {1: 0, 3: 1, 4: 2} """ topo_dup = copy.deepcopy(self) old_id = sorted(self.all_qubit_id()) nodes = topo_dup.choose(old_id) old_new_id_dict = {i: j for j, i in enumerate(old_id)} for qid in old_id: topo_dup.isolate_with_near(qid) for node in nodes: node.id_ = old_new_id_dict[node.id_] out = QubitsTopology(nodes) for i, j in self.edges_with_id(): _ = out[old_new_id_dict[i]] >> out[old_new_id_dict[j]] edge_color = self.get_edge_color(i, j) if edge_color is not None: out.set_edge_color(old_new_id_dict[i], old_new_id_dict[j], edge_color) return out, old_new_id_dict
[docs] def edges_with_id(self) -> typing.Set[typing.Tuple[int, int]]: """ Get edges with id of two connected qubits. Returns: Set[Tuple[int, int]], all connected edges in this qubit topology. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(2)]) >>> topology[0] << topology[1] >>> topology.edges_with_id() {(0, 1)} """ out: typing.Set[typing.Tuple[int, int]] = set() for qubit1 in self.qubits.values(): for qid_2 in qubit1.neighbor: if qubit1.qubit_id > qid_2: out.add((qid_2, qubit1.qubit_id)) else: out.add((qubit1.qubit_id, qid_2)) return out
[docs] def edges_with_poi(self) -> typing.Set[typing.Tuple[typing.Tuple[float, float], typing.Tuple[float, float]]]: """ Get edges with position of two connected qubits. Returns: Set[Tuple[Tuple[float, float], Tuple[float, float]]], the x and y position of two connected qubits. Examples: >>> from mindquantum.device import QubitNode, QubitsTopology >>> q0 = QubitNode(0, poi_x=0, poi_y=0) >>> q1 = QubitNode(1, poi_x=1, poi_y=0) >>> q0 >> q1 >>> topology = QubitsTopology([q0, q1]) >>> topology.edges_with_poi() {((0, 0), (1, 0))} """ out: typing.Set[typing.Tuple[typing.Tuple[float, float], typing.Tuple[float, float]]] = set() for q_id_1, q_id_2 in self.edges_with_id(): q1 = self.qubits[q_id_1] q2 = self.qubits[q_id_2] out.add(((q1.poi_x, q1.poi_y), (q2.poi_x, q2.poi_y))) return out
[docs] def has_qubit_node(self, qubit_id: int) -> bool: """ Check whether a qubit is in this topology. Args: qubit_id (int): the id of qubit you want to check. Returns: bool, whether this topology has qubit node with given id. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(2)]) >>> topology.has_qubit_node(0) True """ _check_int_type("qubit_id", qubit_id) return qubit_id in self.qubits
[docs] def is_coupled_with(self, id1: int, id2: int) -> bool: """ Check whether two qubit nodes are coupled. Args: id1 (int): the id of one qubit you want to check. id2 (int): the id of the other qubit you want to check. Returns: bool, whether two qubits node with given ids coupled. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(2)]) >>> topology.is_coupled_with(0, 1) False >>> topology[0] >> topology[1] >>> topology.is_coupled_with(0, 1) True """ _check_int_type("id1", id1) _check_int_type("id2", id2) return id2 in self.qubits[id1].neighbor
[docs] def isolate_with_near(self, qubit_id: int) -> None: """ Disconnect with all coupling qubits. Args: qubit_id (int): the id of qubit you want to disconnect with all nearby qubits. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[0] >> topology[1] >> topology[2] >>> topology.edges_with_id() {(0, 1), (1, 2)} >>> topology.isolate_with_near(1) >>> topology.edges_with_id() set() """ _check_int_type("qubit_id", qubit_id) current_node = self[qubit_id] other_nodes = [self[i] for i in current_node.neighbor] for node in other_nodes: _ = node > current_node
[docs] def n_edges(self) -> int: """ Get total connected edge number. Returns: int, the edge number of this qubit topology. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[0] >> topology[1] >> topology[2] >>> topology.n_edges() 2 """ return sum(len(i.neighbor) for i in self.qubits.values()) >> 1
[docs] def choose(self, ids: typing.List[int]) -> typing.List[QubitNode]: """ Choose qubit nodes based on given qubit id. Args: ids (List[int]): A list of qubit node id. Returns: List[:class:`~.device.QubitNode`], a list of qubit node based on given qubit node id. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[0] >> topology[1] >> topology[2] >>> nodes = topology.choose([0, 1]) >>> print(nodes[0].qubit_id, nodes[1].qubit_id) 0 1 """ _check_input_type("ids", list, ids) for qubit_id in ids: _check_int_type("Element of ids", qubit_id) return [self[i] for i in ids]
[docs] def remove_isolate_node(self) -> None: """ Remove qubit node that do not connect with any other qubits. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology[0] >> topology[1] >> topology[2] >>> topology.edges_with_id() {(0, 1), (1, 2)} >>> topology.isolate_with_near(1) >>> topology.all_qubit_id() {0, 1, 2} >>> topology.remove_isolate_node() >>> topology.all_qubit_id() set() """ will_remove = [] for node in self.qubits.values(): if not node.neighbor: will_remove.append(node.qubit_id) for qubit_id in will_remove: _ = self.qubits.pop(qubit_id)
[docs] def remove_qubit_node(self, qubit_id: int) -> None: """ Remove a qubit node out of this topology. Args: qubit_id (int): the id of qubit you want to remove. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology.remove_qubit_node(1) >>> topology.all_qubit_id() {0, 2} """ _check_int_type("qubit_id", qubit_id) will_remove = self.qubits[qubit_id] near_qubits = [self[i] for i in will_remove.neighbor] for node in near_qubits: _ = node > will_remove self.qubits.pop(qubit_id)
[docs] def select(self, ids: typing.List[int]) -> "QubitsTopology": """ Select certain qubit nodes to generate a new topology. Args: ids (List[int]): A list of qubit node id. Returns: :class:`~.device.QubitsTopology`, a new topology while keeping the connection property. Examples: >>> from mindquantum.device import LinearQubits >>> t1 = LinearQubits(4) >>> t2 = t1.select([0, 1, 2]) >>> t2.edges_with_id() {(0, 1), (1, 2)} """ ids_set = set(ids) nodes = copy.deepcopy(self.choose(ids)) for node in nodes: node.neighbor &= ids_set out = QubitsTopology(nodes) for (i, j), c in self.cached_edge_color.items(): if out.is_coupled_with(i, j): out.set_edge_color(i, j, c) return out
[docs] def set_color(self, qubit_id: int, color: str) -> None: """ Set color of certain qubit. Args: qubit_id (int): the id of qubit you want to change color. color (str): the new color. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology.set_color(0, "#ababab") >>> topology[0].color '#ababab' """ _check_int_type("qubit_id", qubit_id) _check_input_type("color", str, color) self[qubit_id].color_ = color
[docs] def set_edge_color(self, qubit_id1: int, qubit_id2: int, color: str) -> None: """ Set color of edge. The order of qubit_id1 and qubit_id2 does not matter. Args: qubit_id1 (int): The first qubit of edge. qubit_id2 (int): The second qubit of edge. color (str): The color of edge. """ _check_input_type("color", str, color) if not self.is_coupled_with(qubit_id1, qubit_id2): raise ValueError(f"qubit {qubit_id1} is not connected with qubit {qubit_id2}") self.cached_edge_color[tuple(sorted([qubit_id1, qubit_id2]))] = color
[docs] def get_edge_color(self, qubit_id1: int, qubit_id2: int) -> str: """ Get color of edge. The order of qubit_id1 and qubit_id2 does not matter. Args: qubit_id1 (int): The first qubit of edge. qubit_id2 (int): The second qubit of edge. """ return self.cached_edge_color.get(tuple(sorted([qubit_id1, qubit_id2])), None)
[docs] def set_position(self, qubit_id: int, poi_x: float, poi_y: float) -> None: """ Set position of a certain qubit. Args: qubit_id (int): the id of qubit you want to change position. poi_x (float): new x position. poi_y (float): new y position. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology.set_position(0, 1, 1) >>> topology[0].poi_x, topology[0].poi_y (1, 1) """ _check_int_type("qubit_id", qubit_id) _check_input_type("poi_x", numbers.Real, poi_x) _check_input_type("poi_y", numbers.Real, poi_y) self[qubit_id].set_poi(poi_x, poi_y)
[docs] def show(self, method: Optional[AVA_SHOW_METHOD] = None): """ Display the topology. Args: method (str): The method you want to display the topology. If ``None``, we will use default method, which is ``'mpl'`` in terminal environment and ``'svg'`` in jupyter notebook environment. You can also set it to ``'mpl'`` or ``'svg'`` manually. Default: ``None``. """ # pylint: disable=import-outside-toplevel from mindquantum.io.display import draw_topology, draw_topology_plt is_jupyter = Console().is_jupyter if method is None: method = 'svg' if is_jupyter else 'mpl' if method not in get_args(AVA_SHOW_METHOD): raise ValueError(f"method should be one of {get_args(AVA_SHOW_METHOD)}, but get {method}.") if method == 'svg': return draw_topology(self) return draw_topology_plt(self)
[docs] def size(self) -> int: """ Get total qubit number. Returns: int, the total qubit number. Examples: >>> from mindquantum.device import QubitsTopology, QubitNode >>> topology = QubitsTopology([QubitNode(i) for i in range(3)]) >>> topology.size() 3 """ return len(self.qubits)
[docs]class LinearQubits(QubitsTopology): """ Linear qubit topology. Args: n_qubits (int): total qubit size. Examples: >>> from mindquantum.device import LinearQubits >>> topology = LinearQubits(5) >>> topology.is_coupled_with(0, 1) True >>> topology.is_coupled_with(0, 2) False """ def __init__(self, n_qubits: int): """Initialize a linear topology.""" _check_int_type("n_qubits", n_qubits) nodes = [QubitNode(i, poi_x=i) for i in range(n_qubits)] left_node = nodes[0] for node in nodes[1:]: left_node = left_node >> node super().__init__(nodes)
[docs]class GridQubits(QubitsTopology): """ Grid qubit topology. Args: n_row (int): how many rows of your grid qubits. n_col (int): how many columns of your grid quits. Examples: >>> from mindquantum.device import GridQubits >>> topology = GridQubits(2, 3) >>> topology.n_row() 2 """ def __init__(self, n_row: int, n_col: int) -> None: """Initialize a grid topology with row and col number.""" _check_int_type("n_row", n_row) _check_int_type("n_col", n_col) self.n_row_ = n_row self.n_col_ = n_col qubits = [] for r in range(n_row): for c in range(n_col): qubits.append(QubitNode(r * n_col + c, poi_x=c, poi_y=r)) for r in range(n_row): next_node = qubits[r * n_col] for c in range(n_col - 1): next_node = next_node >> qubits[next_node.qubit_id + 1] for c in range(n_col): next_node = qubits[c] for r in range(n_row - 1): next_node = next_node >> qubits[next_node.qubit_id + n_col] super().__init__(qubits)
[docs] def n_col(self) -> int: """ Get column number. Returns: int, the column number. Examples: >>> from mindquantum.device import GridQubits >>> topology = GridQubits(2, 3) >>> topology.n_col() 3 """ return self.n_col_
[docs] def n_row(self) -> int: """ Get row number. Returns: int, the row number. Examples: >>> from mindquantum.device import GridQubits >>> topology = GridQubits(2, 3) >>> topology.n_row() 2 """ return self.n_row_