qiskit/test/python/transpiler/test_unitary_synthesis.py

1030 lines
40 KiB
Python

# This code is part of Qiskit.
#
# (C) Copyright IBM 2017, 2024.
#
# 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=missing-function-docstring
"""
Tests for the default UnitarySynthesis transpiler pass.
"""
import unittest
import math
import numpy as np
import scipy
from ddt import ddt, data
from qiskit import transpile, generate_preset_pass_manager
from qiskit.providers.fake_provider import GenericBackendV2
from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit.circuit.library import quantum_volume
from qiskit.circuit.parameterexpression import ParameterValueType
from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit.transpiler.passes import UnitarySynthesis
from qiskit.quantum_info.operators import Operator
from qiskit.quantum_info.random import random_unitary
from qiskit.transpiler import PassManager, CouplingMap, Target, InstructionProperties
from qiskit.exceptions import QiskitError
from qiskit.transpiler.passes import (
Collect2qBlocks,
ConsolidateBlocks,
Optimize1qGates,
SabreLayout,
Unroll3qOrMore,
CheckMap,
BarrierBeforeFinalMeasurements,
SabreSwap,
TrivialLayout,
)
from qiskit.circuit.library import (
IGate,
CXGate,
RZGate,
RXGate,
SXGate,
XGate,
iSwapGate,
ECRGate,
UGate,
ZGate,
RYYGate,
RZZGate,
RXXGate,
PauliEvolutionGate,
CPhaseGate,
)
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit import Measure
from qiskit.circuit.controlflow import IfElseOp
from qiskit.circuit import Parameter, Gate
from qiskit.synthesis.unitary.qsd import qs_decomposition
from test import combine # pylint: disable=wrong-import-order
from test import QiskitTestCase # pylint: disable=wrong-import-order
from test.python.providers.fake_mumbai_v2 import ( # pylint: disable=wrong-import-order
FakeMumbaiFractionalCX,
)
from ..legacy_cmaps import YORKTOWN_CMAP
class FakeBackend2QV2(GenericBackendV2):
"""A 2-qubit fake backend"""
def __init__(self):
super().__init__(num_qubits=2, basis_gates=["rx", "u"], seed=42)
cx_props = {
(0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115),
}
self._target.add_instruction(CXGate(), cx_props)
ecr_props = {
(1, 0): InstructionProperties(duration=4.52e-9, error=0.0000132115),
}
self._target.add_instruction(ECRGate(), ecr_props)
class FakeBackend5QV2(GenericBackendV2):
"""A 5-qubit fake backend"""
def __init__(self, bidirectional=True):
super().__init__(num_qubits=5, basis_gates=["u"], seed=42)
cx_props = {
(0, 1): InstructionProperties(duration=5.23e-7, error=0.00098115),
(3, 4): InstructionProperties(duration=5.23e-7, error=0.00098115),
}
if bidirectional:
cx_props[(1, 0)] = InstructionProperties(duration=6.23e-7, error=0.00099115)
cx_props[(4, 3)] = InstructionProperties(duration=7.23e-7, error=0.00099115)
self._target.add_instruction(CXGate(), cx_props)
ecr_props = {
(1, 2): InstructionProperties(duration=4.52e-9, error=0.0000132115),
(2, 3): InstructionProperties(duration=4.52e-9, error=0.0000132115),
}
if bidirectional:
ecr_props[(2, 1)] = InstructionProperties(duration=5.52e-9, error=0.0000232115)
ecr_props[(3, 2)] = InstructionProperties(duration=5.52e-9, error=0.0000232115)
@ddt
class TestUnitarySynthesisBasisGates(QiskitTestCase):
"""Test UnitarySynthesis pass with basis gates."""
def test_empty_basis_gates(self):
"""Verify when basis_gates is None, we do not synthesize unitaries."""
qc = QuantumCircuit(3)
op_1q = random_unitary(2, seed=0)
op_2q = random_unitary(4, seed=0)
op_3q = random_unitary(8, seed=0)
qc.unitary(op_1q.data, [0])
qc.unitary(op_2q.data, [0, 1])
qc.unitary(op_3q.data, [0, 1, 2])
out = UnitarySynthesis(basis_gates=None, min_qubits=2)(qc)
self.assertEqual(out.count_ops(), {"unitary": 3})
@data(
["u3", "cx"],
["u1", "u2", "u3", "cx"],
["ry", "rz", "rxx"],
["rx", "rz", "rzz"],
["rx", "rz", "iswap"],
["u3", "rx", "rz", "cz", "iswap"],
)
def test_two_qubit_synthesis_to_basis(self, basis_gates):
"""Verify two qubit unitaries are synthesized to match basis gates."""
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
bell_op = Operator(bell)
qc = QuantumCircuit(2)
qc.unitary(bell_op, [0, 1])
dag = circuit_to_dag(qc)
out = UnitarySynthesis(basis_gates).run(dag)
self.assertTrue(set(out.count_ops()).issubset(basis_gates))
@data(True, False, None)
def test_two_qubit_synthesis_to_directional_cx_from_coupling_map(self, natural_direction):
"""Verify natural cx direction is used when specified in coupling map."""
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=coupling_map,
pulse_optimize=True,
natural_direction=natural_direction,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
qc_out = pm.run(qc)
if natural_direction is False:
self.assertTrue(
all(((qr[1], qr[0]) == instr.qubits for instr in qc_out.get_instructions("cx")))
)
else:
# the decomposer defaults to the [1, 0] direction but the coupling
# map specifies a [0, 1] direction. Check that this is respected.
self.assertTrue(
all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx")))
)
self.assertEqual(Operator(qc), Operator(qc_out))
def test_two_qubit_synthesis_not_pulse_optimal(self):
"""Verify not attempting pulse optimal decomposition when pulse_optimize==False."""
qr = QuantumRegister(2)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
coupling_map = CouplingMap([[0, 1]])
pm_nonoptimal = PassManager(
[
TrivialLayout(coupling_map),
UnitarySynthesis(
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=coupling_map,
pulse_optimize=False,
natural_direction=True,
),
]
)
pm_optimal = PassManager(
[
TrivialLayout(coupling_map),
UnitarySynthesis(
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=coupling_map,
pulse_optimize=True,
natural_direction=True,
),
]
)
qc_nonoptimal = pm_nonoptimal.run(qc)
qc_optimal = pm_optimal.run(qc)
self.assertGreater(qc_nonoptimal.count_ops()["sx"], qc_optimal.count_ops()["sx"])
def test_two_qubit_pulse_optimal_true_raises(self):
"""Verify raises if pulse optimal==True but cx is not in the basis."""
basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
# this assumes iswap pulse optimal decomposition doesn't exist
basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates]
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
basis_gates=basis_gates,
coupling_map=coupling_map,
pulse_optimize=True,
natural_direction=True,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
with self.assertRaises(QiskitError):
pm.run(qc)
def test_two_qubit_natural_direction_true_gate_length_raises(self):
"""Verify that error is raised if preferred direction cannot be inferred
from gate lenghts/errors.
"""
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]])
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
pulse_optimize=True,
natural_direction=True,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
with self.assertRaises(QiskitError):
pm.run(qc)
def test_two_qubit_pulse_optimal_none_optimal(self):
"""Verify pulse optimal decomposition when pulse_optimize==None."""
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=coupling_map,
pulse_optimize=None,
natural_direction=True,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
qc_out = pm.run(qc)
if isinstance(qc_out, QuantumCircuit):
num_ops = qc_out.count_ops()
else:
num_ops = qc_out[0].count_ops()
self.assertIn("sx", num_ops)
self.assertLessEqual(num_ops["sx"], 12)
def test_two_qubit_pulse_optimal_none_no_raise(self):
"""Verify pulse optimal decomposition when pulse_optimize==None doesn't
raise when pulse optimal decomposition unknown."""
basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
# this assumes iswap pulse optimal decomposition doesn't exist
basis_gates = [gate if gate != "cx" else "iswap" for gate in basis_gates]
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 2], [1, 3], [3, 4]])
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
basis_gates=basis_gates,
coupling_map=coupling_map,
pulse_optimize=None,
natural_direction=True,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
try:
qc_out = pm.run(qc)
except QiskitError:
self.fail("pulse_optimize=None raised exception unexpectedly")
if isinstance(qc_out, QuantumCircuit):
num_ops = qc_out.count_ops()
else:
num_ops = qc_out[0].count_ops()
self.assertIn("sx", num_ops)
self.assertLessEqual(num_ops["sx"], 14)
def test_qv_natural(self):
"""Check that quantum volume circuit compiles for natural direction"""
qv64 = quantum_volume(5, seed=15)
def construct_passmanager(basis_gates, coupling_map, synthesis_fidelity, pulse_optimize):
seed = 2
_map = [SabreLayout(coupling_map, max_iterations=2, seed=seed)]
_unroll3q = Unroll3qOrMore()
_swap_check = CheckMap(coupling_map)
_swap = [
BarrierBeforeFinalMeasurements(),
SabreSwap(coupling_map, heuristic="lookahead", seed=seed),
]
_optimize = [
Collect2qBlocks(),
ConsolidateBlocks(basis_gates=basis_gates),
UnitarySynthesis(
basis_gates,
synthesis_fidelity,
coupling_map,
pulse_optimize=pulse_optimize,
natural_direction=True,
),
Optimize1qGates(basis_gates),
]
pm = PassManager()
pm.append(_map) # map to hardware by inserting swaps
pm.append(_unroll3q)
pm.append(_swap_check)
pm.append(_swap)
pm.append(_optimize)
return pm
coupling_map = CouplingMap([[0, 1], [1, 2], [3, 2], [3, 4], [5, 4]])
basis_gates = ["rz", "sx", "cx"]
pm1 = construct_passmanager(
basis_gates=basis_gates,
coupling_map=coupling_map,
synthesis_fidelity=0.99,
pulse_optimize=True,
)
pm2 = construct_passmanager(
basis_gates=basis_gates,
coupling_map=coupling_map,
synthesis_fidelity=0.99,
pulse_optimize=False,
)
qv64_1 = pm1.run(qv64.decompose())
qv64_2 = pm2.run(qv64.decompose())
edges = [list(edge) for edge in coupling_map.get_edges()]
self.assertTrue(
all(
[qv64_1.qubits.index(qubit) for qubit in instr.qubits] in edges
for instr in qv64_1.get_instructions("cx")
)
)
self.assertEqual(Operator(qv64_1), Operator(qv64_2))
@data(1, 2, 3)
def test_coupling_map_transpile(self, opt):
"""test natural_direction works with transpile"""
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [0, 1])
circ_01 = transpile(
circ, basis_gates=["rz", "sx", "cx"], optimization_level=opt, coupling_map=[[0, 1]]
)
circ_10 = transpile(
circ, basis_gates=["rz", "sx", "cx"], optimization_level=opt, coupling_map=[[1, 0]]
)
circ_01_index = {qubit: index for index, qubit in enumerate(circ_01.qubits)}
circ_10_index = {qubit: index for index, qubit in enumerate(circ_10.qubits)}
self.assertTrue(
all(
(
(1, 0) == (circ_10_index[instr.qubits[0]], circ_10_index[instr.qubits[1]])
for instr in circ_10.get_instructions("cx")
)
)
)
self.assertTrue(
all(
(
(0, 1) == (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]])
for instr in circ_01.get_instructions("cx")
)
)
)
def test_if_simple(self):
"""Test a simple if statement."""
basis_gates = {"u", "cx"}
qr = QuantumRegister(2)
cr = ClassicalRegister(2)
qc_uni = QuantumCircuit(2)
qc_uni.h(0)
qc_uni.cx(0, 1)
qc_uni_mat = Operator(qc_uni)
qc_true_body = QuantumCircuit(2)
qc_true_body.unitary(qc_uni_mat, [0, 1])
qc = QuantumCircuit(qr, cr)
qc.if_test((cr, 1), qc_true_body, [0, 1], [])
dag = circuit_to_dag(qc)
cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag)
cqc = dag_to_circuit(cdag)
cbody = cqc.data[0].operation.params[0]
self.assertEqual(cbody.count_ops().keys(), basis_gates)
self.assertEqual(qc_uni_mat, Operator(cbody))
def test_nested_control_flow(self):
"""Test unrolling nested control flow blocks."""
qr = QuantumRegister(2)
cr = ClassicalRegister(1)
qc_uni1 = QuantumCircuit(2)
qc_uni1.swap(0, 1)
qc_uni1_mat = Operator(qc_uni1)
qc = QuantumCircuit(qr, cr)
with qc.for_loop(range(3)):
with qc.while_loop((cr, 0)):
qc.unitary(qc_uni1_mat, [0, 1])
dag = circuit_to_dag(qc)
cdag = UnitarySynthesis(basis_gates=["u", "cx"]).run(dag)
cqc = dag_to_circuit(cdag)
cbody = cqc.data[0].operation.params[2].data[0].operation.params[0]
self.assertEqual(cbody.count_ops().keys(), {"u", "cx"})
self.assertEqual(qc_uni1_mat, Operator(cbody))
def test_default_does_not_fail_on_no_syntheses(self):
qc = QuantumCircuit(1)
qc.unitary(np.eye(2), [0])
pass_ = UnitarySynthesis(["unknown", "gates"])
self.assertEqual(qc, pass_(qc))
@ddt
class TestUnitarySynthesisTarget(QiskitTestCase):
"""Test UnitarySynthesis pass with target/BackendV2."""
@combine(
opt_level=[0, 1, 2, 3],
bidirectional=[True, False],
dsc=(
"test natural_direction works with transpile using opt_level {opt_level} on"
" target with multiple 2q gates with bidirectional={bidirectional}"
),
name="opt_level_{opt_level}_bidirectional_{bidirectional}",
)
def test_coupling_map_transpile_with_backendv2(self, opt_level, bidirectional):
backend = FakeBackend5QV2(bidirectional)
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [0, 1])
circ_01 = transpile(
circ, backend=backend, optimization_level=opt_level, layout_method="trivial"
)
circ_01_index = {qubit: index for index, qubit in enumerate(circ_01.qubits)}
self.assertGreaterEqual(len(circ_01.get_instructions("cx")), 1)
for instr in circ_01.get_instructions("cx"):
self.assertEqual(
(0, 1), (circ_01_index[instr.qubits[0]], circ_01_index[instr.qubits[1]])
)
@combine(
opt_level=[0, 1, 2, 3],
bidirectional=[True, False],
dsc=(
"Test direction with transpile using opt_level {opt_level} on"
" target with multiple 2q gates with bidirectional={bidirectional}"
"direction [0, 1] is lower error and should be picked."
),
name="opt_level_{opt_level}_bidirectional_{bidirectional}",
)
def test_coupling_unequal_duration_with_backendv2(self, opt_level, bidirectional):
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [1, 0])
backend = FakeBackend5QV2(bidirectional)
tqc = transpile(
circ,
backend=backend,
optimization_level=opt_level,
translation_method="synthesis",
layout_method="trivial",
)
tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
self.assertGreaterEqual(len(tqc.get_instructions("cx")), 1)
for instr in tqc.get_instructions("cx"):
self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))
@combine(
opt_level=[0, 1, 2, 3],
dsc=(
"Test direction with transpile using opt_level {opt_level} on"
" target with multiple 2q gates"
),
name="opt_level_{opt_level}",
)
def test_non_overlapping_kak_gates_with_backendv2(self, opt_level):
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [1, 0])
backend = FakeBackend2QV2()
tqc = transpile(
circ,
backend=backend,
optimization_level=opt_level,
translation_method="synthesis",
layout_method="trivial",
)
tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
self.assertGreaterEqual(len(tqc.get_instructions("ecr")), 1)
for instr in tqc.get_instructions("ecr"):
self.assertEqual((1, 0), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))
def test_fractional_cx_with_backendv2(self):
"""Test fractional CX gets used if present in target."""
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [0, 1])
backend = FakeMumbaiFractionalCX()
synth_pass = UnitarySynthesis(target=backend.target)
tqc = synth_pass(circ)
tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
self.assertGreaterEqual(len(tqc.get_instructions("rzx")), 1)
for instr in tqc.get_instructions("rzx"):
self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))
@combine(
opt_level=[0, 1, 2, 3],
dsc=(
"Test direction with transpile using opt_level {opt_level} on"
"target with multiple 2q gates available in reverse direction"
),
name="opt_level_{opt_level}",
)
def test_reverse_direction(self, opt_level):
target = Target(2)
target.add_instruction(CXGate(), {(0, 1): InstructionProperties(error=1.2e-6)})
target.add_instruction(ECRGate(), {(0, 1): InstructionProperties(error=1.2e-7)})
target.add_instruction(
UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")), {(0,): None, (1,): None}
)
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [1, 0])
tqc = transpile(
circ,
target=target,
optimization_level=opt_level,
translation_method="synthesis",
layout_method="trivial",
)
tqc_index = {qubit: index for index, qubit in enumerate(tqc.qubits)}
self.assertGreaterEqual(len(tqc.get_instructions("ecr")), 1)
for instr in tqc.get_instructions("ecr"):
self.assertEqual((0, 1), (tqc_index[instr.qubits[0]], tqc_index[instr.qubits[1]]))
@combine(
opt_level=[0, 1, 2, 3],
dsc=("Test controlled but not supercontrolled basis"),
name="opt_level_{opt_level}",
)
def test_controlled_basis(self, opt_level):
target = Target(2)
target.add_instruction(RYYGate(np.pi / 8), {(0, 1): InstructionProperties(error=1.2e-6)})
target.add_instruction(
UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")), {(0,): None, (1,): None}
)
qr = QuantumRegister(2)
circ = QuantumCircuit(qr)
circ.append(random_unitary(4, seed=1), [1, 0])
tqc = transpile(
circ,
target=target,
optimization_level=opt_level,
translation_method="synthesis",
layout_method="trivial",
)
self.assertGreaterEqual(len(tqc.get_instructions("ryy")), 1)
self.assertEqual(Operator(tqc), Operator(circ))
def test_approximation_controlled(self):
target = Target(2)
target.add_instruction(RZZGate(np.pi / 10), {(0, 1): InstructionProperties(error=0.006)})
target.add_instruction(RXXGate(np.pi / 3), {(0, 1): InstructionProperties(error=0.01)})
target.add_instruction(
UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")),
{(0,): InstructionProperties(error=0.001), (1,): InstructionProperties(error=0.002)},
)
circ = QuantumCircuit(2)
circ.append(random_unitary(4, seed=7), [1, 0])
dag = circuit_to_dag(circ)
dag_100 = UnitarySynthesis(target=target, approximation_degree=1.0).run(dag)
dag_99 = UnitarySynthesis(target=target, approximation_degree=0.99).run(dag)
self.assertGreaterEqual(dag_100.depth(), dag_99.depth())
self.assertEqual(Operator(dag_to_circuit(dag_100)), Operator(circ))
def test_mapping_control_flow(self):
"""Test that inner dags use proper qubit mapping."""
qr = QuantumRegister(3, "q")
qc = QuantumCircuit(qr)
# Create target that supports CX only between 0 and 2.
fake_target = Target()
fake_target.add_instruction(CXGate(), {(0, 2): None})
fake_target.add_instruction(
UGate(Parameter("t"), Parameter("p"), Parameter("l")),
{
(0,): None,
(1,): None,
(2,): None,
},
)
qc_uni1 = QuantumCircuit(2)
qc_uni1.swap(0, 1)
qc_uni1_mat = Operator(qc_uni1)
loop_body = QuantumCircuit(2)
loop_body.unitary(qc_uni1_mat, [0, 1])
# Loop body uses qubits 0 and 2, mapped to 0 and 1 in the block.
# If synthesis doesn't handle recursive mapping, it'll incorrectly
# look for a CX on (0, 1) instead of on (0, 2).
qc.for_loop((0,), None, loop_body, [0, 2], [])
dag = circuit_to_dag(qc)
UnitarySynthesis(basis_gates=["u", "cx"], target=fake_target).run(dag)
def test_single_qubit_with_target(self):
"""Test input circuit with only 1q works with target."""
qc = QuantumCircuit(1)
qc.append(ZGate(), [qc.qubits[0]])
dag = circuit_to_dag(qc)
backend = GenericBackendV2(num_qubits=5, seed=42)
unitary_synth_pass = UnitarySynthesis(target=backend.target)
result_dag = unitary_synth_pass.run(dag)
result_qc = dag_to_circuit(result_dag)
self.assertEqual(qc, result_qc)
def test_single_qubit_identity_with_target(self):
"""Test input single qubit identity works with target."""
qc = QuantumCircuit(1)
qc.unitary([[1.0, 0.0], [0.0, 1.0]], 0)
dag = circuit_to_dag(qc)
backend = GenericBackendV2(num_qubits=5)
unitary_synth_pass = UnitarySynthesis(target=backend.target)
result_dag = unitary_synth_pass.run(dag)
result_qc = dag_to_circuit(result_dag)
self.assertEqual(result_qc, QuantumCircuit(1))
def test_unitary_synthesis_with_ideal_and_variable_width_ops(self):
"""Test unitary synthesis works with a target that contains ideal and variadic ops."""
qc = QuantumCircuit(2)
qc.unitary(np.eye(4), [0, 1])
dag = circuit_to_dag(qc)
target = GenericBackendV2(num_qubits=5).target
target.add_instruction(IfElseOp, name="if_else")
target.add_instruction(ZGate())
target.add_instruction(ECRGate())
unitary_synth_pass = UnitarySynthesis(target=target)
result_dag = unitary_synth_pass.run(dag)
result_qc = dag_to_circuit(result_dag)
self.assertEqual(result_qc, QuantumCircuit(2))
def test_unitary_synthesis_custom_gate_target(self):
qc = QuantumCircuit(2)
qc.unitary(np.eye(4), [0, 1])
dag = circuit_to_dag(qc)
class CustomGate(Gate):
"""Custom Opaque Gate"""
def __init__(self):
super().__init__("custom", 2, [])
target = Target(num_qubits=2)
target.add_instruction(
UGate(Parameter("t"), Parameter("p"), Parameter("l")), {(0,): None, (1,): None}
)
target.add_instruction(CustomGate(), {(0, 1): None, (1, 0): None})
unitary_synth_pass = UnitarySynthesis(target=target)
result_dag = unitary_synth_pass.run(dag)
result_qc = dag_to_circuit(result_dag)
self.assertEqual(result_qc, qc)
def test_iswap_no_cx_synthesis_succeeds(self):
"""Test basis set with iswap but no cx can synthesize a circuit"""
target = Target()
theta = Parameter("theta")
i_props = {
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
}
target.add_instruction(IGate(), i_props)
rz_props = {
(0,): InstructionProperties(duration=0, error=0),
(1,): InstructionProperties(duration=0, error=0),
}
target.add_instruction(RZGate(theta), rz_props)
sx_props = {
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
}
target.add_instruction(SXGate(), sx_props)
x_props = {
(0,): InstructionProperties(duration=35.5e-9, error=0.000413),
(1,): InstructionProperties(duration=35.5e-9, error=0.000502),
}
target.add_instruction(XGate(), x_props)
iswap_props = {
(0, 1): InstructionProperties(duration=519.11e-9, error=0.01201),
(1, 0): InstructionProperties(duration=554.66e-9, error=0.01201),
}
target.add_instruction(iSwapGate(), iswap_props)
measure_props = {
(0,): InstructionProperties(duration=5.813e-6, error=0.0751),
(1,): InstructionProperties(duration=5.813e-6, error=0.0225),
}
target.add_instruction(Measure(), measure_props)
qc = QuantumCircuit(2)
cxmat = Operator(CXGate()).to_matrix()
qc.unitary(cxmat, [0, 1])
unitary_synth_pass = UnitarySynthesis(target=target)
dag = circuit_to_dag(qc)
result_dag = unitary_synth_pass.run(dag)
result_qc = dag_to_circuit(result_dag)
self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat))
@combine(is_random=[True, False], param_gate=[RXXGate, RZZGate, CPhaseGate])
def test_parameterized_basis_gate_in_target(self, is_random, param_gate):
"""Test synthesis with parameterized RZZ/RXX gate."""
theta = Parameter("θ")
lam = Parameter("λ")
phi = Parameter("ϕ")
target = Target(num_qubits=2)
target.add_instruction(RZGate(lam))
target.add_instruction(RXGate(phi))
target.add_instruction(param_gate(theta))
qc = QuantumCircuit(2)
if is_random:
qc.unitary(random_unitary(4, seed=1234), [0, 1])
qc.cp(np.pi / 2, 0, 1)
qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42)
opcount = qc_transpiled.count_ops()
self.assertTrue(set(opcount).issubset({"rz", "rx", param_gate(theta).name}))
self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))
def test_custom_parameterized_gate_in_target(self):
"""Test synthesis with custom parameterized gate in target."""
class CustomXXGate(RXXGate):
"""Custom RXXGate subclass that's not a standard gate"""
_standard_gate = None
def __init__(self, theta, label=None):
super().__init__(theta, label)
self.name = "MyCustomXXGate"
theta = Parameter("θ")
lam = Parameter("λ")
phi = Parameter("ϕ")
target = Target(num_qubits=2)
target.add_instruction(RZGate(lam))
target.add_instruction(RXGate(phi))
target.add_instruction(CustomXXGate(theta))
qc = QuantumCircuit(2)
qc.unitary(random_unitary(4, seed=1234), [0, 1])
qc_transpiled = UnitarySynthesis(target=target)(qc)
opcount = qc_transpiled.count_ops()
self.assertTrue(set(opcount).issubset({"rz", "rx", "MyCustomXXGate"}))
self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))
def test_custom_parameterized_gate_in_target_skips(self):
"""Test that synthesis is skipped with custom parameterized
gate in target that is not RXX equivalent."""
class CustomXYGate(Gate):
"""Custom Gate subclass that's not a standard gate and not RXX equivalent"""
_standard_gate = None
def __init__(self, theta: ParameterValueType, label=None):
"""Create new custom rotstion XY gate."""
super().__init__("MyCustomXYGate", 2, [theta])
def __array__(self, dtype=None):
"""Return a Numpy.array for the custom gate."""
theta = self.params[0]
cos = math.cos(theta)
isin = 1j * math.sin(theta)
return np.array(
[[1, 0, 0, 0], [0, cos, -isin, 0], [0, -isin, cos, 0], [0, 0, 0, 1]],
dtype=dtype,
)
def inverse(self, annotated: bool = False):
return CustomXYGate(-self.params[0])
theta = Parameter("θ")
lam = Parameter("λ")
phi = Parameter("ϕ")
target = Target(num_qubits=2)
target.add_instruction(RZGate(lam))
target.add_instruction(RXGate(phi))
target.add_instruction(CustomXYGate(theta))
qc = QuantumCircuit(2)
qc.unitary(random_unitary(4, seed=1234), [0, 1])
qc_transpiled = UnitarySynthesis(target=target)(qc)
opcount = qc_transpiled.count_ops()
self.assertTrue(set(opcount).issubset({"unitary"}))
self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))
@data(
["rx", "ry", "rxx"],
["rx", "rz", "rzz"],
)
def test_parameterized_backend(self, basis_gates):
"""Test synthesis with parameterized backend."""
backend = GenericBackendV2(3, basis_gates=basis_gates, seed=0)
qc = QuantumCircuit(3)
qc.unitary(random_unitary(4, seed=1234), [0, 1])
qc.unitary(random_unitary(4, seed=4321), [0, 2])
qc.cp(np.pi / 2, 0, 1)
qc_transpiled = transpile(qc, backend, optimization_level=3, seed_transpiler=42)
opcount = qc_transpiled.count_ops()
self.assertTrue(set(opcount).issubset(basis_gates))
self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc)))
@data(1, 2, 3)
def test_qsd(self, opt):
"""Test that the unitary synthesis pass runs qsd successfully with a target."""
num_qubits = 3
target = Target(num_qubits=num_qubits)
target.add_instruction(UGate(Parameter("theta"), Parameter("phi"), Parameter("lam")))
target.add_instruction(CXGate())
mat = scipy.stats.ortho_group.rvs(2**num_qubits)
qc = qs_decomposition(mat, opt_a1=True, opt_a2=False)
qc_transpiled = transpile(qc, target=target, optimization_level=opt)
self.assertTrue(np.allclose(mat, Operator(qc_transpiled).data))
def test_3q_with_measure(self):
"""Test 3-qubit synthesis with measurements."""
backend = FakeBackend5QV2()
qc = QuantumCircuit(3, 1)
qc.unitary(np.eye(2**3), range(3))
qc.measure(0, 0)
qc_transpiled = transpile(qc, backend)
self.assertTrue(qc_transpiled.size, 1)
self.assertTrue(qc_transpiled.count_ops().get("measure", 0), 1)
def test_3q_series(self):
"""Test a series of 3-qubit blocks."""
backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1)
x = QuantumCircuit(3)
x.x(2)
x_mat = Operator(x)
qc = QuantumCircuit(3)
qc.unitary(x_mat, range(3))
qc.unitary(np.eye(2**3), range(3))
tqc = transpile(qc, backend, optimization_level=0, initial_layout=[0, 1, 2])
expected = np.kron(np.eye(2**2), x_mat)
self.assertEqual(Operator(tqc), Operator(expected))
def test_3q_measure_all(self):
"""Regression test of #13586."""
hamiltonian = SparsePauliOp.from_list(
[("IXX", 1), ("IYY", 1), ("IZZ", 1), ("XXI", 1), ("YYI", 1), ("ZZI", 1)]
)
qc = QuantumCircuit(3)
qc.x([1, 2])
op = PauliEvolutionGate(hamiltonian, time=1)
qc.append(op.power(8), [0, 1, 2])
qc.measure_all()
backend = GenericBackendV2(5, basis_gates=["u", "cx"], seed=1)
tqc = transpile(qc, backend)
ops = tqc.count_ops()
self.assertIn("u", ops)
self.assertIn("cx", ops)
self.assertIn("measure", ops)
def test_target_with_global_gates(self):
"""Test that 2q decomposition can handle a target with global gates."""
basis_gates = ["h", "p", "cp", "rz", "cx", "ccx", "swap"]
target = Target.from_configuration(basis_gates=basis_gates)
bell = QuantumCircuit(2)
bell.h(0)
bell.cx(0, 1)
bell_op = Operator(bell)
qc = QuantumCircuit(2)
qc.unitary(bell_op, [0, 1])
tqc = transpile(qc, target=target)
self.assertTrue(set(tqc.count_ops()).issubset(basis_gates))
def test_determinism(self):
"""Test that the decomposition is deterministic."""
gate_counts = {"rx": 6, "rz": 12, "iswap": 2}
basis_gates = ["rx", "rz", "iswap"]
target = Target.from_configuration(basis_gates=basis_gates)
pm = generate_preset_pass_manager(target=target, optimization_level=2, seed_transpiler=42)
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
for _ in range(10):
out = pm.run(qc)
self.assertTrue(Operator(out).equiv(qc))
self.assertTrue(set(out.count_ops()).issubset(basis_gates))
for basis_gate in basis_gates:
self.assertLessEqual(out.count_ops()[basis_gate], gate_counts[basis_gate])
@combine(gate=["unitary", "swap"], natural_direction=[True, False])
def test_two_qubit_synthesis_to_directional_cx_target(self, gate, natural_direction):
"""Verify two qubit unitaries are synthesized to match basis gates."""
# TODO: should make check more explicit e.g. explicitly set gate
# direction in test instead of using specific fake backend
backend = GenericBackendV2(
num_qubits=5,
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=YORKTOWN_CMAP,
seed=1,
)
coupling_map = CouplingMap(backend.coupling_map)
triv_layout_pass = TrivialLayout(coupling_map)
qr = QuantumRegister(2)
qc = QuantumCircuit(qr)
if gate == "unitary":
qc.unitary(random_unitary(4, seed=12), [0, 1])
elif gate == "swap":
qc.swap(qr[0], qr[1])
unisynth_pass = UnitarySynthesis(
target=backend.target,
pulse_optimize=True,
natural_direction=natural_direction,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
qc_out = pm.run(qc)
self.assertEqual(Operator(qc), Operator(qc_out))
@data(True, False)
def test_two_qubit_synthesis_to_directional_cx_multiple_registers_target(
self, natural_direction
):
"""Verify two qubit unitaries are synthesized to match basis gates
across multiple registers."""
# TODO: should make check more explicit e.g. explicitly set gate
# direction in test instead of using specific fake backend
backend = GenericBackendV2(
num_qubits=5,
basis_gates=["id", "rz", "sx", "x", "cx", "reset"],
coupling_map=YORKTOWN_CMAP,
seed=1,
)
qr0 = QuantumRegister(1)
qr1 = QuantumRegister(1)
coupling_map = CouplingMap(backend.coupling_map)
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr0, qr1)
qc.unitary(random_unitary(4, seed=12), [qr0[0], qr1[0]])
unisynth_pass = UnitarySynthesis(
target=backend.target,
pulse_optimize=True,
natural_direction=natural_direction,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
qc_out = pm.run(qc)
self.assertEqual(Operator(qc), Operator(qc_out))
def test_two_qubit_natural_direction_true_duration_fallback_target(self):
"""Verify fallback path when pulse_optimize==True."""
basis_gates = ["id", "rz", "sx", "x", "cx", "reset"]
qr = QuantumRegister(2)
coupling_map = CouplingMap([[0, 1], [1, 0], [1, 2], [1, 3], [3, 4]])
backend = GenericBackendV2(
num_qubits=5, basis_gates=basis_gates, coupling_map=coupling_map, seed=1
)
triv_layout_pass = TrivialLayout(coupling_map)
qc = QuantumCircuit(qr)
qc.unitary(random_unitary(4, seed=12), [0, 1])
unisynth_pass = UnitarySynthesis(
target=backend.target,
pulse_optimize=True,
natural_direction=True,
)
pm = PassManager([triv_layout_pass, unisynth_pass])
qc_out = pm.run(qc)
self.assertTrue(
all(((qr[0], qr[1]) == instr.qubits for instr in qc_out.get_instructions("cx")))
)
if __name__ == "__main__":
unittest.main()