diff --git a/qiskit/converters/__init__.py b/qiskit/converters/__init__.py index e31cd0dc6238..9c035c349b09 100644 --- a/qiskit/converters/__init__.py +++ b/qiskit/converters/__init__.py @@ -27,6 +27,10 @@ circuit_to_instruction circuit_to_gate ast_to_dag + dagdependency_to_circuit + circuit_to_dagdependency + dag_to_dagdependency + dagdependency_to_dag """ from .circuit_to_dag import circuit_to_dag @@ -34,6 +38,10 @@ from .circuit_to_instruction import circuit_to_instruction from .circuit_to_gate import circuit_to_gate from .ast_to_dag import ast_to_dag +from .circuit_to_dagdependency import circuit_to_dagdependency +from .dagdependency_to_circuit import dagdependency_to_circuit +from .dag_to_dagdependency import dag_to_dagdependency +from .dagdependency_to_dag import dagdependency_to_dag def isinstanceint(obj): diff --git a/qiskit/converters/circuit_to_dagdependency.py b/qiskit/converters/circuit_to_dagdependency.py new file mode 100644 index 000000000000..1453a3d1d0be --- /dev/null +++ b/qiskit/converters/circuit_to_dagdependency.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +'''Helper function for converting a circuit to a dag dependency''' + +from qiskit.dagcircuit.dagdependency import DAGDependency + + +def circuit_to_dagdependency(circuit): + """Build a ``DAGDependency`` object from a ``QuantumCircuit``. + + Args: + circuit (QuantumCircuit): the input circuits. + + Return: + DAGDependency: the DAG representing the input circuit as a dag dependency. + """ + dagdependency = DAGDependency() + dagdependency.name = circuit.name + + for register in circuit.qregs: + dagdependency.add_qreg(register) + + for register in circuit.cregs: + dagdependency.add_creg(register) + + for operation, qargs, cargs in circuit.data: + dagdependency.add_op_node(operation, qargs, cargs) + + dagdependency._add_successors() + + return dagdependency diff --git a/qiskit/converters/dag_to_dagdependency.py b/qiskit/converters/dag_to_dagdependency.py new file mode 100644 index 000000000000..14b0ae47b941 --- /dev/null +++ b/qiskit/converters/dag_to_dagdependency.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Helper function for converting a dag circuit to a dag dependency""" +from qiskit.dagcircuit.dagdependency import DAGDependency + + +def dag_to_dagdependency(dag): + """Build a ``DAGDependency`` object from a ``DAGCircuit``. + + Args: + dag (DAGCircuit): the input dag. + + Return: + DAGDependency: the DAG representing the input circuit as a dag dependency. + """ + + dagdependency = DAGDependency() + DAGDependency.name = dag.name + + qregs = list(dag.qregs.values()) + cregs = list(dag.cregs.values()) + + for register in qregs: + dagdependency.add_qreg(register) + + for register in cregs: + dagdependency.add_creg(register) + + for node in dag.topological_op_nodes(): + # Get arguments for classical control (if any) + inst = node.op.copy() + inst.condition = node.condition + dagdependency.add_op_node(inst, node.qargs, node.cargs) + + dagdependency._add_successors() + + return dagdependency diff --git a/qiskit/converters/dagdependency_to_circuit.py b/qiskit/converters/dagdependency_to_circuit.py new file mode 100644 index 000000000000..300641a35948 --- /dev/null +++ b/qiskit/converters/dagdependency_to_circuit.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Helper function for converting a dag dependency to a circuit""" +from qiskit.circuit import QuantumCircuit + + +def dagdependency_to_circuit(dagdependency): + """Build a ``QuantumCircuit`` object from a ``DAGDependency``. + + Args: + dagdependency (DAGDependency): the input dag. + + Return: + QuantumCircuit: the circuit representing the input dag dependency. + """ + + name = dagdependency.name or None + circuit = QuantumCircuit(*dagdependency.qregs.values(), *dagdependency.cregs.values(), + name=name) + + for node in dagdependency.get_nodes(): + # Get arguments for classical control (if any) + inst = node.op.copy() + inst.condition = node.condition + circuit._append(inst, node.qargs, node.cargs) + + return circuit diff --git a/qiskit/converters/dagdependency_to_dag.py b/qiskit/converters/dagdependency_to_dag.py new file mode 100644 index 000000000000..567ebf93fa25 --- /dev/null +++ b/qiskit/converters/dagdependency_to_dag.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Helper function for converting a dag dependency to a dag circuit""" +from qiskit.dagcircuit.dagcircuit import DAGCircuit + + +def dagdependency_to_dag(dagdependency): + """Build a ``DAGCircuit`` object from a ``DAGDependency``. + + Args: + dag dependency (DAGDependency): the input dag. + + Return: + DAGCircuit: the DAG representing the input circuit. + """ + + dagcircuit = DAGCircuit() + dagcircuit.name = dagdependency.name + + qregs = list(dagdependency.qregs.values()) + cregs = list(dagdependency.cregs.values()) + + for register in qregs: + dagcircuit.add_qreg(register) + + for register in cregs: + dagcircuit.add_creg(register) + + for node in dagdependency.get_nodes(): + # Get arguments for classical control (if any) + inst = node.op.copy() + inst.condition = node.condition + + dagcircuit.apply_operation_back(inst, node.qargs, node.cargs, inst.condition) + + return dagcircuit diff --git a/qiskit/dagcircuit/__init__.py b/qiskit/dagcircuit/__init__.py index c8e937c47683..987face7870f 100644 --- a/qiskit/dagcircuit/__init__.py +++ b/qiskit/dagcircuit/__init__.py @@ -27,6 +27,8 @@ DAGCircuit DAGNode + DAGDepNode + DAGDependency Exceptions ========== @@ -38,4 +40,6 @@ """ from .dagcircuit import DAGCircuit from .dagnode import DAGNode +from .dagdepnode import DAGDepNode from .exceptions import DAGCircuitError +from .dagdependency import DAGDependency diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py new file mode 100644 index 000000000000..b5767282aa7c --- /dev/null +++ b/qiskit/dagcircuit/dagdependency.py @@ -0,0 +1,541 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""DAGDependency class for representing non-commutativity in a circuit. +""" + +import heapq +from collections import OrderedDict +import networkx as nx +import retworkx as rx +import numpy as np + +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.classicalregister import ClassicalRegister +from qiskit.dagcircuit.exceptions import DAGDependencyError +from qiskit.dagcircuit.dagdepnode import DAGDepNode +from qiskit.quantum_info.operators import Operator + + +class DAGDependency: + """Object to represent a quantum circuit as a directed acyclic graph + via operation dependencies (i.e. lack of commutation). + + The nodes in the graph are operations represented by quantum gates. + The edges correspond to non-commutation between two operations + (i.e. a dependency). A directed edge from node A to node B means that + operation A does not commute with operation B. + The object's methods allow circuits to be constructed. + + The nodes in the graph have the following attributes: + 'operation', 'successors', 'predecessors'. + + **Example:** + + Bell circuit with no measurement. + + .. parsed-literal:: + + ┌───┐ + qr_0: ┤ H ├──■── + └───┘┌─┴─┐ + qr_1: ─────┤ X ├ + └───┘ + + The dependency DAG for the above circuit is represented by two nodes. + The first one corresponds to Hadamard gate, the second one to the CNOT gate + as the gates do not commute there is an edge between the two nodes. + + **Reference:** + + [1] Iten, R., Sutter, D. and Woerner, S., 2019. + Efficient template matching in quantum circuits. + `arXiv:1909.05270 `_ + """ + + def __init__(self): + """ + Create an empty DAGDependency. + """ + # Circuit name + self.name = None + + # Directed multigraph whose nodes are operations(gates) and edges + # represent non-commutativity between two gates. + self._multi_graph = rx.PyDAG() + + # Map of qreg name to QuantumRegister object + self.qregs = OrderedDict() + + # Map of creg name to ClassicalRegister object + self.cregs = OrderedDict() + + def to_networkx(self): + """Returns a copy of the DAGDependency in networkx format.""" + # For backwards compatibility, return networkx structure from terra 0.12 + # where DAGNodes instances are used as indexes on the networkx graph. + + dag_networkx = nx.MultiDiGraph() + + for node in self.get_nodes(): + dag_networkx.add_node(node) + for node in self.topological_nodes(): + for source_id, dest_id, edge in \ + self.get_in_edges(node.node_id): + dag_networkx.add_edge(self.get_node(source_id), self.get_node(dest_id), **edge) + return dag_networkx + + def to_retworkx(self): + """ Returns the DAGDependency in retworkx format.""" + return self._multi_graph + + def qubits(self): + """Return a list of qubits (as a list of Qubit instances).""" + return [qubit for qreg in self.qregs.values() for qubit in qreg] + + def clbits(self): + """Return a list of classical bits (as a list of Clbit instances).""" + return [clbit for creg in self.cregs.values() for clbit in creg] + + def size(self): + """ Returns the number of gates in the circuit""" + return len(self._multi_graph) + + def depth(self): + """Return the circuit depth. + Returns: + int: the circuit depth + """ + depth = rx.dag_longest_path_length(self._multi_graph) + return depth if depth >= 0 else 0 + + def add_qreg(self, qreg): + """Add qubits in a quantum register.""" + if not isinstance(qreg, QuantumRegister): + raise DAGDependencyError("not a QuantumRegister instance.") + if qreg.name in self.qregs: + raise DAGDependencyError("duplicate register %s" % qreg.name) + self.qregs[qreg.name] = qreg + + def add_creg(self, creg): + """Add all wires in a classical register.""" + if not isinstance(creg, ClassicalRegister): + raise DAGDependencyError("not a ClassicalRegister instance.") + if creg.name in self.cregs: + raise DAGDependencyError("duplicate register %s" % creg.name) + self.cregs[creg.name] = creg + + def _add_multi_graph_node(self, node): + """ + Args: + node (DAGDepNode): considered node. + + Returns: + node_id(int): corresponding label to the added node. + """ + node_id = self._multi_graph.add_node(node) + node.node_id = node_id + return node_id + + def get_nodes(self): + """ + Returns: + generator(dict): iterator over all the nodes. + """ + return iter(self._multi_graph.nodes()) + + def get_node(self, node_id): + """ + Args: + node_id (int): label of considered node. + + Returns: + node: corresponding to the label. + """ + return self._multi_graph.get_node_data(node_id) + + def _add_multi_graph_edge(self, src_id, dest_id, data): + """ + Function to add an edge from given data (dict) between two nodes. + + Args: + src_id (int): label of the first node. + dest_id (int): label of the second node. + data (dict): data contained on the edge. + + """ + self._multi_graph.add_edge(src_id, dest_id, data) + + def get_edges(self, src_id, dest_id): + """ + Edge enumeration between two nodes through method get_all_edge_data. + + Args: + src_id (int): label of the first node. + dest_id (int): label of the second node. + + Returns: + List: corresponding to all edges between the two nodes. + """ + return self._multi_graph.get_all_edge_data(src_id, dest_id) + + def get_all_edges(self): + """ + Enumaration of all edges. + + Returns: + List: corresponding to the label. + """ + + return [(src, dest, data) + for src_node in self._multi_graph.nodes() + for (src, dest, data) + in self._multi_graph.out_edges(src_node.node_id)] + + def get_in_edges(self, node_id): + """ + Enumeration of all incoming edges for a given node. + + Args: + node_id (int): label of considered node. + + Returns: + List: corresponding incoming edges data. + """ + return self._multi_graph.in_edges(node_id) + + def get_out_edges(self, node_id): + """ + Enumeration of all outgoing edges for a given node. + + Args: + node_id (int): label of considered node. + + Returns: + List: corresponding outgoing edges data. + """ + return self._multi_graph.out_edges(node_id) + + def direct_successors(self, node_id): + """ + Direct successors id of a given node as sorted list. + + Args: + node_id (int): label of considered node. + + Returns: + List: direct successors id as a sorted list + """ + return sorted(list(self._multi_graph.adj_direction(node_id, False).keys())) + + def direct_predecessors(self, node_id): + """ + Direct predecessors id of a given node as sorted list. + + Args: + node_id (int): label of considered node. + + Returns: + List: direct predecessors id as a sorted list + """ + return sorted(list(self._multi_graph.adj_direction(node_id, True).keys())) + + def successors(self, node_id): + """ + Successors id of a given node as sorted list. + + Args: + node_id (int): label of considered node. + + Returns: + List: all successors id as a sorted list + """ + return self._multi_graph.get_node_data(node_id).successors + + def predecessors(self, node_id): + """ + Predecessors id of a given node as sorted list. + + Args: + node_id (int): label of considered node. + + Returns: + List: all predecessors id as a sorted list + """ + return self._multi_graph.get_node_data(node_id).predecessors + + def topological_nodes(self): + """ + Yield nodes in topological order. + + Returns: + generator(DAGNode): node in topological order. + """ + + def _key(x): + return x.sort_key + + return iter(rx.lexicographical_topological_sort( + self._multi_graph, + key=_key)) + + def add_op_node(self, operation, qargs, cargs): + """Add a DAGDepNode to the graph and update the edges. + + Args: + operation (qiskit.circuit.Instruction): operation as a quantum gate. + qargs (list[Qubit]): list of qubits on which the operation acts + cargs (list[Clbit]): list of classical wires to attach to. + """ + all_qubits = self.qubits() + all_clbits = self.clbits() + + directives = ['measure', 'barrier', 'snapshot'] + if operation.name not in directives: + qindices_list = [] + for elem in qargs: + qindices_list.append(all_qubits.index(elem)) + if operation.condition: + for clbit in all_clbits: + if clbit.register == operation.condition[0]: + initial = all_clbits.index(clbit) + final = all_clbits.index(clbit) + clbit.register.size + cindices_list = range(initial, final) + break + else: + cindices_list = [] + else: + qindices_list = [] + cindices_list = [] + + new_node = DAGDepNode(type="op", op=operation, name=operation.name, qargs=qargs, + cargs=cargs, condition=operation.condition, successors=[], + predecessors=[], qindices=qindices_list, cindices=cindices_list) + self._add_multi_graph_node(new_node) + self._update_edges() + + def _gather_pred(self, node_id, direct_pred): + """Function set an attribute predecessors and gather multiple lists + of direct predecessors into a single one. + + Args: + node_id (int): label of the considered node in the DAG + direct_pred (list): list of direct successors for the given node + + Returns: + DAGDependency: A multigraph with update of the attribute ['predecessors'] + the lists of direct successors are put into a single one + """ + gather = self._multi_graph + gather.get_node_data(node_id).predecessors = [] + for d_pred in direct_pred: + gather.get_node_data(node_id).predecessors.append([d_pred]) + pred = self._multi_graph.get_node_data(d_pred).predecessors + gather.get_node_data(node_id).predecessors.append(pred) + return gather + + def _gather_succ(self, node_id, direct_succ): + """ + Function set an attribute successors and gather multiple lists + of direct successors into a single one. + + Args: + node_id (int): label of the considered node in the DAG + direct_succ (list): list of direct successors for the given node + + Returns: + MultiDiGraph: with update of the attribute ['predecessors'] + the lists of direct successors are put into a single one + """ + gather = self._multi_graph + for d_succ in direct_succ: + gather.get_node_data(node_id).successors.append([d_succ]) + succ = gather.get_node_data(d_succ).successors + gather.get_node_data(node_id).successors.append(succ) + return gather + + def _list_pred(self, node_id): + """ + Use _gather_pred function and merge_no_duplicates to construct + the list of predecessors for a given node. + + Args: + node_id (int): label of the considered node + """ + direct_pred = self.direct_predecessors(node_id) + self._multi_graph = self._gather_pred(node_id, direct_pred) + self._multi_graph.get_node_data(node_id).predecessors = list( + merge_no_duplicates(*(self._multi_graph.get_node_data(node_id).predecessors))) + + def _update_edges(self): + """ + Function to verify the commutation relation and reachability + for predecessors, the nodes do not commute and + if the predecessor is reachable. Update the DAGDependency by + introducing edges and predecessors(attribute) + """ + max_node_id = len(self._multi_graph) - 1 + max_node = self._multi_graph.get_node_data(max_node_id) + + for current_node_id in range(0, max_node_id): + self._multi_graph.get_node_data(current_node_id).reachable = True + # Check the commutation relation with reachable node, it adds edges if it does not commute + for prev_node_id in range(max_node_id - 1, -1, -1): + if self._multi_graph.get_node_data(prev_node_id).reachable and not _commute( + self._multi_graph.get_node_data(prev_node_id), max_node): + self._multi_graph.add_edge(prev_node_id, max_node_id, {'commute': False}) + self._list_pred(max_node_id) + list_predecessors = self._multi_graph.get_node_data(max_node_id).predecessors + for pred_id in list_predecessors: + self._multi_graph.get_node_data(pred_id).reachable = False + + def _add_successors(self): + """ + Use _gather_succ and merge_no_duplicates to create the list of successors + for each node. Update DAGDependency 'successors' attribute. It has to + be used when the DAGDependency() object is complete (i.e. converters). + """ + for node_id in range(len(self._multi_graph) - 1, -1, -1): + direct_successors = self.direct_successors(node_id) + + self._multi_graph = self._gather_succ(node_id, direct_successors) + + self._multi_graph.get_node_data(node_id).successors = list( + merge_no_duplicates(*self._multi_graph.get_node_data(node_id).successors)) + + def copy(self): + """ + Function to copy a DAGDependency object. + Returns: + DAGDependency: a copy of a DAGDependency object. + """ + + dag = DAGDependency() + dag.name = self.name + dag.cregs = self.cregs.copy() + dag.qregs = self.qregs.copy() + + for node in self.get_nodes(): + dag._multi_graph.add_node(node.copy()) + for edges in self.get_all_edges(): + dag._multi_graph.add_edge(edges[0], edges[1], edges[2]) + return dag + + def draw(self, scale=0.7, filename=None, style='color'): + """ + Draws the DAGDependency graph. + + This function needs `pydot `, which in turn needs + Graphviz ` to be installed. + + Args: + scale (float): scaling factor + filename (str): file path to save image to (format inferred from name) + style (str): 'plain': B&W graph + 'color' (default): color input/output/op nodes + + Returns: + Ipython.display.Image: if in Jupyter notebook and not saving to file, + otherwise None. + """ + from qiskit.visualization.dag_visualization import dag_drawer + return dag_drawer(dag=self, scale=scale, filename=filename, + style=style, category='dependency') + + +def merge_no_duplicates(*iterables): + """Merge K list without duplicate using python heapq ordered merging + + Args: + *iterables: A list of k sorted lists + + Yields: + Iterator: List from the merging of the k ones (without duplicates + """ + last = object() + for val in heapq.merge(*iterables): + if val != last: + last = val + yield val + + +def _commute(node1, node2): + """Function to verify commutation relation between two nodes in the DAG + + Args: + node1 (DAGnode): first node operation (attribute ['operation'] in the DAG) + node2 (DAGnode): second node operation + + Return: + bool: True if the gates commute and false if it is not the case. + """ + + # Create set of qubits on which the operation acts + qarg1 = [node1.qargs[i].index for i in range(0, len(node1.qargs))] + qarg2 = [node2.qargs[i].index for i in range(0, len(node2.qargs))] + + # Create set of cbits on which the operation acts + carg1 = [node1.qargs[i].index for i in range(0, len(node1.cargs))] + carg2 = [node2.qargs[i].index for i in range(0, len(node2.cargs))] + + # Commutation for classical conditional gates + if node1.condition or node2.condition: + intersection = set(qarg1).intersection(set(qarg2)) + if intersection or carg1 or carg2: + commute_condition = False + else: + commute_condition = True + return commute_condition + + # Commutation for measurement + if node1.name == 'measure' or node2.name == 'measure': + intersection_q = set(qarg1).intersection(set(qarg2)) + intersection_c = set(carg1).intersection(set(carg2)) + if intersection_q or intersection_c: + commute_measurement = False + else: + commute_measurement = True + return commute_measurement + + # Commutation for barrier-like directives + directives = ['barrier', 'snapshot'] + if node1.name in directives or node2.name in directives: + intersection = set(qarg1).intersection(set(qarg2)) + if intersection: + commute_directive = False + else: + commute_directive = True + return commute_directive + + # List of non commuting gates (TO DO: add more elements) + non_commute_list = [set(['x', 'y']), set(['x', 'z'])] + + if qarg1 == qarg2 and (set([node1.name, node2.name]) in non_commute_list): + return False + + # Create matrices to check commutation relation if no other criteria are matched + qarg = list(set(node1.qargs + node2.qargs)) + qbit_num = len(qarg) + + qarg1 = [qarg.index(q) for q in node1.qargs] + qarg2 = [qarg.index(q) for q in node2.qargs] + + id_op = Operator(np.eye(2 ** qbit_num)) + + op12 = id_op.compose(node1.op, qargs=qarg1).compose(node2.op, qargs=qarg2) + op21 = id_op.compose(node2.op, qargs=qarg2).compose(node1.op, qargs=qarg1) + + if_commute = (op12 == op21) + + return if_commute diff --git a/qiskit/dagcircuit/dagdepnode.py b/qiskit/dagcircuit/dagdepnode.py new file mode 100644 index 000000000000..5b7f3ca855de --- /dev/null +++ b/qiskit/dagcircuit/dagdepnode.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=redefined-builtin + +"""Object to represent the information at a node in the DAGCircuit.""" + +from qiskit.exceptions import QiskitError + + +class DAGDepNode: + """Object to represent the information at a node in the DAGDependency(). + + It is used as the return value from `*_nodes()` functions and can + be supplied to functions that take a node. + """ + + __slots__ = ['type', '_op', 'name', '_qargs', 'cargs', 'condition', + 'sort_key', 'node_id', 'successors', 'predecessors', + 'reachable', 'matchedwith', 'isblocked', 'successorstovisit', + 'qindices', 'cindices'] + + def __init__(self, type=None, op=None, name=None, qargs=None, cargs=None, + condition=None, successors=None, predecessors=None, reachable=None, + matchedwith=None, successorstovisit=None, isblocked=None, qindices=None, + cindices=None, nid=-1): + + self.type = type + self._op = op + self.name = name + self._qargs = qargs if qargs is not None else [] + self.cargs = cargs if cargs is not None else [] + self.condition = condition + self.node_id = nid + self.sort_key = str(self._qargs) + self.successors = successors if successors is not None else [] + self.predecessors = predecessors if predecessors is not None else [] + self.reachable = reachable + self.matchedwith = matchedwith if matchedwith is not None else [] + self.isblocked = isblocked + self.successorstovisit = successorstovisit if successorstovisit is not None else [] + self.qindices = qindices if qindices is not None else [] + self.cindices = cindices if cindices is not None else [] + + @property + def op(self): + """Returns the Instruction object corresponding to the op for the node, else None""" + if not self.type or self.type != 'op': + raise QiskitError("The node %s is not an op node" % (str(self))) + return self._op + + @op.setter + def op(self, data): + self._op = data + + @property + def qargs(self): + """ + Returns list of Qubit, else an empty list. + """ + return self._qargs + + @qargs.setter + def qargs(self, new_qargs): + """Sets the qargs to be the given list of qargs.""" + self._qargs = new_qargs + self.sort_key = str(new_qargs) + + @staticmethod + def semantic_eq(node1, node2): + """ + Check if DAG nodes are considered equivalent, e.g., as a node_match for nx.is_isomorphic. + + Args: + node1 (DAGDepNode): A node to compare. + node2 (DAGDepNode): The other node to compare. + + Return: + Bool: If node1 == node2 + """ + # For barriers, qarg order is not significant so compare as sets + if 'barrier' == node1.name == node2.name: + return set(node1._qargs) == set(node2._qargs) + result = False + if node1.type == node2.type: + if node1._op == node2._op: + if node1.name == node2.name: + if node1._qargs == node2._qargs: + if node1.cargs == node2.cargs: + if node1.condition == node2.condition: + result = True + return result + + def copy(self): + """ + Function to copy a DAGDepNode object. + Returns: + DAGDepNode: a copy of a DAGDepNode objectto. + """ + + dagdepnode = DAGDepNode() + + dagdepnode.type = self.type + dagdepnode._op = self.op + dagdepnode.name = self.name + dagdepnode._qargs = self._qargs + dagdepnode.cargs = self.cargs + dagdepnode.condition = self.condition + dagdepnode.node_id = self.node_id + dagdepnode.sort_key = self.sort_key + dagdepnode.successors = self.successors + dagdepnode.predecessors = self.predecessors + dagdepnode.reachable = self.reachable + dagdepnode.isblocked = self.isblocked + dagdepnode.successorstovisit = self.successorstovisit + dagdepnode.qindices = self.qindices + dagdepnode.cindices = self.cindices + dagdepnode.matchedwith = self.matchedwith.copy() + + return dagdepnode diff --git a/qiskit/dagcircuit/exceptions.py b/qiskit/dagcircuit/exceptions.py index 0d2c2cb04cc7..9bcd97e89018 100644 --- a/qiskit/dagcircuit/exceptions.py +++ b/qiskit/dagcircuit/exceptions.py @@ -29,3 +29,16 @@ def __init__(self, *msg): def __str__(self): """Return the message.""" return repr(self.msg) + + +class DAGDependencyError(QiskitError): + """Base class for errors raised by the DAGDependency object.""" + + def __init__(self, *msg): + """Set the error message.""" + super().__init__(*msg) + self.msg = ' '.join(msg) + + def __str__(self): + """Return the message.""" + return repr(self.msg) diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index e430ab2edfee..3125874e7c08 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -18,7 +18,7 @@ from qiskit.circuit.gate import Gate from qiskit.transpiler.basepasses import TransformationPass -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagcircuit import DAGCircuit class Decompose(TransformationPass): diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index c9112920b017..34ce00fb60e7 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -2,7 +2,7 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2018. +# (C) Copyright IBM 2017, 2018, 2020. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -33,7 +33,7 @@ HAS_PIL = False -def dag_drawer(dag, scale=0.7, filename=None, style='color'): +def dag_drawer(dag, scale=0.7, filename=None, style='color', category=None): """Plot the directed acyclic graph (dag) to represent operation dependencies in a quantum circuit. @@ -52,6 +52,7 @@ def dag_drawer(dag, scale=0.7, filename=None, style='color'): filename (str): file path to save image to (format inferred from name) style (str): 'plain': B&W graph 'color' (default): color input/output/op nodes + category (str): 'dependency' for drawing DAG dependency Returns: PIL.Image: if in Jupyter notebook and not saving to file, @@ -86,32 +87,64 @@ def dag_drawer(dag, scale=0.7, filename=None, style='color'): except ImportError: raise ImportError("dag_drawer requires pydot. " "Run 'pip install pydot'.") - - G = dag.to_networkx() - G.graph['dpi'] = 100 * scale - - if style == 'plain': - pass - elif style == 'color': - for node in G.nodes: - n = G.nodes[node] - n['label'] = node.name - if node.type == 'op': - n['color'] = 'blue' - n['style'] = 'filled' - n['fillcolor'] = 'lightblue' - if node.type == 'in': - n['color'] = 'black' - n['style'] = 'filled' - n['fillcolor'] = 'green' - if node.type == 'out': - n['color'] = 'black' - n['style'] = 'filled' - n['fillcolor'] = 'red' - for e in G.edges(data=True): - e[2]['label'] = e[2]['name'] + if category is None: + G = dag.to_networkx() + G.graph['dpi'] = 100 * scale + + if style == 'plain': + pass + elif style == 'color': + for node in G.nodes: + n = G.nodes[node] + n['label'] = node.name + if node.type == 'op': + n['color'] = 'blue' + n['style'] = 'filled' + n['fillcolor'] = 'lightblue' + if node.type == 'in': + n['color'] = 'black' + n['style'] = 'filled' + n['fillcolor'] = 'green' + if node.type == 'out': + n['color'] = 'black' + n['style'] = 'filled' + n['fillcolor'] = 'red' + for e in G.edges(data=True): + e[2]['label'] = e[2]['name'] + else: + raise VisualizationError("Unrecognized style for the dag_drawer.") + + elif category == 'dependency': + G = dag.to_networkx() + G.graph['dpi'] = 100 * scale + + if style == 'plain': + pass + elif style == 'color': + for node in G.nodes: + n = G.nodes[node] + n['label'] = str(node.node_id) + ': ' + node.name + if node.name == 'measure': + n['color'] = 'blue' + n['style'] = 'filled' + n['fillcolor'] = 'lightblue' + if node.name == 'barrier': + n['color'] = 'black' + n['style'] = 'filled' + n['fillcolor'] = 'green' + if node.name == 'snapshot': + n['color'] = 'black' + n['style'] = 'filled' + n['fillcolor'] = 'red' + if node.condition: + n['label'] = str(node.node_id) + ': ' + node.name + ' (conditional)' + n['color'] = 'black' + n['style'] = 'filled' + n['fillcolor'] = 'lightgreen' + else: + raise VisualizationError("Unrecognized style for the dag_drawer.") else: - raise VisualizationError("Unrecognized style for the dag_drawer.") + raise VisualizationError("Unrecognized category of DAG") dot = to_pydot(G) diff --git a/releasenotes/notes/Add-canonical-form-47e0466ed57640f3.yaml b/releasenotes/notes/Add-canonical-form-47e0466ed57640f3.yaml new file mode 100644 index 000000000000..8f209440267b --- /dev/null +++ b/releasenotes/notes/Add-canonical-form-47e0466ed57640f3.yaml @@ -0,0 +1,44 @@ +--- +features: + - | + Added a new DAG to qiskit; the dependency form. In this DAG, the node are + operation (gates, measure, barrier, etc...). The edges corresponds to + non-commutation between two operations. + The ''qiskit.dagcircuit.dagdependency'' introduces the DAGDependency class. + It allows to create an empty DAG dependency. For exmaple: + + from qiskit.dagcircuit.dependency import DAGDependency + + dag_dependency = DAGDependency() + + - | + The ''qiskit.converters.circuit_to_dagdependency'' introduces a converter + from a QuantumCircuit() object to a DAGDependency() object. + The ''qiskit.converters.dagdependency_to_ciruit'' introduces a converter + from a DAGDependency() object to a QuantumCircuit() object. + The ''qiskit.converters.dag_to_dagdepency'' introduces a converter + from a DAGCircuit() object to a DAGDependency() object. + The ''qiskit.converters.dagdependency_to_ciruit'' introduces a converter + from a DAGDependcy() object to a DAGCircuit() object. + + For example: + + from qiskit.converters.dagdependency_to_circuit import dagdependency_to_circuit + from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit + + circuit_in = QuantumCircuit(2) + circuit_in.h(qr[0]) + circuit_in.h(qr[1]) + + dag_dependency = circuit_to_dagdependency(circuit_in) + circuit_out = dagdepency_to_circuit(dag_dependency) + + - | + The ''qiskit/visualization/dag_visualization'' has been updated such that + it can draw DAG dependency forms. It takes the new argument 'type'. + + +issues: + - | + In ''qiskit.dagcircuit.dagdependency' the function commute could be + improved. diff --git a/test/python/converters/test_circuit_to_dagdependency.py b/test/python/converters/test_circuit_to_dagdependency.py new file mode 100644 index 000000000000..16b07fd844dd --- /dev/null +++ b/test/python/converters/test_circuit_to_dagdependency.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test for the converter dag dependency to circuit and circuit to dag +dependency.""" + +import unittest + +from qiskit.converters.dagdependency_to_circuit import dagdependency_to_circuit +from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency +from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.test import QiskitTestCase + + +class TestCircuitToDagCanonical(QiskitTestCase): + """Test QuantumCircuit to DAGDependency.""" + + def test_circuit_and_dag_canonical(self): + """Check convert to dag dependency and back""" + qr = QuantumRegister(3) + cr = ClassicalRegister(3) + circuit_in = QuantumCircuit(qr, cr) + circuit_in.h(qr[0]) + circuit_in.h(qr[1]) + circuit_in.measure(qr[0], cr[0]) + circuit_in.measure(qr[1], cr[1]) + circuit_in.x(qr[0]).c_if(cr, 0x3) + circuit_in.measure(qr[0], cr[0]) + circuit_in.measure(qr[1], cr[1]) + circuit_in.measure(qr[2], cr[2]) + dag_dependency = circuit_to_dagdependency(circuit_in) + circuit_out = dagdependency_to_circuit(dag_dependency) + self.assertEqual(circuit_out, circuit_in) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test/python/converters/test_dag_to_dagdependency.py b/test/python/converters/test_dag_to_dagdependency.py new file mode 100644 index 000000000000..5e48e11824c1 --- /dev/null +++ b/test/python/converters/test_dag_to_dagdependency.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test for the converter dag dependency to dag circuit and +dag circuit to dag dependency.""" + +import unittest + +from qiskit.converters.circuit_to_dag import circuit_to_dag +from qiskit.converters.dag_to_dagdependency import dag_to_dagdependency +from qiskit.converters.dagdependency_to_dag import dagdependency_to_dag +from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.test import QiskitTestCase + + +class TestCircuitToDagDependency(QiskitTestCase): + """Test DAGCircuit to DAGDependency.""" + + def test_circuit_and_dag_dependency(self): + """Check convert to dag dependency and back""" + qr = QuantumRegister(3) + cr = ClassicalRegister(3) + circuit_in = QuantumCircuit(qr, cr) + circuit_in.h(qr[0]) + circuit_in.h(qr[1]) + circuit_in.measure(qr[0], cr[0]) + circuit_in.measure(qr[1], cr[1]) + circuit_in.x(qr[0]).c_if(cr, 0x3) + circuit_in.measure(qr[0], cr[0]) + circuit_in.measure(qr[1], cr[1]) + circuit_in.measure(qr[2], cr[2]) + dag_in = circuit_to_dag(circuit_in) + + dag_dependency = dag_to_dagdependency(dag_in) + dag_out = dagdependency_to_dag(dag_dependency) + + self.assertEqual(dag_out, dag_in) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/test/python/dagcircuit/test_dagdependency.py b/test/python/dagcircuit/test_dagdependency.py new file mode 100644 index 000000000000..c4d541bf46e5 --- /dev/null +++ b/test/python/dagcircuit/test_dagdependency.py @@ -0,0 +1,249 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Test for the DAGDependency object""" + +import unittest + +from qiskit.dagcircuit import DAGDependency +from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.circuit import Measure +from qiskit.circuit import Instruction +from qiskit.extensions.standard.h import HGate +from qiskit.dagcircuit.exceptions import DAGDependencyError +from qiskit.converters import circuit_to_dagdependency +from qiskit.test import QiskitTestCase + +try: + import retworkx as rx +except ImportError: + pass + + +def raise_if_dagdependency_invalid(dag): + """Validates the internal consistency of a DAGDependency._multi_graph. + Intended for use in testing. + + Raises: + DAGDependencyError: if DAGDependency._multi_graph is inconsistent. + """ + + multi_graph = dag._multi_graph + + if not rx.is_directed_acyclic_graph(multi_graph): + raise DAGDependencyError('multi_graph is not a DAG.') + + # Every node should be of type op. + for node in dag.get_nodes(): + if node.type != 'op': + raise DAGDependencyError('Found node of unexpected type: {}'.format( + node.type)) + + +class TestDagRegisters(QiskitTestCase): + """Test qreg and creg inside the dag""" + + def test_add_qreg_creg(self): + """add_qreg() and add_creg() methods""" + dag = DAGDependency() + dag.add_qreg(QuantumRegister(2, 'qr')) + dag.add_creg(ClassicalRegister(1, 'cr')) + self.assertDictEqual(dag.qregs, {'qr': QuantumRegister(2, 'qr')}) + self.assertDictEqual(dag.cregs, {'cr': ClassicalRegister(1, 'cr')}) + + def test_dag_get_qubits(self): + """get_qubits() method """ + dag = DAGDependency() + dag.add_qreg(QuantumRegister(1, 'qr1')) + dag.add_qreg(QuantumRegister(1, 'qr10')) + dag.add_qreg(QuantumRegister(1, 'qr0')) + dag.add_qreg(QuantumRegister(1, 'qr3')) + dag.add_qreg(QuantumRegister(1, 'qr4')) + dag.add_qreg(QuantumRegister(1, 'qr6')) + self.assertListEqual(dag.qubits(), [QuantumRegister(1, 'qr1')[0], + QuantumRegister(1, 'qr10')[0], + QuantumRegister(1, 'qr0')[0], + QuantumRegister(1, 'qr3')[0], + QuantumRegister(1, 'qr4')[0], + QuantumRegister(1, 'qr6')[0]]) + + def test_add_reg_duplicate(self): + """add_qreg with the same register twice is not allowed.""" + dag = DAGDependency() + qr = QuantumRegister(2) + dag.add_qreg(qr) + self.assertRaises(DAGDependencyError, dag.add_qreg, qr) + + def test_add_reg_duplicate_name(self): + """Adding quantum registers with the same name is not allowed.""" + dag = DAGDependency() + qr1 = QuantumRegister(3, 'qr') + dag.add_qreg(qr1) + qr2 = QuantumRegister(2, 'qr') + self.assertRaises(DAGDependencyError, dag.add_qreg, qr2) + + def test_add_reg_bad_type(self): + """add_qreg with a classical register is not allowed.""" + dag = DAGDependency() + cr = ClassicalRegister(2) + self.assertRaises(DAGDependencyError, dag.add_qreg, cr) + + +class TestDagNodeEdge(QiskitTestCase): + """Test adding nodes and edges to a dag and related functions.""" + + def setUp(self): + self.dag = DAGDependency() + self.qreg = QuantumRegister(2, 'qr') + self.creg = ClassicalRegister(2, 'cr') + + self.dag.add_qreg(self.qreg) + self.dag.add_creg(self.creg) + + def test_node(self): + """Test the methods add_op_node(), get_node() and get_nodes()""" + circuit = QuantumCircuit(self.qreg, self.creg) + + circuit.h(self.qreg[0]) + self.dag.add_op_node(circuit.data[0][0], circuit.data[0][1], circuit.data[0][2]) + self.assertIsInstance(self.dag.get_node(0).op, HGate) + + circuit.measure(self.qreg[0], self.creg[0]) + self.dag.add_op_node(circuit.data[1][0], circuit.data[1][1], circuit.data[1][2]) + self.assertIsInstance(self.dag.get_node(1).op, Measure) + + nodes = list(self.dag.get_nodes()) + self.assertEqual(len(list(nodes)), 2) + + for node in nodes: + self.assertIsInstance(node.op, Instruction) + + node_1 = nodes.pop() + node_2 = nodes.pop() + + self.assertIsInstance(node_1.op, Measure) + self.assertIsInstance(node_2.op, HGate) + + def test_add_edge(self): + """Test that add_edge(), get_edges(), get_all_edges(), + get_in_edges() and get_out_edges(). """ + circuit = QuantumCircuit(self.qreg, self.creg) + circuit.h(self.qreg[0]) + circuit.x(self.qreg[1]) + circuit.cx(self.qreg[1], self.qreg[0]) + circuit.measure(self.qreg[0], self.creg[0]) + + self.dag = circuit_to_dagdependency(circuit) + + second_edge = self.dag.get_edges(1, 2) + self.assertEqual(second_edge[0]['commute'], False) + + all_edges = self.dag.get_all_edges() + self.assertEqual(len(all_edges), 3) + + for edges in all_edges: + self.assertEqual(edges[2]['commute'], False) + + in_edges = self.dag.get_in_edges(2) + self.assertEqual(len(list(in_edges)), 2) + + out_edges = self.dag.get_out_edges(2) + self.assertEqual(len(list(out_edges)), 1) + + +class TestDagNodeSelection(QiskitTestCase): + """Test methods that select successors and predecessors""" + + def setUp(self): + self.dag = DAGDependency() + self.qreg = QuantumRegister(2, 'qr') + self.creg = ClassicalRegister(2, 'cr') + self.dag.add_qreg(self.qreg) + self.dag.add_creg(self.creg) + + def test_successors_predecessors(self): + """Test the method direct_successors.""" + + circuit = QuantumCircuit(self.qreg, self.creg) + circuit.h(self.qreg[0]) + circuit.x(self.qreg[0]) + circuit.h(self.qreg[0]) + circuit.x(self.qreg[1]) + circuit.h(self.qreg[0]) + circuit.measure(self.qreg[0], self.creg[0]) + + self.dag = circuit_to_dagdependency(circuit) + + dir_successors_second = self.dag.direct_successors(1) + self.assertEqual(dir_successors_second, [2, 4]) + + dir_successors_fourth = self.dag.direct_successors(3) + self.assertEqual(dir_successors_fourth, []) + + successors_second = self.dag.successors(1) + self.assertEqual(successors_second, [2, 4, 5]) + + successors_fourth = self.dag.successors(3) + self.assertEqual(successors_fourth, []) + + dir_predecessors_sixth = self.dag.direct_predecessors(5) + self.assertEqual(dir_predecessors_sixth, [2, 4]) + + dir_predecessors_fourth = self.dag.direct_predecessors(3) + self.assertEqual(dir_predecessors_fourth, []) + + predecessors_sixth = self.dag.predecessors(5) + self.assertEqual(predecessors_sixth, [0, 1, 2, 4]) + + predecessors_fourth = self.dag.predecessors(3) + self.assertEqual(predecessors_fourth, []) + + +class TestDagProperties(QiskitTestCase): + """Test the DAG properties. + """ + + def setUp(self): + qr1 = QuantumRegister(4) + qr2 = QuantumRegister(2) + circ = QuantumCircuit(qr1, qr2) + circ.h(qr1[0]) + circ.cx(qr1[2], qr1[3]) + circ.h(qr1[2]) + circ.t(qr1[2]) + circ.ch(qr1[2], qr1[1]) + circ.u2(0.1, 0.2, qr1[3]) + circ.ccx(qr2[0], qr2[1], qr1[0]) + + self.dag = circuit_to_dagdependency(circ) + + def test_size(self): + """Test total number of operations in dag.""" + self.assertEqual(self.dag.size(), 7) + + def test_dag_depth(self): + """Test dag depth.""" + self.assertEqual(self.dag.depth(), 2) + + def test_dag_depth_empty(self): + """Empty circuit DAG is zero depth + """ + q = QuantumRegister(5, 'q') + qc = QuantumCircuit(q) + dag = circuit_to_dagdependency(qc) + self.assertEqual(dag.depth(), 0) + + +if __name__ == '__main__': + unittest.main()