Skip to content

Commit

Permalink
Adding DAG visualizer (#1059)
Browse files Browse the repository at this point in the history
This PR introduces a graphical DAG visualizer to ease the task of debugging the transpilation procedure.
  • Loading branch information
delapuente authored Oct 9, 2018
1 parent 5de7f86 commit 9c8f35e
Show file tree
Hide file tree
Showing 5 changed files with 202 additions and 97 deletions.
162 changes: 78 additions & 84 deletions qiskit/dagcircuit/_dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
composed, and modified. Some natural properties like depth can be computed
directly from the graph.
"""
import itertools
import copy
from collections import OrderedDict
import copy
import itertools

import networkx as nx
import sympy

from qiskit import QuantumRegister
from qiskit import QISKitError
from qiskit import CompositeGate
from qiskit import QuantumRegister, QISKitError, CompositeGate
from ._dagcircuiterror import DAGCircuitError


Expand Down Expand Up @@ -1139,7 +1138,7 @@ def remove_nondescendants_of(self, node):
self._remove_op_node(n)

def layers(self):
"""Yield a layer for all d layers of this circuit.
"""Yield a shallow view on a layer of this DAGCircuit for all d layers of this circuit.
A layer is a circuit whose gates act on disjoint qubits, i.e.
a layer has depth 1. The total number of layers equals the
Expand All @@ -1153,84 +1152,54 @@ def layers(self):
layers as this is currently implemented. This may not be
the desired behavior.
"""
# node_map contains an input node or previous layer node for
# each wire in the circuit.
node_map = self.input_map.copy()
# wires_with_ops_remaining is a set of wire names that have
# operations we still need to assign to layers
wires_with_ops_remaining = sorted(set(self.input_map.keys()))

while wires_with_ops_remaining:
# Create a new circuit graph and populate with regs and basis
new_layer = DAGCircuit()
for k, v in self.qregs.items():
new_layer.add_qreg(k, v)
for k, v in self.cregs.items():
new_layer.add_creg(k, v)
new_layer.basis = self.basis.copy()
new_layer.gates = self.gates.copy()
# Save the support of each operation we add to the layer
support_list = []
# Determine what operations to add in this layer
# ops_touched is a map from operation nodes touched in this
# iteration to the set of their unvisited input wires. When all
# of the inputs of a touched node are visited, the node is a
# foreground node we can add to the current layer.
ops_touched = {}
wires_loop = list(wires_with_ops_remaining)
emit = False
for w in wires_loop:
oe = [x for x in self.multi_graph.out_edges(nbunch=[node_map[w]],
data=True) if
x[2]["name"] == w]
if len(oe) != 1:
raise QISKitError("should only be one out-edge per (qu)bit")

nxt_nd_idx = oe[0][1]
nxt_nd = self.multi_graph.node[nxt_nd_idx]
# If we reach an output node, we are done with this wire.
if nxt_nd["type"] == "out":
wires_with_ops_remaining.remove(w)
# Otherwise, we are somewhere inside the circuit
elif nxt_nd["type"] == "op":
# Operation data
qa = copy.copy(nxt_nd["qargs"])
ca = copy.copy(nxt_nd["cargs"])
pa = copy.copy(nxt_nd["params"])
co = copy.copy(nxt_nd["condition"])
cob = self._bits_in_condition(co)
# First time we see an operation, add to ops_touched
if nxt_nd_idx not in ops_touched:
ops_touched[nxt_nd_idx] = set(qa) | set(ca) | set(cob)
# Mark inputs visited by deleting from set
# NOTE: expect trouble with if(c==1) measure q -> c;
if w not in ops_touched[nxt_nd_idx]:
raise QISKitError("expected wire")

ops_touched[nxt_nd_idx].remove(w)
# Node becomes "foreground" if set becomes empty,
# i.e. every input is available for this operation
if not ops_touched[nxt_nd_idx]:
# Add node to new_layer
new_layer.apply_operation_back(nxt_nd["name"],
qa, ca, pa, co)
# Update node_map to point to this op
for v in itertools.chain(qa, ca, cob):
node_map[v] = nxt_nd_idx
# Add operation to partition
if nxt_nd["name"] not in ["barrier",
"snapshot", "save", "load", "noise"]:
# support_list.append(list(set(qa) | set(ca) |
# set(cob)))
support_list.append(list(qa))
emit = True
if emit:
l_dict = {"graph": new_layer, "partition": support_list}
yield l_dict
emit = False
else:
if wires_with_ops_remaining:
raise QISKitError("not finished but empty?")
graph_layers = self.multigraph_layers()
try:
next(graph_layers) # Remove input nodes
except StopIteration:
return

def nodes_data(nodes):
"""Construct full nodes from just node ids."""
return ((node_id, self.multi_graph.nodes[node_id]) for node_id in nodes)

for graph_layer in graph_layers:
# Get the op nodes from the layer, removing any input and ouput nodes.
op_nodes = [node for node in nodes_data(graph_layer) if node[1]["type"] == "op"]

# Stop yielding once there are no more op_nodes in a layer.
if not op_nodes:
return

# Construct a shallow copy of self
new_layer = copy.copy(self)
new_layer.multi_graph = nx.MultiDiGraph()

new_layer.multi_graph.add_nodes_from(nodes_data(self.input_map.values()))
new_layer.multi_graph.add_nodes_from(nodes_data(self.output_map.values()))

# The quantum registers that have an operation in this layer.
support_list = [
op_node[1]["qargs"]
for op_node in op_nodes
if op_node[1]["name"] not in {"barrier", "snapshot", "save", "load", "noise"}
]
new_layer.multi_graph.add_nodes_from(op_nodes)

# Now add the edges to the multi_graph
# By default we just wire inputs to the outputs.
wires = {self.input_map[register]: self.output_map[register]
for register in self.wire_type}
# Wire inputs to op nodes, and op nodes to outputs.
for op_node in op_nodes:
args = self._bits_in_condition(op_node[1]["condition"]) \
+ op_node[1]["cargs"] + op_node[1]["qargs"]
arg_ids = (self.input_map[arg] for arg in args) # map from ("q",0) to node id.
for arg_id in arg_ids:
wires[arg_id], wires[op_node[0]] = op_node[0], wires[arg_id]

# Add wiring to/from the operations and between unused inputs & outputs.
new_layer.multi_graph.add_edges_from(wires.items())
yield {"graph": new_layer, "partition": support_list}

def serial_layers(self):
"""Yield a layer for all gates of this circuit.
Expand Down Expand Up @@ -1267,6 +1236,31 @@ def serial_layers(self):
l_dict = {"graph": new_layer, "partition": support_list}
yield l_dict

def multigraph_layers(self):
"""Yield layers of the multigraph."""
predecessor_count = dict() # Dict[node, predecessors not visited]
cur_layer = [node for node in self.input_map.values()]
yield cur_layer
next_layer = []
while cur_layer:
for node in cur_layer:
# Count multiedges with multiplicity.
for successor in self.multi_graph.successors(node):
multiplicity = self.multi_graph.number_of_edges(node, successor)
if successor in predecessor_count:
predecessor_count[successor] -= multiplicity
else:
predecessor_count[successor] =\
self.multi_graph.in_degree(successor) - multiplicity

if predecessor_count[successor] == 0:
next_layer.append(successor)
del predecessor_count[successor]

yield next_layer
cur_layer = next_layer
next_layer = []

def collect_runs(self, namelist):
"""Return a set of runs of "op" nodes with the given names.
Expand Down
3 changes: 2 additions & 1 deletion qiskit/tools/visualization/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
# This source code is licensed under the Apache License, Version 2.0 found in
# the LICENSE.txt file in the root directory of this source tree.

"""Main QISKit visualization methods."""
"""Main Qiskit visualization methods."""

import sys
from qiskit._util import _has_connection
from ._circuit_visualization import circuit_drawer, plot_circuit, generate_latex_source,\
latex_circuit_drawer, matplotlib_circuit_drawer, qx_color_scheme
from ._error import VisualizationError
from ._state_visualization import plot_bloch_vector
from ._dag_visualization import dag_drawer


if ('ipykernel' in sys.modules) and ('spyder' not in sys.modules):
Expand Down
74 changes: 74 additions & 0 deletions qiskit/tools/visualization/_dag_visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# -*- coding: utf-8 -*-

# Copyright 2018, IBM.
#
# This source code is licensed under the Apache License, Version 2.0 found in
# the LICENSE.txt file in the root directory of this source tree.

# pylint: disable=invalid-name

"""
Visualization function for DAG circuit representation.
"""

import sys
import copy
import nxpd
from ._error import VisualizationError


def dag_drawer(dag, scale=0.7, filename=None, style='color'):
"""Plot the directed acyclic graph (dag) to represent operation dependencies
in a quantum circuit.
Args:
dag (DAGCircuit): The dag to draw.
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.
Raises:
VisualizationError: when style is not recognized.
"""
G = copy.deepcopy(dag.multi_graph) # don't modify the original graph attributes
G.graph['dpi'] = 100 * scale

if style == 'plain':
pass
elif style == 'color':
for node in G.nodes:
n = G.nodes[node]
if n['type'] == 'op':
n['label'] = str(n['name'])
n['color'] = 'blue'
n['style'] = 'filled'
n['fillcolor'] = 'lightblue'
if n['type'] == 'in':
n['label'] = n['name'][0] + '[' + str(n['name'][1]) + ']'
n['color'] = 'black'
n['style'] = 'filled'
n['fillcolor'] = 'green'
if n['type'] == 'out':
n['label'] = n['name'][0] + '[' + str(n['name'][1]) + ']'
n['color'] = 'black'
n['style'] = 'filled'
n['fillcolor'] = 'red'
for e in G.edges(data=True):
e[2]['label'] = e[2]['name'][0] + "[" + str(e[2]['name'][1]) + "]"
else:
raise VisualizationError("Unrecognized style for the dag_drawer.")

show = nxpd.nxpdParams['show']
if filename:
show = False
elif ('ipykernel' in sys.modules) and ('spyder' not in sys.modules):
show = 'ipynb'
else:
show = True

return nxpd.draw(G, filename=filename, show=show)
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ scipy>=0.19,!=0.19.1
sympy>=1.0
pillow>=4.2.1
psutil>=5
nxpd>=0.2
59 changes: 47 additions & 12 deletions test/python/test_dagcircuit.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,29 +16,28 @@


class TestDagCircuit(QiskitTestCase):
"""QasmParser"""

"""Testing the dag circuit representation"""
def test_create(self):
qubit0 = ('qr', 0)
qubit1 = ('qr', 1)
clbit0 = ('cr', 0)
clbit1 = ('cr', 1)
condition = None
condition = ('cr', 3)
dag = DAGCircuit()
dag.add_basis_element('h', 1, number_classical=0, number_parameters=0)
dag.add_basis_element('cx', 2)
dag.add_basis_element('x', 1)
dag.add_basis_element('measure', 1, number_classical=1,
number_parameters=0)
dag.add_basis_element('measure', 1, number_classical=1, number_parameters=0)
dag.add_qreg('qr', 2)
dag.add_creg('cr', 2)
dag.apply_operation_back('h', [qubit0], [], [], condition)
dag.apply_operation_back('cx', [qubit0, qubit1], [],
[], condition)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition)
dag.apply_operation_back('x', [qubit1], [], [], ('cr', 1))
dag.apply_operation_back('measure', [qubit0], [clbit0], [], condition)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition)
dag.apply_operation_back('h', [qubit0], [], [], condition=None)
dag.apply_operation_back('cx', [qubit0, qubit1], [], [], condition=None)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition=None)
dag.apply_operation_back('x', [qubit1], [], [], condition=condition)
dag.apply_operation_back('measure', [qubit0], [clbit0], [], condition=None)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition=None)
self.assertEqual(len(dag.multi_graph.nodes), 14)
self.assertEqual(len(dag.multi_graph.edges), 16)

