Skip to content

Commit

Permalink
Support block collection for non-CX two-qubit gates.
Browse files Browse the repository at this point in the history
  • Loading branch information
kdk committed Jun 25, 2020
1 parent 26da46c commit be0e644
Show file tree
Hide file tree
Showing 6 changed files with 126 additions and 40 deletions.
4 changes: 3 additions & 1 deletion qiskit/quantum_info/synthesis/two_qubit_decompose.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from qiskit.circuit.library.standard_gates.u3 import U3Gate
from qiskit.circuit.library.standard_gates.x import CXGate
from qiskit.exceptions import QiskitError
from qiskit.quantum_info.operators import Operator
from qiskit.quantum_info.operators.predicates import is_unitary_matrix
from qiskit.quantum_info.synthesis.weyl import weyl_coordinates
from qiskit.quantum_info.synthesis.one_qubit_decompose import OneQubitEulerDecomposer
Expand Down Expand Up @@ -284,7 +285,8 @@ class TwoQubitBasisDecomposer():
def __init__(self, gate, basis_fidelity=1.0):
self.gate = gate
self.basis_fidelity = basis_fidelity
basis = self.basis = TwoQubitWeylDecomposition(gate.to_matrix())

basis = self.basis = TwoQubitWeylDecomposition(Operator(gate).data)

# FIXME: find good tolerances
self.is_supercontrolled = np.isclose(basis.a, np.pi/4) and np.isclose(basis.c, 0.)
Expand Down
80 changes: 57 additions & 23 deletions qiskit/transpiler/passes/optimization/collect_2q_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,38 @@ def run(self, dag):
# Initiate the commutation set
self.property_set['commutation_set'] = defaultdict(list)

good_names = ["cx", "u1", "u2", "u3", "id"]
good_1q_names = ["u1", "u2", "u3", "id", "rx", "ry", "rz"]
good_2q_names = ["cx", "rxx", "cz", "iswap"]
good_names = good_1q_names + good_2q_names
block_list = []
nodes = list(dag.topological_nodes())
nodes_seen = dict(zip(nodes, [False] * len(nodes)))
for nd in dag.topological_op_nodes():

