From 179c29cdf6a2c35b43ddf892aad8cb592a4b4dfc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 19 Jan 2023 19:38:44 +0200 Subject: [PATCH] High-level-synthesis for permutations (#9157) * Adding permutation synthesis algorithm for LNN * release notes * Checking that the synthesized permutation adheres to the LNN connectivity and has depth guaranteed by the algorithm * Adding tests for 15 qubits Co-authored-by: Nir Gavrielov * Changing Permutation to be a Gate rather than QuantumCircuit * Adding the property pattern to Permutation class * fixing assert * improving description message for _get_ordered_swap * applying suggestions from code review * minor * attempt to fix docstring * Another attempt to fix docsting * another attempt to fix docstring * temporarily simplifying docstring to see if this passes docs build * adding blank line * another attempt * Restoring docstring * removing extra line * adding __array__ method for permutation + tests * HLS permutation plugin based on the original synthesis algorithm for permutations * speeding up _get_ordered_swap based on review comments * Adding depth-2 synthesis algorithm for permutations for all-to-all connectivity; including tests and plugin * release notes * Adding example to release notes * Update documentation of Permutation * add missing import * drawing decomposed circuit with permutations * forgot parenthesis * restoring qasm for circuits containing Permutations * Adding permutation method to QuantumCircuit * Adding test for quantum circuit with permutations * pylint * adding inverse() method to permutations * qpy support for permutations * tests for quantum circuits with permutations * checking depth bound on the ACG method * Adding tests for new Permutation functionality * black * Following review, keeping the old Permutation quantum circuit for backward compatibility, and naming the new permutation gate class as PermutationGate * additional fixes * updating release notes * docs fix * Removing permutation method from QuantumCircuit * Adding QPY test for circuits with permutation gates * Update qiskit/circuit/quantumcircuit.py Co-authored-by: Matthew Treinish * Set default qasm name override to None Co-authored-by: Nir Gavrielov Co-authored-by: Matthew Treinish Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- qiskit/circuit/library/__init__.py | 2 + .../library/generalized_gates/__init__.py | 2 +- .../library/generalized_gates/permutation.py | 115 +++++++++++++- qiskit/circuit/quantumcircuit.py | 37 +++-- qiskit/synthesis/__init__.py | 8 +- qiskit/synthesis/permutation/__init__.py | 1 + .../synthesis/permutation/permutation_full.py | 90 +++++++++++ .../permutation/permutation_utils.py | 29 ++++ .../passes/synthesis/high_level_synthesis.py | 30 +++- .../synthesis/linear_functions_synthesis.py | 6 +- ...on-synthesis-plugins-9ab9409bc852f5de.yaml | 68 ++++++++ setup.py | 4 + .../circuit/library/test_permutation.py | 145 +++++++++++++++++- test/python/circuit/test_circuit_qasm.py | 19 +++ test/python/circuit/test_gate_definitions.py | 1 + .../synthesis/test_permutation_synthesis.py | 63 +++++++- .../test_linear_functions_passes.py | 2 +- 17 files changed, 593 insertions(+), 29 deletions(-) create mode 100644 qiskit/synthesis/permutation/permutation_full.py create mode 100644 releasenotes/notes/add-permutation-synthesis-plugins-9ab9409bc852f5de.yaml diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index 08d7cfea098a..5657604c1ccd 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -180,6 +180,7 @@ MCMT MCMTVChain Permutation + PermutationGate GMS GR GRX @@ -497,6 +498,7 @@ MCMT, MCMTVChain, Permutation, + PermutationGate, GMS, MSGate, GR, diff --git a/qiskit/circuit/library/generalized_gates/__init__.py b/qiskit/circuit/library/generalized_gates/__init__.py index e7dd1b6c8b0f..43c0b4c36e5a 100644 --- a/qiskit/circuit/library/generalized_gates/__init__.py +++ b/qiskit/circuit/library/generalized_gates/__init__.py @@ -13,7 +13,7 @@ """The circuit library module on generalized gates.""" from .diagonal import Diagonal -from .permutation import Permutation +from .permutation import Permutation, PermutationGate from .mcmt import MCMT, MCMTVChain from .gms import GMS, MSGate from .gr import GR, GRX, GRY, GRZ diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index c415d5704e06..732d056ce761 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -10,13 +10,17 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Permutation circuit.""" +"""Permutation circuit (the old way to specify permutations, which is required for +backward compatibility and which will be eventually deprecated) and the permutation +gate (the new way to specify permutations, allowing a variety of synthesis algorithms). +""" from typing import List, Optional import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.quantumcircuit import Gate from qiskit.circuit.exceptions import CircuitError @@ -88,3 +92,112 @@ def __init__( all_qubits = self.qubits self.append(circuit.to_gate(), all_qubits) + + +class PermutationGate(Gate): + """A gate that permutes qubits.""" + + def __init__( + self, + pattern: List[int], + ) -> None: + """Return a permutation gate. + + Args: + pattern: permutation pattern, describing which qubits occupy the + positions 0, 1, 2, etc. after applying the permutation, that + is ``pattern[k] = m`` when the permutation maps qubit ``m`` + to position ``k``. As an example, the pattern ``[2, 4, 3, 0, 1]`` + means that qubit ``2`` goes to position ``0``, qubit ``4`` + goes to the position ``1``, etc. + + Raises: + CircuitError: if permutation pattern is malformed. + + Reference Circuit: + .. plot:: + + from qiskit.circuit.quantumcircuit import QuantumCircuit + from qiskit.circuit.library import PermutationGate + A = [2,4,3,0,1] + permutation = PermutationGate(A) + circuit = QuantumCircuit(5) + circuit.append(permutation, [0, 1, 2, 3, 4]) + circuit.draw('mpl') + + Expanded Circuit: + .. plot:: + + from qiskit.circuit.quantumcircuit import QuantumCircuit + from qiskit.circuit.library import PermutationGate + from qiskit.tools.jupyter.library import _generate_circuit_library_visualization + A = [2,4,3,0,1] + permutation = PermutationGate(A) + circuit = QuantumCircuit(5) + circuit.append(permutation, [0, 1, 2, 3, 4]) + + _generate_circuit_library_visualization(circuit.decompose()) + """ + num_qubits = len(pattern) + if sorted(pattern) != list(range(num_qubits)): + raise CircuitError( + "Permutation pattern must be some ordering of 0..num_qubits-1 in a list." + ) + pattern = np.array(pattern) + + # This is needed to support qasm() + self._qasm_name = "permutation__" + "_".join([str(n) for n in pattern]) + "_" + self._qasm_definition = None + + super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern]) + + def __array__(self, dtype=None): + """Return a numpy.array for the Permutation gate.""" + nq = len(self.pattern) + mat = np.zeros((2**nq, 2**nq), dtype=dtype) + + for r in range(2**nq): + # convert row to bitstring, reverse, apply permutation pattern, reverse again, + # and convert to row + bit = bin(r)[2:].zfill(nq)[::-1] + permuted_bit = "".join([bit[j] for j in self.pattern]) + pr = int(permuted_bit[::-1], 2) + mat[pr, r] = 1 + + return mat + + def validate_parameter(self, parameter): + """Parameter validation.""" + return parameter + + @property + def pattern(self): + """Returns the permutation pattern defining this permutation.""" + return self.params[0] + + def inverse(self): + """Returns the inverse of the permutation.""" + + # pylint: disable=cyclic-import + from qiskit.synthesis.permutation.permutation_utils import _inverse_pattern + + return PermutationGate(pattern=_inverse_pattern(self.pattern)) + + def qasm(self): + """The qasm for a permutation.""" + + if not self._qasm_definition: + + # pylint: disable=cyclic-import + from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap + + # This qasm should be identical to the one produced when permutation + # was a circuit rather than a gate. + swaps = _get_ordered_swap(self.pattern) + gates_def = "".join(["swap q" + str(i) + ",q" + str(j) + "; " for i, j in swaps]) + qubit_list = ",".join(["q" + str(n) for n in range(len(self.pattern))]) + self._qasm_definition = ( + "gate " + self._qasm_name + " " + qubit_list + " { " + gates_def + "}" + ) + + return self._qasmif(self._qasm_name) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 55cd82dcf4e7..26fd256350a4 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1698,20 +1698,29 @@ def qasm( operation = operation.copy(name=escaped) # decompose gate using definitions if they are not defined in OpenQASM2 - if ( - operation.name not in existing_gate_names - and operation not in existing_composite_circuits - ): - if operation.name in [ - operation.name for operation in existing_composite_circuits - ]: - # append operation id to name of operation copy to make it unique - operation = operation.copy(name=f"{operation.name}_{id(operation)}") - - existing_composite_circuits.append(operation) - _add_sub_instruction_to_existing_composite_circuits( - operation, existing_gate_names, existing_composite_circuits - ) + if operation.name not in existing_gate_names: + op_qasm_name = None + if operation.name == "permutation": + op_qasm_name = getattr(operation, "_qasm_name", None) + if op_qasm_name: + operation = operation.copy(name=op_qasm_name) + + if operation not in existing_composite_circuits: + if operation.name in [ + operation.name for operation in existing_composite_circuits + ]: + # append operation id to name of operation copy to make it unique + operation = operation.copy(name=f"{operation.name}_{id(operation)}") + + existing_composite_circuits.append(operation) + + # Strictly speaking, the code below does not work for operations that + # do not have the "definition" method but require a complex (recursive) + # "_qasm_definition". Fortunately, right now we do not have any such operations. + if getattr(operation, "definition", None) is not None: + _add_sub_instruction_to_existing_composite_circuits( + operation, existing_gate_names, existing_composite_circuits + ) # Insert qasm representation of the original instruction string_temp += "{} {};\n".format( diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index ee157ddc653d..b843c5a8eb82 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -44,6 +44,8 @@ :toctree: ../stubs/ synth_permutation_depth_lnn_kms + synth_permutation_basic + synth_permutation_acg Clifford Synthesis ================== @@ -86,8 +88,12 @@ QDrift, ) +from .permutation import ( + synth_permutation_depth_lnn_kms, + synth_permutation_basic, + synth_permutation_acg, +) from .linear import synth_cnot_count_full_pmh, synth_cnot_depth_line_kms -from .permutation import synth_permutation_depth_lnn_kms from .clifford import ( synth_clifford_full, synth_clifford_ag, diff --git a/qiskit/synthesis/permutation/__init__.py b/qiskit/synthesis/permutation/__init__.py index 9c6fc78ba46e..7cc8d0174d71 100644 --- a/qiskit/synthesis/permutation/__init__.py +++ b/qiskit/synthesis/permutation/__init__.py @@ -14,3 +14,4 @@ from .permutation_lnn import synth_permutation_depth_lnn_kms +from .permutation_full import synth_permutation_basic, synth_permutation_acg diff --git a/qiskit/synthesis/permutation/permutation_full.py b/qiskit/synthesis/permutation/permutation_full.py new file mode 100644 index 000000000000..3b5697511f03 --- /dev/null +++ b/qiskit/synthesis/permutation/permutation_full.py @@ -0,0 +1,90 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Synthesis algorithm for Permutation gates for full-connectivity.""" + +from qiskit.circuit.quantumcircuit import QuantumCircuit +from .permutation_utils import ( + _get_ordered_swap, + _inverse_pattern, + _pattern_to_cycles, + _decompose_cycles, +) + + +def synth_permutation_basic(pattern): + """Synthesize a permutation circuit for a fully-connected + architecture using sorting. + + More precisely, if the input permutation is a cycle of length ``m``, + then this creates a quantum circuit with ``m-1`` SWAPs (and of depth ``m-1``); + if the input permutation consists of several disjoint cycles, then each cycle + is essentially treated independently. + + Args: + pattern (Union[list[int], np.ndarray]): permutation pattern, describing + which qubits occupy the positions 0, 1, 2, etc. after applying the + permutation. That is, ``pattern[k] = m`` when the permutation maps + qubit ``m`` to position ``k``. As an example, the pattern ``[2, 4, 3, 0, 1]`` + means that qubit ``2`` goes to position ``0``, qubit ``4`` goes to + position ``1``, etc. + + Returns: + QuantumCircuit: the synthesized quantum circuit. + """ + # This is the very original Qiskit algorithm for synthesizing permutations. + + num_qubits = len(pattern) + qc = QuantumCircuit(num_qubits) + + swaps = _get_ordered_swap(pattern) + + for swap in swaps: + qc.swap(swap[0], swap[1]) + + return qc + + +def synth_permutation_acg(pattern): + """Synthesize a permutation circuit for a fully-connected + architecture using the Alon, Chung, Graham method. + + This produces a quantum circuit of depth 2 (measured in the number of SWAPs). + + This implementation is based on the Theorem 2 in the paper + "Routing Permutations on Graphs Via Matchings" (1993), + available at https://www.cs.tau.ac.il/~nogaa/PDFS/r.pdf. + + Args: + pattern (Union[list[int], np.ndarray]): permutation pattern, describing + which qubits occupy the positions 0, 1, 2, etc. after applying the + permutation. That is, ``pattern[k] = m`` when the permutation maps + qubit ``m`` to position ``k``. As an example, the pattern ``[2, 4, 3, 0, 1]`` + means that qubit ``2`` goes to position ``0``, qubit ``4`` goes to + position ``1``, etc. + + Returns: + QuantumCircuit: the synthesized quantum circuit. + """ + + num_qubits = len(pattern) + qc = QuantumCircuit(num_qubits) + + # invert pattern (Qiskit notation is opposite) + cur_pattern = _inverse_pattern(pattern) + cycles = _pattern_to_cycles(cur_pattern) + swaps = _decompose_cycles(cycles) + + for swap in swaps: + qc.swap(swap[0], swap[1]) + + return qc diff --git a/qiskit/synthesis/permutation/permutation_utils.py b/qiskit/synthesis/permutation/permutation_utils.py index dc8fdc2bc14a..6c6d950dc383 100644 --- a/qiskit/synthesis/permutation/permutation_utils.py +++ b/qiskit/synthesis/permutation/permutation_utils.py @@ -42,3 +42,32 @@ def _inverse_pattern(pattern): """Finds inverse of a permutation pattern.""" b_map = {pos: idx for idx, pos in enumerate(pattern)} return [b_map[pos] for pos in range(len(pattern))] + + +def _pattern_to_cycles(pattern): + """Given a permutation pattern, creates its disjoint cycle decomposition.""" + nq = len(pattern) + explored = [False] * nq + cycles = [] + for i in pattern: + cycle = [] + while not explored[i]: + cycle.append(i) + explored[i] = True + i = pattern[i] + if len(cycle) >= 2: + cycles.append(cycle) + return cycles + + +def _decompose_cycles(cycles): + """Given a disjoint cycle decomposition, decomposes every cycle into a SWAP + circuit of depth 2.""" + swap_list = [] + for cycle in cycles: + m = len(cycle) + for i in range((m - 1) // 2): + swap_list.append((cycle[i - 1], cycle[m - 3 - i])) + for i in range(m // 2): + swap_list.append((cycle[i - 1], cycle[m - 2 - i])) + return swap_list diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index 356cacdef6b5..703f814b72fa 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -15,11 +15,13 @@ from qiskit.converters import circuit_to_dag +from qiskit.synthesis import synth_permutation_basic, synth_permutation_acg from qiskit.transpiler.basepasses import TransformationPass from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError from qiskit.synthesis import synth_clifford_full from qiskit.synthesis.linear import synth_cnot_count_full_pmh +from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin @@ -114,7 +116,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: hls_plugin_manager = HighLevelSynthesisPluginManager() for node in dag.op_nodes(): - if node.name in self.hls_config.methods.keys(): # the operation's name appears in the user-provided config, # we use the list of methods provided by the user @@ -170,3 +171,30 @@ def run(self, high_level_object, **options): """Run synthesis for the given LinearFunction.""" decomposition = synth_cnot_count_full_pmh(high_level_object.linear) return decomposition + + +class KMSSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the Kutin, Moulton, Smithline method.""" + + def run(self, high_level_object, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_depth_lnn_kms(high_level_object.pattern) + return decomposition + + +class BasicSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on sorting.""" + + def run(self, high_level_object, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_basic(high_level_object.pattern) + return decomposition + + +class ACGSynthesisPermutation(HighLevelSynthesisPlugin): + """The permutation synthesis plugin based on the Alon, Chung, Graham method.""" + + def run(self, high_level_object, **options): + """Run synthesis for the given Permutation.""" + decomposition = synth_permutation_acg(high_level_object.pattern) + return decomposition diff --git a/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py b/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py index 62e6a0e48031..892902bbdb2a 100644 --- a/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py @@ -17,7 +17,7 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.dagcircuit.dagcircuit import DAGCircuit -from qiskit.circuit.library import Permutation +from qiskit.circuit.library import PermutationGate from qiskit.circuit.exceptions import CircuitError from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig @@ -62,6 +62,6 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: except CircuitError: continue - permutation = Permutation(len(pattern), pattern) - dag.substitute_node(node, permutation.to_instruction()) + permutation = PermutationGate(pattern) + dag.substitute_node(node, permutation) return dag diff --git a/releasenotes/notes/add-permutation-synthesis-plugins-9ab9409bc852f5de.yaml b/releasenotes/notes/add-permutation-synthesis-plugins-9ab9409bc852f5de.yaml new file mode 100644 index 000000000000..091955c0edf3 --- /dev/null +++ b/releasenotes/notes/add-permutation-synthesis-plugins-9ab9409bc852f5de.yaml @@ -0,0 +1,68 @@ +--- +features: + - | + Added a new class :class:`~qiskit.circuit.library.PermutationGate` for + representing permutation logic. The older way of representing permutations + via quantum circuits, see :class:`~qiskit.circuit.library.Permutation`, + remains for backward compatibility, but will be deprecated in the future. + The new way of representing permutation logic avoids synthesizing a permutation + circuit when a ``PermutationGate`` is declared, delaying the actual synthesis to + the transpiler. It also allows to easily choose between several different + algorithms for synthesizing permutations, which are available as high-level-synthesis + permutation plugins. + + The :class:`~qiskit.circuit.library.PermutationGate` has a method + ``__array__`` that returns a unitary matrix for a permutation, in particular + allowing to create unitary matrices from circuits containing + :class:`~qiskit.circuit.library.PermutationGate` gates. + + - | + Added several high-level-synthesis plugins for synthesizing permutations. + The ``BasicSynthesisPermutation`` plugin applies to fully-connected + architectures and is based on sorting. This is the previously used + algorithm for constructing quantum circuits for permutations. + The ``ACGSynthesisPermutation`` plugin also applies to fully-connected + architectures but is based on the Alon, Chung, Graham method. It synthesizes + any permutation in depth 2 (measured in terms of SWAPs). + The ``KMSSynthesisPermutation`` plugin applies to linear nearest-neighbor + architectures and corresponds to the recently added Kutin, Moulton, Smithline + method. + + For example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import PermutationGate + from qiskit.transpiler import PassManager + from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig, HighLevelSynthesis + from qiskit.transpiler.passes.synthesis.plugin import HighLevelSynthesisPluginManager + + # Create a permutation and add it to a quantum circuit + perm = PermutationGate([4, 6, 3, 7, 1, 2, 0, 5]) + qc = QuantumCircuit(8) + qc.append(perm, range(8)) + + # Print available plugin names for synthesizing permutations + # Returns ['acg', 'basic', 'default', 'kms'] + print(HighLevelSynthesisPluginManager().method_names("permutation")) + + # Default plugin for permutations + # Returns a quantum circuit with size 6 and depth 3 + qct = PassManager(HighLevelSynthesis()).run(qc) + print(f"Default: {qct.size() = }, {qct.depth() = }") + + # KMSSynthesisPermutation plugin for permutations + # Returns a quantum circuit with size 18 and depth 6 + # but adhering to the linear nearest-neighbor architecture. + qct = PassManager(HighLevelSynthesis(HLSConfig(permutation=[("kms", {})]))).run(qc) + print(f"kms: {qct.size() = }, {qct.depth() = }") + + # BasicSynthesisPermutation plugin for permutations + # Returns a quantum circuit with size 6 and depth 3 + qct = PassManager(HighLevelSynthesis(HLSConfig(permutation=[("basic", {})]))).run(qc) + print(f"basic: {qct.size() = }, {qct.depth() = }") + + # ACGSynthesisPermutation plugin for permutations + # Returns a quantum circuit with size 6 and depth 2 + qct = PassManager(HighLevelSynthesis(HLSConfig(permutation=[("acg", {})]))).run(qc) + print(f"acg: {qct.size() = }, {qct.depth() = }") + diff --git a/setup.py b/setup.py index 4154a56ebb0c..29c93a427d64 100755 --- a/setup.py +++ b/setup.py @@ -106,6 +106,10 @@ "qiskit.synthesis": [ "clifford.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisClifford", "linear_function.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:DefaultSynthesisLinearFunction", + "permutation.default = qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation", + "permutation.kms = qiskit.transpiler.passes.synthesis.high_level_synthesis:KMSSynthesisPermutation", + "permutation.basic = qiskit.transpiler.passes.synthesis.high_level_synthesis:BasicSynthesisPermutation", + "permutation.acg = qiskit.transpiler.passes.synthesis.high_level_synthesis:ACGSynthesisPermutation", ], "qiskit.transpiler.routing": [ "basic = qiskit.transpiler.preset_passmanagers.builtin_plugins:BasicSwapPassManager", diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py index b250964f9564..bf4da582b6ad 100644 --- a/test/python/circuit/library/test_permutation.py +++ b/test/python/circuit/library/test_permutation.py @@ -10,15 +10,21 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test library of permutation logic quantum circuits.""" +"""Test permutation quantum circuits, permutation gates, and quantum circuits that +contain permutation gates.""" + +import io import unittest +import numpy as np +from qiskit import QuantumRegister from qiskit.test.base import QiskitTestCase from qiskit.circuit import QuantumCircuit from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.library import Permutation +from qiskit.circuit.library import Permutation, PermutationGate from qiskit.quantum_info import Operator +from qiskit.qpy import dump, load class TestPermutationLibrary(QiskitTestCase): @@ -39,5 +45,140 @@ def test_permutation_bad(self): self.assertRaises(CircuitError, Permutation, 4, [1, 0, -1, 2]) +class TestPermutationGate(QiskitTestCase): + """Tests for the PermutationGate class.""" + + def test_permutation(self): + """Test that Operator can be constructed.""" + perm = PermutationGate(pattern=[1, 0, 3, 2]) + expected = QuantumCircuit(4) + expected.swap(0, 1) + expected.swap(2, 3) + expected = Operator(expected) + simulated = Operator(perm) + self.assertTrue(expected.equiv(simulated)) + + def test_permutation_bad(self): + """Test that [0,..,n-1] permutation is required (no -1 for last element).""" + self.assertRaises(CircuitError, PermutationGate, [1, 0, -1, 2]) + + def test_permutation_array(self): + """Test correctness of the ``__array__`` method.""" + perm = PermutationGate([1, 2, 0]) + # The permutation pattern means q1->q0, q2->q1, q0->q2, or equivalently + # q0'=q1, q1'=q2, q2'=q0, where the primed values are the values after the + # permutation. The following matrix is the expected unitary matrix for this. + # As an example, the second column represents the result of applying + # the permutation to |001>, i.e. to q2=0, q1=0, q0=1. We should get + # q2'=q0=1, q1'=q2=0, q0'=q1=0, that is the state |100>. This corresponds + # to the "1" in the 5 row. + expected_op = np.array( + [ + [1, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 1, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 1, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 1, 0], + [0, 1, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 1, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 1], + ] + ) + self.assertTrue(np.array_equal(perm.__array__(dtype=int), expected_op)) + + def test_pattern(self): + """Test the ``pattern`` method.""" + pattern = [1, 3, 5, 0, 4, 2] + perm = PermutationGate(pattern) + self.assertTrue(np.array_equal(perm.pattern, pattern)) + + def test_inverse(self): + """Test correctness of the ``inverse`` method.""" + perm = PermutationGate([1, 3, 5, 0, 4, 2]) + + # We have the permutation 1->0, 3->1, 5->2, 0->3, 4->4, 2->5. + # The inverse permutations is 0->1, 1->3, 2->5, 3->0, 4->4, 5->2, or + # after reordering 3->0, 0->1, 5->2, 1->3, 4->4, 2->5. + inverse_perm = perm.inverse() + expected_inverse_perm = PermutationGate([3, 0, 5, 1, 4, 2]) + self.assertTrue(np.array_equal(inverse_perm.pattern, expected_inverse_perm.pattern)) + + +class TestPermutationGatesOnCircuit(QiskitTestCase): + """Tests for quantum circuits containing permutations.""" + + def test_append_to_circuit(self): + """Test method for adding Permutations to quantum circuit.""" + qc = QuantumCircuit(5) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2]) + qc.append(PermutationGate([2, 3, 0, 1]), [1, 2, 3, 4]) + self.assertIsInstance(qc.data[0].operation, PermutationGate) + self.assertIsInstance(qc.data[1].operation, PermutationGate) + + def test_inverse(self): + """Test inverse method for circuits with permutations.""" + qc = QuantumCircuit(5) + qc.append(PermutationGate([1, 2, 3, 0]), [0, 4, 2, 1]) + qci = qc.inverse() + qci_pattern = qci.data[0].operation.pattern + expected_pattern = [3, 0, 1, 2] + + # The inverse permutations should be defined over the same qubits but with the + # inverse permutation pattern. + self.assertTrue(np.array_equal(qci_pattern, expected_pattern)) + self.assertTrue(np.array_equal(qc.data[0].qubits, qci.data[0].qubits)) + + def test_reverse_ops(self): + """Test reverse_ops method for circuits with permutations.""" + qc = QuantumCircuit(5) + qc.append(PermutationGate([1, 2, 3, 0]), [0, 4, 2, 1]) + qcr = qc.reverse_ops() + + # The reversed circuit should have the permutation gate with the same pattern and over the + # same qubits. + self.assertTrue(np.array_equal(qc.data[0].operation.pattern, qcr.data[0].operation.pattern)) + self.assertTrue(np.array_equal(qc.data[0].qubits, qcr.data[0].qubits)) + + def test_conditional(self): + """Test adding conditional permutations.""" + qc = QuantumCircuit(5, 1) + qc.append(PermutationGate([1, 2, 0]), [2, 3, 4]).c_if(0, 1) + self.assertIsNotNone(qc.data[0].operation.condition) + + def test_qasm(self): + """Test qasm for circuits with permutations.""" + qr = QuantumRegister(5, "q0") + circuit = QuantumCircuit(qr) + pattern = [2, 4, 3, 0, 1] + permutation = PermutationGate(pattern) + circuit.append(permutation, [0, 1, 2, 3, 4]) + circuit.h(qr[0]) + + expected_qasm = ( + "OPENQASM 2.0;\n" + 'include "qelib1.inc";\n' + "gate permutation__2_4_3_0_1_ q0,q1,q2,q3,q4 { swap q2,q3; swap q1,q4; swap q0,q3; }\n" + "qreg q0[5];\n" + "permutation__2_4_3_0_1_ q0[0],q0[1],q0[2],q0[3],q0[4];\n" + "h q0[0];\n" + ) + self.assertEqual(expected_qasm, circuit.qasm()) + + def test_qpy(self): + """Test qpy for circuits with permutations.""" + circuit = QuantumCircuit(6, 1) + circuit.cx(0, 1) + circuit.append(PermutationGate([1, 2, 0]), [2, 4, 5]) + circuit.h(4) + print(circuit) + + qpy_file = io.BytesIO() + dump(circuit, qpy_file) + qpy_file.seek(0) + new_circuit = load(qpy_file)[0] + + self.assertEqual(circuit, new_circuit) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index d02aba018d58..060178eb61e8 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -12,6 +12,7 @@ """Test Qiskit's QuantumCircuit class.""" +import unittest from math import pi import re @@ -529,3 +530,21 @@ def test_circuit_raises_on_single_bit_condition(self): with self.assertRaisesRegex(QasmError, "OpenQASM 2 can only condition on registers"): qc.qasm() + + def test_circuit_qasm_with_permutations(self): + """Test circuit qasm() method with Permutation gates.""" + from qiskit.circuit.library import PermutationGate + + qc = QuantumCircuit(4) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + + expected_qasm = """OPENQASM 2.0; +include "qelib1.inc"; +gate permutation__2_1_0_ q0,q1,q2 { swap q0,q2; } +qreg q[4]; +permutation__2_1_0_ q[0],q[1],q[2];\n""" + self.assertEqual(qc.qasm(), expected_qasm) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index cac5e3f33879..b7d5f4f06834 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -279,6 +279,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "ClassicalElement", "StatePreparation", "LinearFunction", + "PermutationGate", "Commuting2qBlock", "PauliEvolutionGate", } diff --git a/test/python/synthesis/test_permutation_synthesis.py b/test/python/synthesis/test_permutation_synthesis.py index 93002235bac6..85e4be4d7cb1 100644 --- a/test/python/synthesis/test_permutation_synthesis.py +++ b/test/python/synthesis/test_permutation_synthesis.py @@ -17,8 +17,10 @@ import numpy as np from ddt import ddt, data -from qiskit.circuit.library import LinearFunction -from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms +from qiskit.quantum_info.operators import Operator +from qiskit.circuit.library import LinearFunction, PermutationGate +from qiskit.synthesis import synth_permutation_acg +from qiskit.synthesis.permutation import synth_permutation_depth_lnn_kms, synth_permutation_basic from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap from qiskit.test import QiskitTestCase @@ -32,14 +34,53 @@ def test_get_ordered_swap(self, width): """Test get_ordered_swap function produces correct swap list.""" np.random.seed(1) for _ in range(5): - permutation = np.random.permutation(width) - swap_list = _get_ordered_swap(permutation) + pattern = np.random.permutation(width) + swap_list = _get_ordered_swap(pattern) output = list(range(width)) for i, j in swap_list: output[i], output[j] = output[j], output[i] - self.assertTrue(np.array_equal(permutation, output)) + self.assertTrue(np.array_equal(pattern, output)) self.assertLess(len(swap_list), width) + @data(4, 5, 10, 15, 20) + def test_synth_permutation_basic(self, width): + """Test synth_permutation_basic function produces the correct + circuit.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + qc = synth_permutation_basic(pattern) + + # Check that the synthesized circuit consists of SWAP gates only. + for instruction in qc.data: + self.assertEqual(instruction.operation.name, "swap") + + # Construct a linear function from the synthesized circuit, and + # check that its permutation pattern matches the original pattern. + synthesized_pattern = LinearFunction(qc).permutation_pattern() + self.assertTrue(np.array_equal(synthesized_pattern, pattern)) + + @data(4, 5, 10, 15, 20) + def test_synth_permutation_acg(self, width): + """Test synth_permutation_acg function produces the correct + circuit.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + qc = synth_permutation_acg(pattern) + + # Check that the synthesized circuit consists of SWAP gates only. + for instruction in qc.data: + self.assertEqual(instruction.operation.name, "swap") + + # Check that the depth of the circuit (measured in terms of SWAPs) is at most 2. + self.assertLessEqual(qc.depth(), 2) + + # Construct a linear function from the synthesized circuit, and + # check that its permutation pattern matches the original pattern. + synthesized_pattern = LinearFunction(qc).permutation_pattern() + self.assertTrue(np.array_equal(synthesized_pattern, pattern)) + @data(4, 5, 10, 15, 20) def test_synth_permutation_depth_lnn_kms(self, width): """Test synth_permutation_depth_lnn_kms function produces the correct @@ -67,6 +108,18 @@ def test_synth_permutation_depth_lnn_kms(self, width): synthesized_pattern = LinearFunction(qc).permutation_pattern() self.assertTrue(np.array_equal(synthesized_pattern, pattern)) + @data(4, 5, 6, 7) + def test_permutation_matrix(self, width): + """Test that the unitary matrix constructed from permutation pattern + is correct.""" + np.random.seed(1) + for _ in range(5): + pattern = np.random.permutation(width) + qc = synth_permutation_depth_lnn_kms(pattern) + expected = Operator(qc) + constructed = Operator(PermutationGate(pattern)) + self.assertEqual(expected, constructed) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_linear_functions_passes.py b/test/python/transpiler/test_linear_functions_passes.py index 52df84d38a15..e6e7336e799f 100644 --- a/test/python/transpiler/test_linear_functions_passes.py +++ b/test/python/transpiler/test_linear_functions_passes.py @@ -208,7 +208,7 @@ def test_to_permutation(self, do_commutative_analysis): # check that there is one linear function and one permutation self.assertEqual(circuit3.count_ops()["linear_function"], 1) - self.assertEqual(circuit3.count_ops()["permutation_[2,0,1]"], 1) + self.assertEqual(circuit3.count_ops()["permutation"], 1) # check that the final circuit is still equivalent to the original circuit self.assertEqual(Operator(circuit1), Operator(circuit3))