first pass at swap mapper, bugs fixed

This commit is contained in:
Andrew Cross 2017-04-19 23:11:59 -04:00
parent dd2f7bcfee
commit 17f9de403e
8 changed files with 452 additions and 123 deletions

View File

@ -69,6 +69,10 @@ class Circuit:
# Output precision for printing floats
self.prec = 10
def get_qubits(self):
"""Return a list of qubits as (qreg, index) pairs."""
return [(k, i) for k, v in self.qregs.items() for i in range(v)]
def rename_register(self, regname, newname):
"""Rename a classical or quantum register throughout the circuit.
@ -652,65 +656,108 @@ class Circuit:
out += "\n{\n" + self.gates[name]["body"].qasm() + "}"
return out
def qasm(self, qeflag=False):
def qasm(self, decls_only=False, add_swap=False,
no_decls=False, qeflag=False, aliases=None):
"""Return a string containing QASM for this circuit.
if qeflag is True, add a line to include "qelib1.inc"
and only generate gate code for gates not in qelib1.
if no_decls is True, only print the instructions.
if aliases is not None, aliases contains a dict mapping
the current qubits in the circuit to new qubit names.
We will deduce the register names and sizes from aliases.
if decls_only is True, only print the declarations.
if add_swap is True, add the definition of swap in terms of
cx if necessary.
"""
printed_gates = []
out = "OPENQASM 2.0;\n"
if qeflag:
out += "include \"qelib1.inc\";\n"
for k, v in sorted(self.qregs.items()):
out += "qreg %s[%d];\n" % (k, v)
for k, v in sorted(self.cregs.items()):
out += "creg %s[%d];\n" % (k, v)
omit = ["U", "CX", "measure", "reset", "barrier"]
if qeflag:
qelib = ["u3", "u2", "u1", "cx", "id", "x", "y", "z", "h",
"s", "sdg", "t", "tdg", "cz", "cy", "ccx", "cu1", "cu3"]
omit.extend(qelib)
printed_gates.extend(qelib)
for k in self.basis.keys():
if k not in omit:
if not self.gates[k]["opaque"]:
calls = self.gates[k]["body"].calls()
for c in calls:
if c not in printed_gates:
out += self._gate_string(c) + "\n"
printed_gates.append(c)
if k not in printed_gates:
out += self._gate_string(k) + "\n"
printed_gates.append(k)
ts = nx.topological_sort(self.G)
for n in ts:
nd = self.G.node[n]
if nd["type"] == "op":
if nd["condition"] is not None:
out += "if(%s==%d) " \
% (nd["condition"][0], nd["condition"][1])
if len(nd["cargs"]) == 0:
nm = nd["name"]
qarg = ",".join(map(lambda x: "%s[%d]" % (x[0], x[1]),
nd["qargs"]))
if len(nd["params"]) > 0:
param = ",".join(nd["params"])
out += "%s(%s) %s;\n" % (nm, param, qarg)
# Rename qregs if necessary
if aliases:
qregdata = {}
for q in aliases.values():
if q[0] not in qregdata:
qregdata[q[0]] = q[1] + 1
elif qregdata[q[0]] < q[1] + 1:
qregdata[q[0]] = q[1] + 1
else:
qregdata = self.qregs
# Write top matter
if no_decls:
out = ""
else:
printed_gates = []
out = "OPENQASM 2.0;\n"
if qeflag:
out += "include \"qelib1.inc\";\n"
for k, v in sorted(qregdata.items()):
out += "qreg %s[%d];\n" % (k, v)
for k, v in sorted(self.cregs.items()):
out += "creg %s[%d];\n" % (k, v)
omit = ["U", "CX", "measure", "reset", "barrier"]
if qeflag:
qelib = ["u3", "u2", "u1", "cx", "id", "x", "y", "z", "h",
"s", "sdg", "t", "tdg", "cz", "cy", "ccx", "cu1",
"cu3"]
omit.extend(qelib)
printed_gates.extend(qelib)
for k in self.basis.keys():
if k not in omit:
if not self.gates[k]["opaque"]:
calls = self.gates[k]["body"].calls()
for c in calls:
if c not in printed_gates:
out += self._gate_string(c) + "\n"
printed_gates.append(c)
if k not in printed_gates:
out += self._gate_string(k) + "\n"
printed_gates.append(k)
if add_swap and not qeflag and "cx" not in self.basis:
out += "gate cx a,b { CX a,b; }\n"
if add_swap and "swap" not in self.basis:
out += "gate swap a,b { cx a,b; cx b,a; cx a,b; }\n"
# Write the instructions
if not decls_only:
ts = nx.topological_sort(self.G)
for n in ts:
nd = self.G.node[n]
if nd["type"] == "op":
if nd["condition"] is not None:
out += "if(%s==%d) " \
% (nd["condition"][0], nd["condition"][1])
if len(nd["cargs"]) == 0:
nm = nd["name"]
if aliases:
qarglist = map(lambda x: aliases[x], nd["qargs"])
else:
qarglist = nd["qargs"]
qarg = ",".join(map(lambda x: "%s[%d]" % (x[0], x[1]),
qarglist))
if len(nd["params"]) > 0:
param = ",".join(nd["params"])
out += "%s(%s) %s;\n" % (nm, param, qarg)
else:
out += "%s %s;\n" % (nm, qarg)
else:
out += "%s %s;\n" % (nm, qarg)
else:
if nd["name"] == "measure":
assert len(nd["cargs"]) == 1 and \
len(nd["qargs"]) == 1 and \
len(nd["params"]) == 0, "bad node data"
out += "measure %s[%d] -> %s[%d];\n" \
% (nd["qargs"][0][0],
nd["qargs"][0][1],
nd["cargs"][0][0],
nd["cargs"][0][1])
else:
assert False, "bad node data"
if nd["name"] == "measure":
assert len(nd["cargs"]) == 1 and \
len(nd["qargs"]) == 1 and \
len(nd["params"]) == 0, "bad node data"
qname = nd["qargs"][0][0]
qindex = nd["qargs"][0][1]
if aliases:
newq = aliases[(qname, qindex)]
qname = newq[0]
qindex = newq[1]
out += "measure %s[%d] -> %s[%d];\n" \
% (qname,
qindex,
nd["cargs"][0][0],
nd["cargs"][0][1])
else:
assert False, "bad node data"
return out
def _check_wires_list(self, wires, name, input_circuit):
@ -1022,6 +1069,10 @@ class Circuit:
earliest layer at index 0. The layers are constructed using a
greedy algorithm. Each returned layer is a dict containing
{"graph": circuit graph, "partition": list of qubit lists}.
TODO: Gates that use the same cbits will end up in different
layers as this is currently implemented. This is may not be
the desired behavior.
"""
layers_list = []
# node_map contains an input node or previous layer node for
@ -1048,6 +1099,7 @@ class Circuit:
# 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 = list(filter(lambda x: x[2]["name"] == w,
self.G.out_edges(nbunch=[node_map[w]],
@ -1083,10 +1135,52 @@ class Circuit:
for v in itertools.chain(qa, ca, cob):
node_map[v] = nxt_nd_idx
# Add operation to partition
support_list.append(list(set(qa) | set(ca) | set(cob)))
if support_list:
if nxt_nd["name"] != "barrier":
# support_list.append(list(set(qa) | set(ca) |
# set(cob)))
support_list.append(list(set(qa)))
emit = True
if emit:
l_dict = {"graph": new_layer, "partition": support_list}
layers_list.append(l_dict)
emit = False
else:
assert not wires_with_ops_remaining, "not finished but empty?"
return layers_list
def serial_layers(self):
"""Return a list of layers for all gates of this circuit.
A serial layer is a circuit with one gate. The layers have the
same structure as in layers().
"""
layers_list = []
ts = nx.topological_sort(self.G)
for n in ts:
nxt_nd = self.G.node[n]
if nxt_nd["type"] == "op":
new_layer = Circuit()
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 = copy.deepcopy(self.basis)
new_layer.gates = copy.deepcopy(self.gates)
# Save the support of the operation we add to the layer
support_list = []
# 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)
# Add node to new_layer
new_layer.apply_operation_back(nxt_nd["name"],
qa, ca, pa, co)
# Add operation to partition
if nxt_nd["name"] != "barrier":
# support_list.append(list(set(qa) | set(ca) | set(cob)))
support_list.append(list(set(qa)))
l_dict = {"graph": new_layer, "partition": support_list}
layers_list.append(l_dict)
return layers_list

View File

@ -1 +1,2 @@
from ._coupling import Coupling
from ._layout import swap_mapper

View File

@ -9,7 +9,6 @@ onto a device with this coupling.
Author: Andrew Cross
"""
import networkx as nx
import numpy as np
from ._couplingerror import CouplingError
@ -32,6 +31,8 @@ class Coupling:
"""
# self.qubits is dict from qubit (regname,idx) tuples to node indices
self.qubits = {}
# self.index_to_qubit is a dict from node indices to qubits
self.index_to_qubit = {}
# self.node_counter is integer counter for labeling nodes
self.node_counter = 0
# self.G is the coupling digraph
@ -39,9 +40,6 @@ class Coupling:
# self.dist is a dict of dicts from node pairs to distances
# it must be computed, it is the distance on the digraph
self.dist = None
# self.hdist is a dict of dicts from node pairs to distances
# it must be computed, it is a heuristic distance function
self.hdist = None
# Add edges to the graph if the couplingstr is present
if couplingstr is not None:
edge_list = couplingstr.split(';')
@ -52,6 +50,23 @@ class Coupling:
vtuple0 = (vertex0[0], int(vertex0[1]))
vtuple1 = (vertex1[0], int(vertex1[1]))
self.add_edge(vtuple0, vtuple1)
self.compute_distance()
def size(self):
"""Return the number of qubits in this graph."""
return len(self.qubits)
def get_qubits(self):
"""Return the qubits in this graph as (qreg, index) tuples."""
return list(self.qubits.keys())
def get_edges(self):
"""Return a list of edges in the coupling graph.
Each edge is a pair of qubits and each qubit is a tuple (qreg, index).
"""
return list(map(lambda x: (self.index_to_qubit[x[0]],
self.index_to_qubit[x[1]]), self.G.edges()))
def add_qubit(self, name):
"""
@ -66,6 +81,7 @@ class Coupling:
self.G.add_node(self.node_counter)
self.G.node[self.node_counter]["name"] = name
self.qubits[name] = self.node_counter
self.index_to_qubit[self.node_counter] = name
def add_edge(self, s_name, d_name):
"""
@ -75,15 +91,9 @@ class Coupling:
d_name = destination qubit tuple
"""
if s_name not in self.qubits:
self.node_counter += 1
self.G.add_node(self.node_counter)
self.G.node[self.node_counter]["name"] = s_name
self.qubits[s_name] = self.node_counter
self.add_qubit(s_name)
if d_name not in self.qubits:
self.node_counter += 1
self.G.add_node(self.node_counter)
self.G.node[self.node_counter]["name"] = d_name
self.qubits[d_name] = self.node_counter
self.add_qubit(d_name)
self.G.add_edge(self.qubits[s_name], self.qubits[d_name])
def connected(self):
@ -94,53 +104,38 @@ class Coupling:
"""
return nx.is_weakly_connected(self.G)
def compute_distance(self, randomize=False):
def compute_distance(self):
"""
Compute the distance function on pairs of nodes.
The distance map self.dist is computed from the graph using
all_pairs_shortest_path_length. The distance map self.hdist is also
computed. If randomize is False, we use self.dist. Otherwise, we use
Sergey Bravyi's randomization heuristic.
all_pairs_shortest_path_length.
"""
if not self.connected():
raise CouplingError("coupling graph not connected")
lengths = nx.all_pairs_shortest_path_length(self.G.to_undirected())
self.dist = {}
self.hdist = {}
for i in self.qubits.keys():
self.dist[i] = {}
self.hdist[i] = {}
for j in self.qubits.keys():
self.dist[i][j] = lengths[self.qubits[i]][self.qubits[j]]
self.hdist[i][j] = self.dist[i][j]
if randomize:
for i in self.qubits.keys():
for j in self.qubits.keys():
scale = (1.0 + np.random.normal(0.0, 1.0/len(self.qubits)))
self.hdist[i][j] = scale * self.dist[i][j]**2
self.hdist[j][i] = self.hdist[i][j]
def distance(self, q1, q2, h=False):
"""
Return the distance between qubit q1 to qubit q2.
We look this up in self.dist if h is False and in self.hdist
if h is True.
"""
def distance(self, q1, q2):
"""Return the distance between qubit q1 to qubit q2."""
if self.dist is None:
raise CouplingError("distance has not been computed")
if q1 not in self.qubits:
raise CouplingError("%s not in coupling graph" % q1)
if q2 not in self.qubits:
raise CouplingError("%s not in coupling graph" % q2)
if h:
return self.hdist[q1][q2]
else:
return self.dist[q1][q2]
return self.dist[q1][q2]
def __str__(self):
"""Return a string representation of the coupling graph."""
s = "%s" % self.qubits
s += "\n%s" % self.G.edges()
s = "qubits: "
s += ", ".join(["%s[%d] @ %d" % (k[0], k[1], v)
for k, v in self.qubits.items()])
s += "\nedges: "
s += ", ".join(["%s[%d]-%s[%d]" % (e[0][0], e[0][1], e[1][0], e[1][1])
for e in self.get_edges()])
return s

248
qiskit/localize/_layout.py Normal file
View File

@ -0,0 +1,248 @@
"""
Layout object to represent placement of data qubits onto physical qubits.
Author: Andrew Cross
"""
import sys
import copy
import numpy as np
from qiskit import QISKitException
from qiskit.qasm import Qasm
import qiskit.unroll as unroll
def layer_permutation(layer_partition, layout, qubit_subset, coupling, trials):
"""Find a swap circuit that implements a permutation for this layer.
The goal is to swap qubits such that qubits in the same two qubit gates
are adjacent.
The layer_partition is a list of (qu)bit lists and each qubit is a
tuple (qreg, index).
The layout is a dict mapping qubits in the circuit to qubits in the
coupling graph and represents the current positions of the data.
The qubit_subset is the subset of qubits in the coupling graph that
we have chosen to map into.
The coupling is a CouplingGraph.
TRIALS is the number of attempts the randomized algorithm makes.
Returns: success_flag, best_circ, best_d, best_layout
If success_flag is True, then best_circ contains an OPENQASM string with
the swap circuit, best_d contains the depth of the swap circuit, and
best_layout contains the new positions of the data qubits after the
swap circuit has been applied.
"""
rev_layout = {b: a for a, b in layout.items()}
gates = []
for layer in layer_partition:
if len(layer) > 2:
raise QISKitException("Layer contains >2 qubit gates")
elif len(layer) == 2:
gates.append(tuple(layer))
# Begin loop over trials of randomized algorithm
n = coupling.size()
best_d = sys.maxsize # initialize best depth
best_circ = None # initialize best swap circuit
best_layout = None # initialize best final layout
for trial in range(trials):
trial_layout = copy.deepcopy(layout)
rev_trial_layout = copy.deepcopy(rev_layout)
trial_circ = ""
# Compute Sergey's randomized distance
xi = {}
for i in coupling.get_qubits():
xi[i] = {}
for i in coupling.get_qubits():
for j in coupling.get_qubits():
scale = 1.0 + np.random.normal(0.0, 1.0/n)
xi[i][j] = scale * coupling.distance(i, j)**2
xi[j][i] = xi[i][j]
# Loop over depths d up to a max depth of 2n+1
for d in range(1, 2*n+1):
circ = ""
# Set of available qubits
qubit_set = set(qubit_subset)
# While there are still qubits available
while qubit_set:
# Compute the objective function
min_cost = sum([xi[trial_layout[g[0]]][trial_layout[g[1]]]
for g in gates])
# Try to decrease objective function
progress_made = False
# Loop over edges of coupling graph
for e in coupling.get_edges():
# Are the qubits available?
if e[0] in qubit_set and e[1] in qubit_set:
# Try this edge to reduce the cost
new_layout = copy.deepcopy(trial_layout)
new_layout[rev_trial_layout[e[0]]] = e[1]
new_layout[rev_trial_layout[e[1]]] = e[0]
rev_new_layout = copy.deepcopy(rev_trial_layout)
rev_new_layout[e[0]] = rev_trial_layout[e[1]]
rev_new_layout[e[1]] = rev_trial_layout[e[0]]
# Compute the objective function
new_cost = sum([xi[new_layout[g[0]]][new_layout[g[1]]]
for g in gates])
# Record progress if we succceed
if new_cost < min_cost:
progress_made = True
min_cost = new_cost
opt_layout = new_layout
rev_opt_layout = rev_new_layout
opt_edge = e
# Were there any good choices?
if progress_made:
qubit_set.remove(opt_edge[0])
qubit_set.remove(opt_edge[1])
trial_layout = opt_layout
rev_trial_layout = rev_opt_layout
circ += "swap %s[%d],%s[%d]; " % (opt_edge[0][0],
opt_edge[0][1],
opt_edge[1][0],
opt_edge[1][1])
else:
break
# We have either run out of qubits or failed to improve
# Compute the coupling graph distance
dist = sum([coupling.distance(trial_layout[g[0]],
trial_layout[g[1]]) for g in gates])
# If all gates can be applied now, we are finished
# Otherwise we need to consider a deeper swap circuit
if dist == len(gates):
trial_circ = circ
break
# Either we have succeeded at some depth d < dmax or failed
dist = sum([coupling.distance(trial_layout[g[0]],
trial_layout[g[1]]) for g in gates])
if dist == len(gates):
if d < best_d:
best_circ = trial_circ
best_layout = trial_layout
best_d = min(best_d, d)
if best_circ is None:
return False, None, None, None
else:
return True, best_circ, best_d, best_layout
def swap_mapper(circuit_graph, coupling_graph,
initial_layout=None,
basis="cx,u1,u2,u3", verbose=False):
"""Map a Circuit onto a CouplingGraph using swap gates.
circuit_graph = input Circuit
coupling_graph = CouplingGraph to map onto
initial_layout = dict from qubits of circuit_graph to qubits
of coupling_graph (optional)
basis = basis string specifying basis of output Circuit
verbose = optional flag to print more information
Returns a Circuit object containing a circuit equivalent to
circuit_graph that respects couplings in coupling_graph.
"""
if circuit_graph.width() > coupling_graph.size():
raise QISKitException("Not enough qubits in CouplingGraph")
# Schedule the input circuit
layerlist = circuit_graph.layers()
if verbose:
print("schedule:")
for i in range(len(layerlist)):
print(" %d: %s" % (i, layerlist[i]["partition"]))
# Check input layout and create default layout if necessary
if initial_layout is not None:
circ_qubits = circuit_graph.get_qubits()
coup_qubits = coupling_graph.get_qubits()
qubit_subset = []
for k, v in initial_layout.values():
qubit_subset.append(v)
if k not in circ_qubits:
raise QISKitException("initial_layout qubit %s[%d] not " +
"in input Circuit" % (k[0], k[1]))
if v not in coup_qubits:
raise QISKitException("initial_layout qubit %s[%d] not " +
" in input CouplingGraph" % (k[0], k[1]))
else:
# Supply a default layout
qubit_subset = coupling_graph.get_qubits()
qubit_subset = qubit_subset[0:circuit_graph.width()]
initial_layout = {a: b for a, b in
zip(circuit_graph.get_qubits(), qubit_subset)}
# Find swap circuit to preceed to each layer of input circuit
layout = copy.deepcopy(initial_layout)
openqasm_output = ""
first_layer = True
for i in range(len(layerlist)):
success_flag, best_circ, best_d, best_layout \
= layer_permutation(layerlist[i]["partition"], layout,
qubit_subset, coupling_graph, 20)
if not success_flag:
if verbose:
print("swap_mapper: failed, layer %d, " % i,
" contention? retrying sequentially")
serial_layerlist = layerlist[i]["graph"].serial_layers()
for j in range(len(serial_layerlist)):
success_flag, best_circ, best_d, best_layout \
= layer_permutation(serial_layerlist[j]["partition"],
layout, qubit_subset, coupling_graph, 20)
if not success_flag:
raise QISKitException("swap_mapper failed: " +
"layer %d, sublayer %d" % (i, j) +
", \"%s\"" %
serial_layerlist[j]["graph"].qasm(
no_decls=True,
aliases=layout))
else:
layout = best_layout
if first_layer:
initial_layout = layout
openqasm_output += circuit_graph.qasm(add_swap=True,
decls_only=True,
aliases=layout)
openqasm_output += serial_layerlist[j]["graph"].qasm(
no_decls=True,
aliases=layout)
first_layer = False
else:
if verbose:
print("swap_mapper: layer %d (%d), depth %d"
% (i, j, best_d))
if best_circ != "":
openqasm_output += best_circ
openqasm_output += serial_layerlist[j]["graph"].qasm(
no_decls=True,
aliases=layout)
else:
layout = best_layout
if first_layer:
initial_layout = layout
openqasm_output += circuit_graph.qasm(add_swap=True,
decls_only=True,
aliases=layout)
openqasm_output += layerlist[i]["graph"].qasm(no_decls=True,
aliases=layout)
first_layer = False
else:
if verbose:
print("swap_mapper: layer %s, depth %d" % (i, best_d))
if best_circ != "":
openqasm_output += best_circ
openqasm_output += layerlist[i]["graph"].qasm(no_decls=True,
aliases=layout)
# Parse openqasm_output into Circuit object
basis += ",swap"
ast = Qasm(data=openqasm_output).parse()
u = unroll.Unroller(ast, unroll.CircuitBackend(basis.split(",")))
u.execute()
return u.be.C

View File

@ -694,7 +694,7 @@ class QasmParser(object):
'''
opaque : OPAQUE id gate_scope bit_list
'''
p[0] = Opaque([p[2], p[4]])
p[0] = node.Opaque([p[2], p[4]])
if p[2].name in self.external_functions:
raise QasmException("OPAQUE names cannot be reserved words. "
+ "Received '" + p[2].name + "'")

View File

@ -37,6 +37,7 @@ def unmajority(p, a, b, c):
# something like p = Program(c1, c2, c3)
# circ.QuantumRegister("a", n)
n = 8
a = QuantumRegister("a", n)
@ -83,7 +84,7 @@ print("width = %d" % C.width())
print("bits = %d" % C.num_cbits())
print("factors = %d" % C.num_tensor_factors())
# print("")
# print("Unrolled OPENQASM")
# print("-----------------------")
# print(C.qasm(qeflag=True))
print("")
print("Unrolled OPENQASM")
print("-----------------------")
print(C.qasm(qeflag=True))

View File

@ -49,29 +49,8 @@ c = make_unrolled_circuit(sys.argv[1], basis)
# Second, create the coupling graph
coupling = localize.Coupling(couplingstr)
print("coupling = \n%s" % coupling)
print("CouplingGraph is = \n%s" % coupling)
if not coupling.connected():
print("Coupling graph must be connected")
sys.exit(1)
print("input circuit is = \n%s" % c.qasm())
print("circuit depth = %d" % c.depth())
# Here down is hacking for now; not done
coupling.compute_distance()
for q1 in coupling.qubits.keys():
for q2 in coupling.qubits.keys():
print("%s[%d] -> %s[%d]: %f" % (q1[0], q1[1], q2[0], q2[1],
coupling.distance(q1, q2)))
layerlist = c.layers()
print("len(layerlist) = %d" % len(layerlist))
print("partition:")
for i in range(len(layerlist)):
print(" %d: %s" % (i, layerlist[i]["partition"]))
for i in range(len(layerlist)):
print("------------ layer %d ------------" % i)
print("%s" % layerlist[i]["graph"].qasm())
# Third, do the mapping
c_prime = localize.swap_mapper(c, coupling)
print("c_prime.qasm() = \n%s" % c_prime.qasm(qeflag=True))

11
testscripts/test7.qasm Normal file
View File

@ -0,0 +1,11 @@
OPENQASM 2.0;
include "qelib1.inc";
qreg q[5];
creg c[5];
cx q[0],q[4];
cx q[1],q[2];
cx q[2],q[3];
cx q[3],q[4];
ccx q[0],q[1],q[2];
measure q -> c;
if(c==1) reset q;