def test_get_named_nodes(self):
dag = DAGCircuit()
Expand All @@ -61,6 +60,42 @@ def test_get_named_nodes(self):
(('q', 0), ('q', 2))}
self.assertEqual(expected_gates, node_qargs)

def test_layers_basic(self):
qubit0 = ('qr', 0)
qubit1 = ('qr', 1)
clbit0 = ('cr', 0)
clbit1 = ('cr', 1)
condition = ('cr', 3)
dag = DAGCircuit()
dag.add_basis_element('h', 1, number_classical=0, number_parameters=0)
dag.add_basis_element('cx', 2)
dag.add_basis_element('x', 1)
dag.add_basis_element('measure', 1, number_classical=1, number_parameters=0)
dag.add_qreg('qr', 2)
dag.add_creg('cr', 2)
dag.apply_operation_back('h', [qubit0], [], [], condition=None)
dag.apply_operation_back('cx', [qubit0, qubit1], [], [], condition=None)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition=None)
dag.apply_operation_back('x', [qubit1], [], [], condition=condition)
dag.apply_operation_back('measure', [qubit0], [clbit0], [], condition=None)
dag.apply_operation_back('measure', [qubit1], [clbit1], [], condition=None)

layers = list(dag.layers())
self.assertEqual(5, len(layers))

name_layers = [
[node[1]["name"]
for node in layer["graph"].multi_graph.nodes(data=True)
if node[1]["type"] == "op"] for layer in layers]

self.assertEqual([
['h'],
['cx'],
['measure'],
['x'],
['measure', 'measure']
], name_layers)


if __name__ == '__main__':
unittest.main()

0 comments on commit 9c8f35e

Please sign in to comment.