diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 7a293131e3f2..bdf213e6539a 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -54,6 +54,8 @@ Unroller Unroll3qOrMore Decompose + UnrollCustomDefinitions + BasisTranslator Optimizations ============= @@ -121,7 +123,9 @@ # basis change from .basis import Decompose from .basis import Unroller +from .basis import UnrollCustomDefinitions from .basis import Unroll3qOrMore +from .basis import BasisTranslator # optimization from .optimization import Optimize1qGates diff --git a/qiskit/transpiler/passes/basis/__init__.py b/qiskit/transpiler/passes/basis/__init__.py index 1c5fbbbc02a7..692e50d56188 100644 --- a/qiskit/transpiler/passes/basis/__init__.py +++ b/qiskit/transpiler/passes/basis/__init__.py @@ -16,4 +16,6 @@ from .decompose import Decompose from .unroller import Unroller +from .unroll_custom_definitions import UnrollCustomDefinitions from .unroll_3q_or_more import Unroll3qOrMore +from .basis_translator import BasisTranslator diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py new file mode 100644 index 000000000000..b7112e2f01a4 --- /dev/null +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""Translates gates to a target basis using a given equivalence library.""" + +import time +import logging + +from heapq import heappush, heappop +from itertools import zip_longest +from itertools import count as iter_count +from collections import defaultdict + +import numpy as np + +from qiskit.circuit import Gate, ParameterVector, QuantumRegister +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.exceptions import TranspilerError + + +logger = logging.getLogger(__name__) + + +class BasisTranslator(TransformationPass): + """Translates gates to a target basis by searching for a set of translations + from a given EquivalenceLibrary. + + This pass operates in several steps: + + * Determine the source basis from the input circuit. + * Perform an A* search over basis sets, starting from the source basis and + targeting the device's target_basis, with edges discovered from the + provided EquivalenceLibrary. The heuristic used by the A* search is the + number of distinct circuit basis gates not in the target_basis, plus the + number of distinct device basis gates not used in the current basis. + * The found path, as a set of rules from the EquivalenceLibrary, is composed + into a set of gate replacement rules. + * The composed replacement rules are applied in-place to each op node which + is not already in the target_basis. + + """ + + def __init__(self, equivalence_library, target_basis): + """Initialize a BasisTranslator instance. + + Args: + equivalence_library (EquivalenceLibrary): The equivalence library + which will be used by the BasisTranslator pass. (Instructions in + this library will not be unrolled by this pass.) + target_basis (list[str]): Target basis names to unroll to, e.g. `['u3', 'cx']`. + """ + + super().__init__() + + self._equiv_lib = equivalence_library + self._target_basis = target_basis + + def run(self, dag): + """Translate an input DAGCircuit to the target basis. + + Args: + dag (DAGCircuit): input dag + + Raises: + TranspilerError: if the target basis cannot be reached + + Returns: + DAGCircuit: translated circuit. + """ + + if self._target_basis is None: + return dag + + # Names of instructions assumed to supported by any backend. + basic_instrs = ['measure', 'reset', 'barrier', 'snapshot'] + + target_basis = set(self._target_basis).union(basic_instrs) + source_basis = set((node.op.name, node.op.num_qubits) + for node in dag.op_nodes()) + + logger.info('Begin BasisTranslator from source basis %s to target ' + 'basis %s.', source_basis, target_basis) + + # Search for a path from source to target basis. + + search_start_time = time.time() + basis_transforms = _basis_search(self._equiv_lib, source_basis, + target_basis, _basis_heuristic) + search_end_time = time.time() + logger.info('Basis translation path search completed in %.3fs.', + search_end_time - search_start_time) + + if basis_transforms is None: + raise TranspilerError( + 'Unable to map source basis {} to target basis {} ' + 'over library {}.'.format( + source_basis, target_basis, self._equiv_lib)) + + # Compose found path into a set of instruction substitution rules. + + compose_start_time = time.time() + instr_map = _compose_transforms(basis_transforms, source_basis, dag) + + compose_end_time = time.time() + logger.info('Basis translation paths composed in %.3fs.', + compose_end_time - compose_start_time) + + # Replace source instructions with target translations. + + replace_start_time = time.time() + for node in dag.op_nodes(): + if node.name in target_basis: + continue + + if (node.op.name, node.op.num_qubits) in instr_map: + target_params, target_dag = instr_map[node.op.name, node.op.num_qubits] + + if len(node.op.params) != len(target_params): + raise TranspilerError( + 'Translation num_params not equal to op num_params.' + 'Op: {} {} Translation: {}\n{}'.format( + node.op.params, node.op.name, + target_params, target_dag)) + + # Convert target to circ and back to assign_parameters, since + # DAGCircuits won't have a ParameterTable. + from qiskit.converters import dag_to_circuit, circuit_to_dag + target_circuit = dag_to_circuit(target_dag) + + target_circuit.assign_parameters( + dict(zip_longest(target_params, node.op.params)), + inplace=True) + + bound_target_dag = circuit_to_dag(target_circuit) + + if (len(bound_target_dag.op_nodes()) == 1 + and len(bound_target_dag.op_nodes()[0].qargs) == len(node.qargs)): + dag.substitute_node(node, bound_target_dag.op_nodes()[0].op, inplace=True) + else: + dag.substitute_node_with_dag(node, bound_target_dag) + else: + raise TranspilerError('BasisTranslator did not map {}.'.format(node.name)) + + replace_end_time = time.time() + logger.info('Basis translation instructions replaced in %.3fs.', + replace_end_time - replace_start_time) + + return dag + + +def _basis_heuristic(basis, target): + """Simple metric to gauge distance between two bases as the number of + elements in the symmetric difference of the circuit basis and the device + basis. + """ + return len(set(gate_name for gate_name, gate_num_qubits in basis) ^ target) + + +def _basis_search(equiv_lib, source_basis, target_basis, heuristic): + """Search for a set of transformations from source_basis to target_basis. + + Args: + equiv_lib (EquivalenceLibrary): Source of valid translations + source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Starting basis. + target_basis (Set[gate_name: str]): Target basis. + heuristic (Callable[[source_basis, target_basis], int]): distance heuristic. + + Returns: + Optional[List[Tuple[gate, equiv_params, equiv_circuit]]]: List of (gate, + equiv_params, equiv_circuit) tuples tuples which, if applied in order + will map from source_basis to target_basis. Returns None if no path + was found. + """ + + source_basis = frozenset(source_basis) + target_basis = frozenset(target_basis) + + open_set = set() # Bases found but not yet inspected. + closed_set = set() # Bases found and inspected. + + # Priority queue for inspection order of open_set. Contains Tuple[priority, count, basis] + open_heap = [] + + # Map from bases in closed_set to predecessor with lowest cost_from_source. + # Values are Tuple[prev_basis, gate_name, params, circuit]. + came_from = {} + + basis_count = iter_count() # Used to break ties in priority. + + open_set.add(source_basis) + heappush(open_heap, (0, next(basis_count), source_basis)) + + # Map from basis to lowest found cost from source. + cost_from_source = defaultdict(lambda: np.inf) + cost_from_source[source_basis] = 0 + + # Map from basis to cost_from_source + heuristic. + est_total_cost = defaultdict(lambda: np.inf) + est_total_cost[source_basis] = heuristic(source_basis, target_basis) + + logger.debug('Begining basis search from %s to %s.', + source_basis, target_basis) + + while open_set: + _, _, current_basis = heappop(open_heap) + + if current_basis in closed_set: + # When we close a node, we don't remove it from the heap, + # so skip here. + continue + + if set(gate_name for gate_name, gate_num_qubits in current_basis).issubset(target_basis): + # Found target basis. Construct transform path. + rtn = [] + last_basis = current_basis + while last_basis != source_basis: + prev_basis, gate_name, gate_num_qubits, params, equiv = came_from[last_basis] + + rtn.append((gate_name, gate_num_qubits, params, equiv)) + last_basis = prev_basis + rtn.reverse() + + logger.debug('Transformation path:') + for gate_name, gate_num_qubits, params, equiv in rtn: + logger.debug('%s/%s => %s\n%s', gate_name, gate_num_qubits, params, equiv) + return rtn + + logger.debug('Inspecting basis %s.', current_basis) + open_set.remove(current_basis) + closed_set.add(current_basis) + + for gate_name, gate_num_qubits in current_basis: + equivs = equiv_lib._get_equivalences((gate_name, gate_num_qubits)) + + basis_remain = current_basis - {(gate_name, gate_num_qubits)} + neighbors = [ + (frozenset(basis_remain | set((inst.name, inst.num_qubits) + for inst, qargs, cargs in equiv.data)), + params, + equiv) + for params, equiv in equivs] + + # Weight total path length of transformation weakly. + tentative_cost_from_source = cost_from_source[current_basis] + 1e-3 + + for neighbor, params, equiv in neighbors: + if neighbor in closed_set: + continue + + if tentative_cost_from_source >= cost_from_source[neighbor]: + continue + + open_set.add(neighbor) + came_from[neighbor] = (current_basis, gate_name, gate_num_qubits, params, equiv) + cost_from_source[neighbor] = tentative_cost_from_source + est_total_cost[neighbor] = tentative_cost_from_source \ + + heuristic(neighbor, target_basis) + heappush(open_heap, (est_total_cost[neighbor], + next(basis_count), + neighbor)) + + return None + + +def _compose_transforms(basis_transforms, source_basis, source_dag): + """Compose a set of basis transforms into a set of replacements. + + Args: + basis_transforms (List[Tuple[gate_name, params, equiv]]): List of + transforms to compose. + source_basis (Set[Tuple[gate_name: str, gate_num_qubits: int]]): Names + of gates which need to be translated. + source_dag (DAGCircuit): DAG with example gates from source_basis. + (Used to determine num_params for gate in source_basis.) + + Returns: + Dict[gate_name, Tuple(params, dag)]: Dictionary mapping between each gate + in source_basis and a DAGCircuit instance to replace it. Gates in + source_basis but not affected by basis_transforms will be included + as a key mapping to itself. + """ + + example_gates = {(node.op.name, node.op.num_qubits): node.op + for node in source_dag.op_nodes()} + mapped_instrs = {} + + for gate_name, gate_num_qubits in source_basis: + # Need to grab a gate instance to find num_qubits and num_params. + # Can be removed following https://github.com/Qiskit/qiskit-terra/pull/3947 . + example_gate = example_gates[gate_name, gate_num_qubits] + num_params = len(example_gate.params) + + placeholder_params = ParameterVector(gate_name, num_params) + placeholder_gate = Gate(gate_name, gate_num_qubits, list(placeholder_params)) + placeholder_gate.params = list(placeholder_params) + + dag = DAGCircuit() + qr = QuantumRegister(gate_num_qubits) + dag.add_qreg(qr) + dag.apply_operation_back(placeholder_gate, qr[:], []) + mapped_instrs[gate_name, gate_num_qubits] = placeholder_params, dag + + for gate_name, gate_num_qubits, equiv_params, equiv in basis_transforms: + logger.debug('Composing transform step: %s/%s %s =>\n%s', + gate_name, gate_num_qubits, equiv_params, equiv) + + for mapped_instr_name, (dag_params, dag) in mapped_instrs.items(): + doomed_nodes = [node for node in dag.op_nodes() + if (node.op.name, node.op.num_qubits) == (gate_name, gate_num_qubits)] + + if doomed_nodes and logger.isEnabledFor(logging.DEBUG): + from qiskit.converters import dag_to_circuit + logger.debug('Updating transform for mapped instr %s %s from \n%s', + mapped_instr_name, dag_params, dag_to_circuit(dag)) + + for node in doomed_nodes: + from qiskit.converters import circuit_to_dag + + replacement = equiv.assign_parameters( + dict(zip_longest(equiv_params, node.op.params))) + + replacement_dag = circuit_to_dag(replacement) + + dag.substitute_node_with_dag(node, replacement_dag) + + if doomed_nodes and logger.isEnabledFor(logging.DEBUG): + from qiskit.converters import dag_to_circuit + logger.debug('Updated transform for mapped instr %s %s to\n%s', + mapped_instr_name, dag_params, dag_to_circuit(dag)) + + return mapped_instrs diff --git a/qiskit/transpiler/passes/basis/unroll_custom_definitions.py b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py new file mode 100644 index 000000000000..7adcf2624df8 --- /dev/null +++ b/qiskit/transpiler/passes/basis/unroll_custom_definitions.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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. + +"""Unrolls instructions with custom definitions.""" + +from qiskit.dagcircuit import DAGCircuit +from qiskit.exceptions import QiskitError +from qiskit.transpiler.basepasses import TransformationPass + + +class UnrollCustomDefinitions(TransformationPass): + """Unrolls instructions with custom definitions.""" + + def __init__(self, equivalence_library, basis_gates): + """Unrolls instructions with custom definitions. + + Args: + equivalence_library (EquivalenceLibrary): The equivalence library + which will be used by the BasisTranslator pass. (Instructions in + this library will not be unrolled by this pass.) + basis_gates (list[str]): Target basis names to unroll to, e.g. `['u3', 'cx']`. + """ + + super().__init__() + self._equiv_lib = equivalence_library + self._basis_gates = basis_gates + + def run(self, dag): + """Run the UnrollCustomDefinitions pass on `dag`. + + Args: + dag (DAGCircuit): input dag + + Raises: + QiskitError: if unable to unroll given the basis due to undefined + decomposition rules (such as a bad basis) or excessive recursion. + + Returns: + DAGCircuit: output unrolled dag + """ + + if self._basis_gates is None: + return dag + + basic_insts = set(('measure', 'reset', 'barrier', 'snapshot')) + device_insts = basic_insts | set(self._basis_gates) + + for node in dag.op_nodes(): + + if node.name in device_insts or self._equiv_lib.has_entry(node.op): + continue + + try: + rule = node.op.definition + except TypeError as err: + raise QiskitError('Error decomposing node {}: {}'.format(node.name, err)) + + if not rule: + if rule == []: + dag.remove_op_node(node) + continue + + # opaque node + raise QiskitError("Cannot unroll the circuit to the given basis, %s. " + "Instruction %s not found in equivalence library " + "and no rule found to expand." % + (str(self._basis_gates), node.op.name)) + + # hacky way to build a dag on the same register as the rule is defined + # TODO: need anonymous rules to address wires by index + decomposition = DAGCircuit() + qregs = {qb.register for inst in rule for qb in inst[1]} + cregs = {cb.register for inst in rule for cb in inst[2]} + for qreg in qregs: + decomposition.add_qreg(qreg) + for creg in cregs: + decomposition.add_creg(creg) + for inst in rule: + decomposition.apply_operation_back(*inst) + + unrolled_dag = UnrollCustomDefinitions(self._equiv_lib, + self._basis_gates).run( + decomposition) + dag.substitute_node_with_dag(node, unrolled_dag) + + return dag diff --git a/releasenotes/notes/add-basistranslator-pass-d5e7de69ab9f20a1.yaml b/releasenotes/notes/add-basistranslator-pass-d5e7de69ab9f20a1.yaml new file mode 100644 index 000000000000..9469f5b62455 --- /dev/null +++ b/releasenotes/notes/add-basistranslator-pass-d5e7de69ab9f20a1.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Two transpiler passes have been added to ``qiskit.transpiler.passes.basis``. + The first, ``UnrollCustomDefinitions``, unrolls all instructions in the + circuit according to their ``.definition`` property, stopping when reaching + either the specified ``basis_gates`` or a set of gates in the provided + ``EquivalenceLibrary``. The second, ``BasisTranslator``, uses the set of + translations in the provided ``EquivalenceLibrary`` to re-write circuit + instructions in a specified basis. diff --git a/test/python/transpiler/test_basis_translator.py b/test/python/transpiler/test_basis_translator.py new file mode 100644 index 000000000000..0fe547e1c9a6 --- /dev/null +++ b/test/python/transpiler/test_basis_translator.py @@ -0,0 +1,589 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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 the BasisTranslator pass""" + + +from numpy import pi + +from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit +from qiskit.test import QiskitTestCase +from qiskit.circuit import Gate, Parameter, EquivalenceLibrary +from qiskit.converters import circuit_to_dag, circuit_to_instruction +from qiskit.exceptions import QiskitError +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.basis import BasisTranslator, UnrollCustomDefinitions + + +from qiskit.circuit.library.standard_gates.equivalence_library \ + import StandardEquivalenceLibrary as std_eqlib + + +class OneQubitZeroParamGate(Gate): + """Mock one qubit zero param gate.""" + def __init__(self): + super().__init__('1q0p', 1, []) + + +class OneQubitOneParamGate(Gate): + """Mock one qubit one param gate.""" + def __init__(self, theta): + super().__init__('1q1p', 1, [theta]) + + +class OneQubitOneParamPrimeGate(Gate): + """Mock one qubit one param gate.""" + def __init__(self, alpha): + super().__init__('1q1p_prime', 1, [alpha]) + + +class OneQubitTwoParamGate(Gate): + """Mock one qubit two param gate.""" + def __init__(self, phi, lam): + super().__init__('1q2p', 1, [phi, lam]) + + +class TwoQubitZeroParamGate(Gate): + """Mock one qubit zero param gate.""" + def __init__(self): + super().__init__('2q0p', 2, []) + + +class VariadicZeroParamGate(Gate): + """Mock variadic zero param gate.""" + def __init__(self, num_qubits): + super().__init__('vq0p', num_qubits, []) + + +class TestBasisTranslator(QiskitTestCase): + """Test the BasisTranslator pass.""" + + def test_circ_in_basis_no_op(self): + """Verify we don't change a circuit already in the target basis.""" + eq_lib = EquivalenceLibrary() + qc = QuantumCircuit(1) + qc.append(OneQubitZeroParamGate(), [0]) + dag = circuit_to_dag(qc) + + expected = circuit_to_dag(qc) + + pass_ = BasisTranslator(eq_lib, ['1q0p']) + actual = pass_.run(dag) + + self.assertEqual(actual, expected) + + def test_raise_if_target_basis_unreachable(self): + """Verify we raise if the circuit cannot be transformed to the target.""" + eq_lib = EquivalenceLibrary() + + qc = QuantumCircuit(1) + qc.append(OneQubitZeroParamGate(), [0]) + dag = circuit_to_dag(qc) + + pass_ = BasisTranslator(eq_lib, ['1q1p']) + + with self.assertRaises(TranspilerError): + pass_.run(dag) + + def test_single_substitution(self): + """Verify we correctly unroll gates through a single equivalence.""" + eq_lib = EquivalenceLibrary() + + gate = OneQubitZeroParamGate() + equiv = QuantumCircuit(1) + equiv.append(OneQubitOneParamGate(pi), [0]) + + eq_lib.add_equivalence(gate, equiv) + + qc = QuantumCircuit(1) + qc.append(OneQubitZeroParamGate(), [0]) + dag = circuit_to_dag(qc) + + expected = QuantumCircuit(1) + expected.append(OneQubitOneParamGate(pi), [0]) + expected_dag = circuit_to_dag(expected) + + pass_ = BasisTranslator(eq_lib, ['1q1p']) + actual = pass_.run(dag) + + self.assertEqual(actual, expected_dag) + + def test_double_substitution(self): + """Verify we correctly unroll gates through multiple equivalences.""" + eq_lib = EquivalenceLibrary() + + gate = OneQubitZeroParamGate() + equiv = QuantumCircuit(1) + equiv.append(OneQubitOneParamGate(pi), [0]) + + eq_lib.add_equivalence(gate, equiv) + + theta = Parameter('theta') + gate = OneQubitOneParamGate(theta) + equiv = QuantumCircuit(1) + equiv.append(OneQubitTwoParamGate(theta, pi/2), [0]) + + eq_lib.add_equivalence(gate, equiv) + + qc = QuantumCircuit(1) + qc.append(OneQubitZeroParamGate(), [0]) + dag = circuit_to_dag(qc) + + expected = QuantumCircuit(1) + expected.append(OneQubitTwoParamGate(pi, pi/2), [0]) + expected_dag = circuit_to_dag(expected) + + pass_ = BasisTranslator(eq_lib, ['1q2p']) + actual = pass_.run(dag) + + self.assertEqual(actual, expected_dag) + + def test_multiple_variadic(self): + """Verify circuit with multiple instances of variadic gate.""" + eq_lib = EquivalenceLibrary() + + # e.g. MSGate + oneq_gate = VariadicZeroParamGate(1) + equiv = QuantumCircuit(1) + equiv.append(OneQubitZeroParamGate(), [0]) + eq_lib.add_equivalence(oneq_gate, equiv) + + twoq_gate = VariadicZeroParamGate(2) + equiv = QuantumCircuit(2) + equiv.append(TwoQubitZeroParamGate(), [0, 1]) + eq_lib.add_equivalence(twoq_gate, equiv) + + qc = QuantumCircuit(2) + qc.append(VariadicZeroParamGate(1), [0]) + qc.append(VariadicZeroParamGate(2), [0, 1]) + + dag = circuit_to_dag(qc) + + expected = QuantumCircuit(2) + expected.append(OneQubitZeroParamGate(), [0]) + expected.append(TwoQubitZeroParamGate(), [0, 1]) + + expected_dag = circuit_to_dag(expected) + + pass_ = BasisTranslator(eq_lib, ['1q0p', '2q0p']) + actual = pass_.run(dag) + + self.assertEqual(actual, expected_dag) + + def test_diamond_path(self): + """Verify we find a path when there are multiple paths to the target basis.""" + eq_lib = EquivalenceLibrary() + + # Path 1: 1q0p -> 1q1p(pi) -> 1q2p(pi, pi/2) + + gate = OneQubitZeroParamGate() + equiv = QuantumCircuit(1) + equiv.append(OneQubitOneParamGate(pi), [0]) + + eq_lib.add_equivalence(gate, equiv) + + theta = Parameter('theta') + gate = OneQubitOneParamGate(theta) + equiv = QuantumCircuit(1) + equiv.append(OneQubitTwoParamGate(theta, pi/2), [0]) + + eq_lib.add_equivalence(gate, equiv) + + # Path 2: 1q0p -> 1q1p_prime(pi/2) -> 1q2p(2 * pi/2, pi/2) + + gate = OneQubitZeroParamGate() + equiv = QuantumCircuit(1) + equiv.append(OneQubitOneParamPrimeGate(pi/2), [0]) + + eq_lib.add_equivalence(gate, equiv) + + alpha = Parameter('alpha') + gate = OneQubitOneParamPrimeGate(alpha) + equiv = QuantumCircuit(1) + equiv.append(OneQubitTwoParamGate(2 * alpha, pi/2), [0]) + + eq_lib.add_equivalence(gate, equiv) + + qc = QuantumCircuit(1) + qc.append(OneQubitZeroParamGate(), [0]) + dag = circuit_to_dag(qc) + + expected = QuantumCircuit(1) + expected.append(OneQubitTwoParamGate(pi, pi/2), [0]) + expected_dag = circuit_to_dag(expected) + + pass_ = BasisTranslator(eq_lib, ['1q2p']) + actual = pass_.run(dag) + + self.assertEqual(actual, expected_dag) + + +class TestUnrollerCompatability(QiskitTestCase): + """Tests backward compatability with the Unroller pass. + + Duplicate of TestUnroller from test.python.transpiler.test_unroller with + Unroller replaced by UnrollCustomDefinitions -> BasisTranslator. + """ + + def test_basic_unroll(self): + """Test decompose a single H into u2. + """ + qr = QuantumRegister(1, 'qr') + circuit = QuantumCircuit(qr) + circuit.h(qr[0]) + dag = circuit_to_dag(circuit) + pass_ = UnrollCustomDefinitions(std_eqlib, ['u2']) + dag = pass_.run(dag) + pass_ = BasisTranslator(std_eqlib, ['u2']) + unrolled_dag = pass_.run(dag) + op_nodes = unrolled_dag.op_nodes() + self.assertEqual(len(op_nodes), 1) + self.assertEqual(op_nodes[0].name, 'u2') + + def test_unroll_toffoli(self): + """Test unroll toffoli on multi regs to h, t, tdg, cx. + """ + qr1 = QuantumRegister(2, 'qr1') + qr2 = QuantumRegister(1, 'qr2') + circuit = QuantumCircuit(qr1, qr2) + circuit.ccx(qr1[0], qr1[1], qr2[0]) + dag = circuit_to_dag(circuit) + pass_ = UnrollCustomDefinitions(std_eqlib, ['h', 't', 'tdg', 'cx']) + dag = pass_.run(dag) + pass_ = BasisTranslator(std_eqlib, ['h', 't', 'tdg', 'cx']) + unrolled_dag = pass_.run(dag) + op_nodes = unrolled_dag.op_nodes() + self.assertEqual(len(op_nodes), 15) + for node in op_nodes: + self.assertIn(node.name, ['h', 't', 'tdg', 'cx']) + + def test_unroll_1q_chain_conditional(self): + """Test unroll chain of 1-qubit gates interrupted by conditional. + """ + qr = QuantumRegister(1, 'qr') + cr = ClassicalRegister(1, 'cr') + circuit = QuantumCircuit(qr, cr) + circuit.h(qr) + circuit.tdg(qr) + circuit.z(qr) + circuit.t(qr) + circuit.ry(0.5, qr) + circuit.rz(0.3, qr) + circuit.rx(0.1, qr) + circuit.measure(qr, cr) + circuit.x(qr).c_if(cr, 1) + circuit.y(qr).c_if(cr, 1) + circuit.z(qr).c_if(cr, 1) + dag = circuit_to_dag(circuit) + pass_ = UnrollCustomDefinitions(std_eqlib, ['u1', 'u2', 'u3']) + dag = pass_.run(dag) + + pass_ = BasisTranslator(std_eqlib, ['u1', 'u2', 'u3']) + unrolled_dag = pass_.run(dag) + + ref_circuit = QuantumCircuit(qr, cr) + ref_circuit.u2(0, pi, qr[0]) + ref_circuit.u1(-pi/4, qr[0]) + ref_circuit.u1(pi, qr[0]) + ref_circuit.u1(pi/4, qr[0]) + ref_circuit.u3(0.5, 0, 0, qr[0]) + ref_circuit.u1(0.3, qr[0]) + ref_circuit.u3(0.1, -pi/2, pi/2, qr[0]) + ref_circuit.measure(qr[0], cr[0]) + ref_circuit.u3(pi, 0, pi, qr[0]).c_if(cr, 1) + ref_circuit.u3(pi, pi/2, pi/2, qr[0]).c_if(cr, 1) + ref_circuit.u1(pi, qr[0]).c_if(cr, 1) + ref_dag = circuit_to_dag(ref_circuit) + self.assertEqual(unrolled_dag, ref_dag) + + def test_unroll_no_basis(self): + """Test when a given gate has no decompositions. + """ + qr = QuantumRegister(1, 'qr') + cr = ClassicalRegister(1, 'cr') + circuit = QuantumCircuit(qr, cr) + circuit.h(qr) + dag = circuit_to_dag(circuit) + pass_ = UnrollCustomDefinitions(std_eqlib, []) + dag = pass_.run(dag) + + pass_ = BasisTranslator(std_eqlib, []) + + with self.assertRaises(QiskitError): + pass_.run(dag) + + def test_unroll_all_instructions(self): + """Test unrolling a circuit containing all standard instructions. + """ + + qr = QuantumRegister(3, 'qr') + cr = ClassicalRegister(3, 'cr') + circuit = QuantumCircuit(qr, cr) + circuit.crx(0.5, qr[1], qr[2]) + circuit.cry(0.5, qr[1], qr[2]) + circuit.ccx(qr[0], qr[1], qr[2]) + circuit.ch(qr[0], qr[2]) + circuit.crz(0.5, qr[1], qr[2]) + circuit.cswap(qr[1], qr[0], qr[2]) + circuit.cu1(0.1, qr[0], qr[2]) + circuit.cu3(0.2, 0.1, 0.0, qr[1], qr[2]) + circuit.cx(qr[1], qr[0]) + circuit.cy(qr[1], qr[2]) + circuit.cz(qr[2], qr[0]) + circuit.h(qr[1]) + circuit.i(qr[0]) + circuit.rx(0.1, qr[0]) + circuit.ry(0.2, qr[1]) + circuit.rz(0.3, qr[2]) + circuit.rzz(0.6, qr[1], qr[0]) + circuit.s(qr[0]) + circuit.sdg(qr[1]) + circuit.swap(qr[1], qr[2]) + circuit.t(qr[2]) + circuit.tdg(qr[0]) + circuit.u1(0.1, qr[1]) + circuit.u2(0.2, -0.1, qr[0]) + circuit.u3(0.3, 0.0, -0.1, qr[2]) + circuit.x(qr[2]) + circuit.y(qr[1]) + circuit.z(qr[0]) + circuit.snapshot('0') + circuit.measure(qr, cr) + dag = circuit_to_dag(circuit) + pass_ = UnrollCustomDefinitions(std_eqlib, ['u3', 'cx', 'id']) + dag = pass_.run(dag) + + pass_ = BasisTranslator(std_eqlib, ['u3', 'cx', 'id']) + unrolled_dag = pass_.run(dag) + + ref_circuit = QuantumCircuit(qr, cr) + ref_circuit.u3(0, 0, pi/2, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(-0.25, 0, 0, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0.25, -pi/2, 0, qr[2]) + ref_circuit.u3(0.25, 0, 0, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(-0.25, 0, 0, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0, 0, -pi/4, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0, 0, pi/4, qr[1]) + ref_circuit.u3(0, 0, -pi/4, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.cx(qr[0], qr[1]) + ref_circuit.u3(0, 0, pi/4, qr[0]) + ref_circuit.u3(0, 0, -pi/4, qr[1]) + ref_circuit.cx(qr[0], qr[1]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.u3(0, 0, pi/2, qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, -pi/4, qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.u3(0, 0, -pi/2, qr[2]) + ref_circuit.u3(0, 0, 0.25, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0, 0, -0.25, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.cx(qr[2], qr[0]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, -pi/4, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, pi/4, qr[0]) + ref_circuit.u3(0, 0, -pi/4, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.cx(qr[1], qr[0]) + ref_circuit.u3(0, 0, -pi/4, qr[0]) + ref_circuit.u3(0, 0, pi/4, qr[1]) + ref_circuit.cx(qr[1], qr[0]) + ref_circuit.u3(0, 0, 0.05, qr[1]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[2]) + ref_circuit.cx(qr[2], qr[0]) + ref_circuit.u3(0, 0, 0.05, qr[0]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, -0.05, qr[2]) + ref_circuit.cx(qr[0], qr[2]) + ref_circuit.u3(0, 0, 0.05, qr[2]) + ref_circuit.u3(0, 0, -0.05, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(-0.1, 0, -0.05, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.cx(qr[1], qr[0]) + ref_circuit.u3(pi/2, 0, pi, qr[0]) + ref_circuit.u3(0.1, 0.1, 0, qr[2]) + ref_circuit.u3(0, 0, -pi/2, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(pi/2, 0, pi, qr[1]) + ref_circuit.u3(0.2, 0, 0, qr[1]) + ref_circuit.u3(0, 0, pi/2, qr[2]) + ref_circuit.cx(qr[2], qr[0]) + ref_circuit.u3(pi/2, 0, pi, qr[0]) + ref_circuit.i(qr[0]) + ref_circuit.u3(0.1, -pi/2, pi/2, qr[0]) + ref_circuit.cx(qr[1], qr[0]) + ref_circuit.u3(0, 0, 0.6, qr[0]) + ref_circuit.cx(qr[1], qr[0]) + ref_circuit.u3(0, 0, pi/2, qr[0]) + ref_circuit.u3(0, 0, -pi/4, qr[0]) + ref_circuit.u3(pi/2, 0.2, -0.1, qr[0]) + ref_circuit.u3(0, 0, pi, qr[0]) + ref_circuit.u3(0, 0, -pi/2, qr[1]) + ref_circuit.u3(0, 0, 0.3, qr[2]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.cx(qr[2], qr[1]) + ref_circuit.cx(qr[1], qr[2]) + ref_circuit.u3(0, 0, 0.1, qr[1]) + ref_circuit.u3(pi, pi/2, pi/2, qr[1]) + ref_circuit.u3(0, 0, pi/4, qr[2]) + ref_circuit.u3(0.3, 0.0, -0.1, qr[2]) + ref_circuit.u3(pi, 0, pi, qr[2]) + ref_circuit.snapshot('0') + ref_circuit.measure(qr, cr) + ref_dag = circuit_to_dag(ref_circuit) + + self.assertEqual(unrolled_dag, ref_dag) + + def test_simple_unroll_parameterized_without_expressions(self): + """Verify unrolling parameterized gates without expressions.""" + qr = QuantumRegister(1) + qc = QuantumCircuit(qr) + + theta = Parameter('theta') + + qc.rz(theta, qr[0]) + dag = circuit_to_dag(qc) + + pass_ = UnrollCustomDefinitions(std_eqlib, ['u1', 'cx']) + dag = pass_.run(dag) + + unrolled_dag = BasisTranslator(std_eqlib, ['u1', 'cx']).run(dag) + + expected = QuantumCircuit(qr) + expected.u1(theta, qr[0]) + + self.assertEqual(circuit_to_dag(expected), unrolled_dag) + + def test_simple_unroll_parameterized_with_expressions(self): + """Verify unrolling parameterized gates with expressions.""" + qr = QuantumRegister(1) + qc = QuantumCircuit(qr) + + theta = Parameter('theta') + phi = Parameter('phi') + sum_ = theta + phi + + qc.rz(sum_, qr[0]) + dag = circuit_to_dag(qc) + pass_ = UnrollCustomDefinitions(std_eqlib, ['u1', 'cx']) + dag = pass_.run(dag) + + unrolled_dag = BasisTranslator(std_eqlib, ['u1', 'cx']).run(dag) + + expected = QuantumCircuit(qr) + expected.u1(sum_, qr[0]) + + self.assertEqual(circuit_to_dag(expected), unrolled_dag) + + def test_definition_unroll_parameterized(self): + """Verify that unrolling complex gates with parameters does not raise.""" + qr = QuantumRegister(2) + qc = QuantumCircuit(qr) + + theta = Parameter('theta') + + qc.cu1(theta, qr[1], qr[0]) + qc.cu1(theta * theta, qr[0], qr[1]) + dag = circuit_to_dag(qc) + pass_ = UnrollCustomDefinitions(std_eqlib, ['u1', 'cx']) + dag = pass_.run(dag) + + out_dag = BasisTranslator(std_eqlib, ['u1', 'cx']).run(dag) + + self.assertEqual(out_dag.count_ops(), {'u1': 6, 'cx': 4}) + + def test_unrolling_parameterized_composite_gates(self): + """Verify unrolling circuits with parameterized composite gates.""" + mock_sel = EquivalenceLibrary(base=std_eqlib) + + qr1 = QuantumRegister(2) + subqc = QuantumCircuit(qr1) + + theta = Parameter('theta') + + subqc.rz(theta, qr1[0]) + subqc.cx(qr1[0], qr1[1]) + subqc.rz(theta, qr1[1]) + + # Expanding across register with shared parameter + qr2 = QuantumRegister(4) + qc = QuantumCircuit(qr2) + + sub_instr = circuit_to_instruction(subqc, equivalence_library=mock_sel) + qc.append(sub_instr, [qr2[0], qr2[1]]) + qc.append(sub_instr, [qr2[2], qr2[3]]) + + dag = circuit_to_dag(qc) + pass_ = UnrollCustomDefinitions(mock_sel, ['u1', 'cx']) + dag = pass_.run(dag) + + out_dag = BasisTranslator(mock_sel, ['u1', 'cx']).run(dag) + + expected = QuantumCircuit(qr2) + expected.u1(theta, qr2[0]) + expected.u1(theta, qr2[2]) + expected.cx(qr2[0], qr2[1]) + expected.cx(qr2[2], qr2[3]) + expected.u1(theta, qr2[1]) + expected.u1(theta, qr2[3]) + + self.assertEqual(circuit_to_dag(expected), out_dag) + + # Expanding across register with shared parameter + qc = QuantumCircuit(qr2) + + phi = Parameter('phi') + gamma = Parameter('gamma') + + sub_instr = circuit_to_instruction(subqc, {theta: phi}, mock_sel) + qc.append(sub_instr, [qr2[0], qr2[1]]) + sub_instr = circuit_to_instruction(subqc, {theta: gamma}, mock_sel) + qc.append(sub_instr, [qr2[2], qr2[3]]) + + dag = circuit_to_dag(qc) + pass_ = UnrollCustomDefinitions(mock_sel, ['u1', 'cx']) + dag = pass_.run(dag) + + out_dag = BasisTranslator(mock_sel, ['u1', 'cx']).run(dag) + + expected = QuantumCircuit(qr2) + expected.u1(phi, qr2[0]) + expected.u1(gamma, qr2[2]) + expected.cx(qr2[0], qr2[1]) + expected.cx(qr2[2], qr2[3]) + expected.u1(phi, qr2[1]) + expected.u1(gamma, qr2[3]) + + self.assertEqual(circuit_to_dag(expected), out_dag) diff --git a/test/python/transpiler/test_unroll_custom_definitions.py b/test/python/transpiler/test_unroll_custom_definitions.py new file mode 100644 index 000000000000..c14c7460ed0b --- /dev/null +++ b/test/python/transpiler/test_unroll_custom_definitions.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 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 the BasisTranslator pass""" + +from qiskit.transpiler.passes.basis import UnrollCustomDefinitions + +from qiskit.test import QiskitTestCase +from qiskit.circuit import EquivalenceLibrary, Gate +from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.converters import circuit_to_dag +from qiskit.exceptions import QiskitError + + +class TestGate(Gate): + """Mock one qubit zero param gate.""" + def __init__(self): + super().__init__('tg', 1, []) + + +class TestCompositeGate(Gate): + """Mock one qubit zero param gate.""" + def __init__(self): + super().__init__('tcg', 1, []) + + +class TestUnrollCustomDefinitions(QiskitTestCase): + """Test the UnrollCustomDefinitions pass.""" + + def test_dont_unroll_a_gate_in_eq_lib(self): + """Verify we don't unroll a gate found in equivalence_library.""" + eq_lib = EquivalenceLibrary() + + gate = TestGate() + equiv = QuantumCircuit(1) + equiv.h(0) + + eq_lib.add_equivalence(gate, equiv) + + qc = QuantumCircuit(1) + qc.append(gate, [0]) + + dag = circuit_to_dag(qc) + out = UnrollCustomDefinitions(eq_lib, ['u3', 'cx']).run(dag) + + expected = qc.copy() + expected_dag = circuit_to_dag(expected) + + self.assertEqual(out, expected_dag) + + def test_dont_unroll_a_gate_in_basis_gates(self): + """Verify we don't unroll a gate in basis_gates.""" + eq_lib = EquivalenceLibrary() + + gate = TestGate() + qc = QuantumCircuit(1) + qc.append(gate, [0]) + + dag = circuit_to_dag(qc) + out = UnrollCustomDefinitions(eq_lib, ['u3', 'cx', 'tg']).run(dag) + + expected = qc.copy() + expected_dag = circuit_to_dag(expected) + + self.assertEqual(out, expected_dag) + + def test_raise_for_opaque_not_in_eq_lib(self): + """Verify we raise for an opaque gate not in basis_gates or eq_lib.""" + eq_lib = EquivalenceLibrary() + + gate = TestGate() + qc = QuantumCircuit(1) + qc.append(gate, [0]) + + dag = circuit_to_dag(qc) + with self.assertRaisesRegex(QiskitError, 'Cannot unroll'): + UnrollCustomDefinitions(eq_lib, ['u3', 'cx']).run(dag) + + def test_unroll_gate_until_reach_basis_gates(self): + """Verify we unroll gates until we hit basis_gates.""" + eq_lib = EquivalenceLibrary() + + gate = TestCompositeGate() + + q = QuantumRegister(1, 'q') + gate.definition = [(TestGate(), [q[0]], [])] + + qc = QuantumCircuit(1) + qc.append(gate, [0]) + + dag = circuit_to_dag(qc) + out = UnrollCustomDefinitions(eq_lib, ['u3', 'cx', 'tg']).run(dag) + + expected = QuantumCircuit(1) + expected.append(TestGate(), [0]) + expected_dag = circuit_to_dag(expected) + + self.assertEqual(out, expected_dag) + + def test_unroll_twice_until_we_get_to_eqlib(self): + """Verify we unroll gates until we hit basis_gates.""" + eq_lib = EquivalenceLibrary() + + base_gate = TestGate() + equiv = QuantumCircuit(1) + equiv.h(0) + + eq_lib.add_equivalence(base_gate, equiv) + + gate = TestCompositeGate() + + q = QuantumRegister(1, 'q') + gate.definition = [(TestGate(), [q[0]], [])] + + qc = QuantumCircuit(1) + qc.append(gate, [0]) + + dag = circuit_to_dag(qc) + out = UnrollCustomDefinitions(eq_lib, ['u3', 'cx']).run(dag) + + expected = QuantumCircuit(1) + expected.append(TestGate(), [0]) + expected_dag = circuit_to_dag(expected) + + self.assertEqual(out, expected_dag) diff --git a/test/python/transpiler/test_unroller.py b/test/python/transpiler/test_unroller.py index 5c1c4d883ce2..35ae020afd5c 100644 --- a/test/python/transpiler/test_unroller.py +++ b/test/python/transpiler/test_unroller.py @@ -295,22 +295,6 @@ def test_definition_unroll_parameterized(self): self.assertEqual(out_dag.count_ops(), {'u1': 6, 'cx': 4}) - def test_definition_unroll_parameterized_with_expressions(self): - """Verify that unrolling complex gates with parameter expressions raises.""" - qr = QuantumRegister(2) - qc = QuantumCircuit(qr) - - theta = Parameter('theta') - phi = Parameter('phi') - sum_ = theta + phi - - qc.cu1(sum_, qr[0], qr[1]) - dag = circuit_to_dag(qc) - - with self.assertRaisesRegex(QiskitError, 'unsupported'): - Unroller(['u1', 'cx']).run(dag) - raise QiskitError('unsupported') - def test_unrolling_parameterized_composite_gates(self): """Verify unrolling circuits with parameterized composite gates.""" qr1 = QuantumRegister(2)