group = []
# Explore predecessors and successors of cx gates
if nd.name == "cx" and nd.condition is None and not nodes_seen[nd]:
if (
nd.name in good_2q_names
and not nodes_seen[nd]
and nd.condition is None
and not nd.op.is_parameterized()
):
these_qubits = set(nd.qargs)
# Explore predecessors of the "cx" node
# Explore predecessors of the 2q node
pred = list(dag.quantum_predecessors(nd))
explore = True
while explore:
pred_next = []
# If there is one predecessor, add it if it's on the right qubits
if len(pred) == 1 and not nodes_seen[pred[0]]:
pnd = pred[0]
if pnd.name in good_names and pnd.condition is None:
if (pnd.name == "cx" and set(pnd.qargs) == these_qubits) or \
pnd.name != "cx" and not pnd.op.is_parameterized():
if (
pnd.name in good_names
and pnd.condition is None
and not pnd.op.is_parameterized()
):
if (pnd.name in good_2q_names and set(pnd.qargs) == these_qubits) \
or pnd.name not in good_2q_names:
group.append(pnd)
nodes_seen[pnd] = True
pred_next.extend(dag.quantum_predecessors(pnd))
Expand All @@ -85,33 +96,42 @@ def run(self, dag):
# We need to avoid accidentally adding a cx on these_qubits
# since these must have a dependency through the other predecessor
# in this case
if pred[0].name == "cx" and set(pred[0].qargs) == these_qubits:
if pred[0].name in good_2q_names and set(pred[0].qargs) == these_qubits:
sorted_pred = [pred[1]]
elif pred[1].name == "cx" and set(pred[1].qargs) == these_qubits:
elif (pred[1].name in good_2q_names
and set(pred[1].qargs) == these_qubits):
sorted_pred = [pred[0]]
else:
sorted_pred = pred
if len(sorted_pred) == 2 and sorted_pred[0].name == "cx" and \
sorted_pred[1].name == "cx":
if len(sorted_pred) == 2 and sorted_pred[0].name in good_2q_names and \
sorted_pred[1].name in good_2q_names:
break # stop immediately if we hit a pair of cx
# Examine each predecessor
for pnd in sorted_pred:
if pnd.name not in good_names or pnd.condition is not None:
if (
pnd.name not in good_names
or pnd.condition is not None
or pnd.op.is_parameterized()
):
# remove any qubits that are interrupted by a gate
# e.g. a measure in the middle of the circuit
these_qubits = list(set(these_qubits) -
set(pnd.qargs))
continue
# If a predecessor is a single qubit gate, add it
if pnd.name != "cx" and not pnd.op.is_parameterized():
if pnd.name not in good_2q_names and not pnd.op.is_parameterized():
if not nodes_seen[pnd]:
group.append(pnd)
nodes_seen[pnd] = True
pred_next.extend(dag.quantum_predecessors(pnd))
# If cx, check qubits
else:
pred_qubits = set(pnd.qargs)
if pred_qubits == these_qubits and pnd.condition is None:
if (
pred_qubits == these_qubits
and pnd.condition is None
and not pnd.op.is_parameterized()
):
# add if on same qubits
if not nodes_seen[pnd]:
group.append(pnd)
Expand Down Expand Up @@ -140,9 +160,13 @@ def run(self, dag):
# If there is one successor, add it if its on the right qubits
if len(succ) == 1 and not nodes_seen[succ[0]]:
snd = succ[0]
if snd.name in good_names and snd.condition is None:
if (snd.name == "cx" and set(snd.qargs) == these_qubits) or \
snd.name != "cx" and not snd.op.is_parameterized():
if (
snd.name in good_names
and snd.condition is None
and not snd.op.is_parameterized()
):
if (snd.name in good_2q_names and set(snd.qargs) == these_qubits) or \
snd.name not in good_2q_names:
group.append(snd)
nodes_seen[snd] = True
succ_next.extend(dag.quantum_successors(snd))
Expand All @@ -157,19 +181,25 @@ def run(self, dag):
# We need to avoid accidentally adding a cx on these_qubits
# since these must have a dependency through the other successor
# in this case
if succ[0].name == "cx" and set(succ[0].qargs) == these_qubits:
if (succ[0].name in good_2q_names
and set(succ[0].qargs) == these_qubits):
sorted_succ = [succ[1]]
elif succ[1].name == "cx" and set(succ[1].qargs) == these_qubits:
elif (succ[1].name in good_2q_names
and set(succ[1].qargs) == these_qubits):
sorted_succ = [succ[0]]
else:
sorted_succ = succ
if len(sorted_succ) == 2 and \
sorted_succ[0].name == "cx" and \
sorted_succ[1].name == "cx":
sorted_succ[0].name in good_2q_names and \
sorted_succ[1].name in good_2q_names:
break # stop immediately if we hit a pair of cx
# Examine each successor
for snd in sorted_succ:
if snd.name not in good_names or snd.condition is not None:
if (
snd.name not in good_names
or snd.condition is not None
or snd.op.is_parameterized()
):
# remove qubits from consideration if interrupted
# by a gate e.g. a measure in the middle of the circuit
these_qubits = list(set(these_qubits) -
Expand All @@ -179,15 +209,19 @@ def run(self, dag):
# If a successor is a single qubit gate, add it
# NB as we have eliminated all gates with names not in
# good_names, this check guarantees they are single qubit
if snd.name != "cx" and not snd.op.is_parameterized():
if snd.name not in good_2q_names and not snd.op.is_parameterized():
if not nodes_seen[snd]:
group.append(snd)
nodes_seen[snd] = True
succ_next.extend(dag.quantum_successors(snd))
else:
# If cx, check qubits
succ_qubits = set(snd.qargs)
if succ_qubits == these_qubits and snd.condition is None:
if (
succ_qubits == these_qubits
and snd.condition is None
and not snd.op.is_parameterized()
):
# add if on same qubits
if not nodes_seen[snd]:
group.append(snd)
Expand Down
25 changes: 19 additions & 6 deletions qiskit/transpiler/passes/optimization/consolidate_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,21 @@ def __init__(self, kak_basis_gate=CXGate(), force_consolidate=False):
"""
super().__init__()
self.force_consolidate = force_consolidate
self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate)
if kak_basis_gate is not None:
self.decomposer = TwoQubitBasisDecomposer(kak_basis_gate)
else:
self.decomposer = None

def run(self, dag):
"""Run the ConsolidateBlocks pass on `dag`.
Iterate over each block and replace it with an equivalent Unitary
on the same wires.
"""

if self.decomposer is None:
return dag

new_dag = DAGCircuit()
for qreg in dag.qregs.values():
new_dag.add_qreg(qreg)
Expand Down Expand Up @@ -97,8 +104,8 @@ def run(self, dag):
# create the dag from the updated list of blocks
basis_gate_name = self.decomposer.gate.name
for block in blocks:

if len(block) == 1 and block[0].name != 'cx':
if len(block) == 1 and (block[0].name not in ['cx', 'cz', 'rxx', 'iswap']
or block[0].op.is_parameterized()):
# an intermediate node that was added into the overall list
new_dag.apply_operation_back(block[0].op, block[0].qargs,
block[0].cargs, block[0].condition)
Expand All @@ -120,10 +127,16 @@ def run(self, dag):
subcirc.append(nd.op, [q[block_index_map[i]] for i in nd.qargs])
unitary = UnitaryGate(Operator(subcirc)) # simulates the circuit
if self.force_consolidate or unitary.num_qubits > 2 or \
self.decomposer.num_basis_gates(unitary) != basis_count:
self.decomposer.num_basis_gates(unitary) < basis_count:

new_dag.apply_operation_back(
unitary, sorted(block_qargs, key=lambda x: block_index_map[x]))
if len(block_qargs) <= 2:
new_dag.apply_operation_back(
self.decomposer(unitary).to_gate(),
sorted(block_qargs, key=lambda x: block_index_map[x]))
else:
new_dag.apply_operation_back(
UnitaryGate(unitary),
sorted(block_qargs, key=lambda x: block_index_map[x]))
else:
for nd in block:
new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs, nd.condition)
Expand Down
15 changes: 14 additions & 1 deletion qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
gate cancellation using commutativity rules and unitary synthesis.
"""

import math

from qiskit.transpiler.passmanager_config import PassManagerConfig
from qiskit.transpiler.passmanager import PassManager

Expand Down Expand Up @@ -162,7 +164,18 @@ def _opt_control(property_set):

_meas = [OptimizeSwapBeforeMeasure(), RemoveDiagonalGatesBeforeMeasure()]

_opt = [Collect2qBlocks(), ConsolidateBlocks(),
# Choose the first available 2q gate to use in the KAK decomposition.
from qiskit.circuit.library.standard_gates import iSwapGate, CXGate, CZGate, RXXGate
kak_gate_names = {
'iswap': iSwapGate(), 'cx': CXGate(), 'cz': CZGate(), 'rxx': RXXGate(math.pi / 2)
}

kak_gate = None
kak_gates = set(basis_gates or []).intersection(kak_gate_names.keys())
if kak_gates:
kak_gate = kak_gate_names[kak_gates.pop()]

_opt = [Collect2qBlocks(), ConsolidateBlocks(kak_basis_gate=kak_gate),
Optimize1qGates(basis_gates), CommutativeCancellation()]

# Build pass manager
Expand Down
27 changes: 25 additions & 2 deletions test/python/compiler/test_transpiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@

"""Tests basic functionality of the transpile function"""

import math
import io
import sys
import math

from logging import StreamHandler, getLogger
from unittest.mock import patch
import sys

from ddt import ddt, data

from qiskit import BasicAer
Expand Down Expand Up @@ -696,6 +698,27 @@ def test_measure_doesnt_unroll_ms(self, optimization_level):

self.assertEqual(qc, out)

@data(
['cx', 'u3'],
['cz', 'u3'],
['cz', 'rx', 'rz'],
['rxx', 'rx', 'ry'],
['iswap', 'rx', 'rz'],
)
def test_block_collection_runs_for_non_cx_bases(self, basis_gates):
"""Verify block collection is run when a single two qubit gate is in the basis."""
twoq_gate, *_ = basis_gates

qc = QuantumCircuit(2)
qc.cx(0, 1)
qc.cx(1, 0)
qc.cx(0, 1)
qc.cx(0, 1)

out = transpile(qc, basis_gates=basis_gates, optimization_level=3)

self.assertLessEqual(out.count_ops()[twoq_gate], 2)


class StreamHandlerRaiseException(StreamHandler):
"""Handler class that will raise an exception on formatting errors."""
Expand Down
15 changes: 8 additions & 7 deletions test/python/transpiler/test_consolidate_blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from qiskit.execute import execute
from qiskit.transpiler.passes import ConsolidateBlocks
from qiskit.providers.basicaer import UnitarySimulatorPy
from qiskit.quantum_info.operators import Operator
from qiskit.quantum_info.operators.measures import process_fidelity
from qiskit.test import QiskitTestCase
from qiskit.transpiler import PassManager
Expand Down Expand Up @@ -53,7 +54,7 @@ def test_consolidate_small_block(self):
result = execute(qc, sim).result()
unitary = UnitaryGate(result.get_unitary())
self.assertEqual(len(new_dag.op_nodes()), 1)
fidelity = process_fidelity(new_dag.op_nodes()[0].op.to_matrix(), unitary.to_matrix())
fidelity = process_fidelity(Operator(new_dag.op_nodes()[0].op), unitary.to_matrix())
self.assertAlmostEqual(fidelity, 1.0, places=7)

def test_wire_order(self):
Expand All @@ -71,10 +72,10 @@ def test_wire_order(self):
self.assertEqual(new_node.qargs, [qr[0], qr[1]])
# the canonical CNOT matrix occurs when the control is more
# significant than target, which is the case here
fidelity = process_fidelity(new_node.op.to_matrix(), np.array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0]]))
fidelity = process_fidelity(Operator(new_node.op), np.array([[1, 0, 0, 0],
[0, 1, 0, 0],
[0, 0, 0, 1],
[0, 0, 1, 0]]))
self.assertAlmostEqual(fidelity, 1.0, places=7)

def test_topological_order_preserved(self):
Expand Down Expand Up @@ -124,7 +125,7 @@ def test_3q_blocks(self):
result = execute(qc, sim).result()
unitary = UnitaryGate(result.get_unitary())
self.assertEqual(len(new_dag.op_nodes()), 1)
fidelity = process_fidelity(new_dag.op_nodes()[0].op.to_matrix(), unitary.to_matrix())
fidelity = process_fidelity(Operator(new_dag.op_nodes()[0].op), unitary.to_matrix())
self.assertAlmostEqual(fidelity, 1.0, places=7)

def test_block_spanning_two_regs(self):
Expand All @@ -145,7 +146,7 @@ def test_block_spanning_two_regs(self):
result = execute(qc, sim).result()
unitary = UnitaryGate(result.get_unitary())
self.assertEqual(len(new_dag.op_nodes()), 1)
fidelity = process_fidelity(new_dag.op_nodes()[0].op.to_matrix(), unitary.to_matrix())
fidelity = process_fidelity(Operator(new_dag.op_nodes()[0].op), unitary.to_matrix())
self.assertAlmostEqual(fidelity, 1.0, places=7)

def test_block_spanning_two_regs_different_index(self):
Expand Down

0 comments on commit be0e644

Please sign in to comment.