From fa3d6df04f57d6a5bcc658a7f1a6e5a99984b949 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Wed, 10 Jul 2024 11:27:55 +0300 Subject: [PATCH 1/8] Adding QFT gate to natively reason about Quantum Fourier Transforms (#11463) * initial commit * release notes * fixing synthesis plugin options * finalize merge conflicts * fixing default option values for qft plugins' * additional tests for qft plugins * renaming QftGate to QFTGate * Also renaming Qft to QFT in synthesis method names * appplying Jake's suggestion from code review * inlining _basic_definition into _define * docstring improvements * more docstring improvements * renaming do_swaps to reverse_qubits in the new code * typos * adding synth_qft_full to __init__ * round of suggestions from code review * another round of code review suggestions * fixes * also adding QFTGate plugins to the docs --- pyproject.toml | 3 + qiskit/circuit/library/__init__.py | 3 +- .../circuit/library/basis_change/__init__.py | 2 +- qiskit/circuit/library/basis_change/qft.py | 46 +++++- qiskit/qpy/binary_io/circuits.py | 2 + qiskit/synthesis/__init__.py | 3 +- qiskit/synthesis/qft/__init__.py | 1 + qiskit/synthesis/qft/qft_decompose_full.py | 79 +++++++++++ qiskit/synthesis/qft/qft_decompose_lnn.py | 26 ++-- .../passes/synthesis/high_level_synthesis.py | 133 +++++++++++++++++- .../notes/add-qft-gate-fd4e08f6721a9da4.yaml | 22 +++ test/python/circuit/library/test_qft.py | 119 +++++++++++++++- test/python/circuit/test_gate_definitions.py | 1 + test/python/synthesis/test_qft_synthesis.py | 51 ++++++- .../transpiler/test_high_level_synthesis.py | 47 ++++++- 15 files changed, 510 insertions(+), 28 deletions(-) create mode 100644 qiskit/synthesis/qft/qft_decompose_full.py create mode 100644 releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml diff --git a/pyproject.toml b/pyproject.toml index 2f62557aa15b..b1f7b039e407 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,9 @@ sk = "qiskit.transpiler.passes.synthesis.solovay_kitaev_synthesis:SolovayKitaevS "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" +"qft.full" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisFull" +"qft.line" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisLine" +"qft.default" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:QFTSynthesisFull" "permutation.token_swapper" = "qiskit.transpiler.passes.synthesis.high_level_synthesis:TokenSwapperSynthesisPermutation" [project.entry-points."qiskit.transpiler.init"] diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index a9ae005d982d..39266cf4ca17 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -226,6 +226,7 @@ :template: autosummary/class_no_inherited_members.rst QFT + QFTGate Arithmetic Circuits =================== @@ -523,7 +524,7 @@ XOR, InnerProduct, ) -from .basis_change import QFT +from .basis_change import QFT, QFTGate from .arithmetic import ( FunctionalPauliRotations, LinearPauliRotations, diff --git a/qiskit/circuit/library/basis_change/__init__.py b/qiskit/circuit/library/basis_change/__init__.py index c2b8896608d4..16b7e53cc2d3 100644 --- a/qiskit/circuit/library/basis_change/__init__.py +++ b/qiskit/circuit/library/basis_change/__init__.py @@ -12,4 +12,4 @@ """The basis change circuits.""" -from .qft import QFT +from .qft import QFT, QFTGate diff --git a/qiskit/circuit/library/basis_change/qft.py b/qiskit/circuit/library/basis_change/qft.py index a8816ad470a2..2ec6dd69cb79 100644 --- a/qiskit/circuit/library/basis_change/qft.py +++ b/qiskit/circuit/library/basis_change/qft.py @@ -10,14 +10,13 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Quantum Fourier Transform Circuit.""" +"""Define a Quantum Fourier Transform circuit (QFT) and a native gate (QFTGate).""" -from typing import Optional +from __future__ import annotations import warnings import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, CircuitInstruction - +from qiskit.circuit.quantumcircuit import QuantumCircuit, QuantumRegister, CircuitInstruction, Gate from ..blueprintcircuit import BlueprintCircuit @@ -75,12 +74,12 @@ class QFT(BlueprintCircuit): def __init__( self, - num_qubits: Optional[int] = None, + num_qubits: int | None = None, approximation_degree: int = 0, do_swaps: bool = True, inverse: bool = False, insert_barriers: bool = False, - name: Optional[str] = None, + name: str | None = None, ) -> None: """Construct a new QFT circuit. @@ -293,3 +292,38 @@ def _build(self) -> None: wrapped = circuit.to_instruction() if self.insert_barriers else circuit.to_gate() self.compose(wrapped, qubits=self.qubits, inplace=True) + + +class QFTGate(Gate): + r"""Quantum Fourier Transform Gate. + + The Quantum Fourier Transform (QFT) on :math:`n` qubits is the operation + + .. math:: + + |j\rangle \mapsto \frac{1}{2^{n/2}} \sum_{k=0}^{2^n - 1} e^{2\pi ijk / 2^n} |k\rangle + + """ + + def __init__( + self, + num_qubits: int, + ): + """ + Args: + num_qubits: The number of qubits on which the QFT acts. + """ + super().__init__(name="qft", num_qubits=num_qubits, params=[]) + + def __array__(self, dtype=complex): + """Return a numpy array for the QFTGate.""" + n = self.num_qubits + nums = np.arange(2**n) + outer = np.outer(nums, nums) + return np.exp(2j * np.pi * outer * (0.5**n), dtype=dtype) * (0.5 ** (n / 2)) + + def _define(self): + """Provide a specific decomposition of the QFTGate into a quantum circuit.""" + from qiskit.synthesis.qft import synth_qft_full + + self.definition = synth_qft_full(num_qubits=self.num_qubits) diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index 0e2045d5be5d..142639a4e164 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -397,6 +397,8 @@ def _read_instruction( "DiagonalGate", }: gate = gate_class(params) + elif gate_name == "QFTGate": + gate = gate_class(len(qargs), *params) else: if gate_name == "Barrier": params = [len(qargs)] diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index b46c8eac5459..cfe5f0b304cb 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -91,6 +91,7 @@ ====================== .. autofunction:: synth_qft_line +.. autofunction:: synth_qft_full Unitary Synthesis ================= @@ -162,7 +163,7 @@ synth_circuit_from_stabilizers, ) from .discrete_basis import SolovayKitaevDecomposition, generate_basic_approximations -from .qft import synth_qft_line +from .qft import synth_qft_line, synth_qft_full from .unitary.qsd import qs_decomposition from .unitary import aqc from .one_qubit import OneQubitEulerDecomposer diff --git a/qiskit/synthesis/qft/__init__.py b/qiskit/synthesis/qft/__init__.py index 99bd2f7da9b2..8140bf4a74e2 100644 --- a/qiskit/synthesis/qft/__init__.py +++ b/qiskit/synthesis/qft/__init__.py @@ -13,3 +13,4 @@ """Module containing stabilizer QFT circuit synthesis.""" from .qft_decompose_lnn import synth_qft_line +from .qft_decompose_full import synth_qft_full diff --git a/qiskit/synthesis/qft/qft_decompose_full.py b/qiskit/synthesis/qft/qft_decompose_full.py new file mode 100644 index 000000000000..9038ea6589c3 --- /dev/null +++ b/qiskit/synthesis/qft/qft_decompose_full.py @@ -0,0 +1,79 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 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. +""" +Circuit synthesis for a QFT circuit. +""" + +from __future__ import annotations +import numpy as np +from qiskit.circuit.quantumcircuit import QuantumCircuit + + +def synth_qft_full( + num_qubits: int, + do_swaps: bool = True, + approximation_degree: int = 0, + insert_barriers: bool = False, + inverse: bool = False, + name: str | None = None, +) -> QuantumCircuit: + """Construct a circuit for the Quantum Fourier Transform using all-to-all connectivity. + + .. note:: + + With the default value of ``do_swaps = True``, this synthesis algorithm creates a + circuit that faithfully implements the QFT operation. This circuit contains a sequence + of swap gates at the end, corresponding to reversing the order of its output qubits. + In some applications this reversal permutation can be avoided. Setting ``do_swaps = False`` + creates a circuit without this reversal permutation, at the expense that this circuit + implements the "QFT-with-reversal" instead of QFT. Alternatively, the + :class:`~.ElidePermutations` transpiler pass is able to remove these swap gates. + + Args: + num_qubits: The number of qubits on which the Quantum Fourier Transform acts. + do_swaps: Whether to synthesize the "QFT" or the "QFT-with-reversal" operation. + approximation_degree: The degree of approximation (0 for no approximation). + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in https://arxiv.org/abs/quant-ph/9601018 or + https://arxiv.org/abs/quant-ph/0403071. + insert_barriers: If ``True``, barriers are inserted for improved visualization. + inverse: If ``True``, the inverse Quantum Fourier Transform is constructed. + name: The name of the circuit. + + Returns: + A circuit implementing the QFT operation. + + """ + + circuit = QuantumCircuit(num_qubits, name=name) + + for j in reversed(range(num_qubits)): + circuit.h(j) + num_entanglements = max(0, j - max(0, approximation_degree - (num_qubits - j - 1))) + for k in reversed(range(j - num_entanglements, j)): + # Use negative exponents so that the angle safely underflows to zero, rather than + # using a temporary variable that overflows to infinity in the worst case. + lam = np.pi * (2.0 ** (k - j)) + circuit.cp(lam, j, k) + + if insert_barriers: + circuit.barrier() + + if do_swaps: + for i in range(num_qubits // 2): + circuit.swap(i, num_qubits - i - 1) + + if inverse: + circuit = circuit.inverse() + + return circuit diff --git a/qiskit/synthesis/qft/qft_decompose_lnn.py b/qiskit/synthesis/qft/qft_decompose_lnn.py index a54be481f51b..f1a0876a0c3f 100644 --- a/qiskit/synthesis/qft/qft_decompose_lnn.py +++ b/qiskit/synthesis/qft/qft_decompose_lnn.py @@ -21,21 +21,29 @@ def synth_qft_line( num_qubits: int, do_swaps: bool = True, approximation_degree: int = 0 ) -> QuantumCircuit: - """Synthesis of a QFT circuit for a linear nearest neighbor connectivity. - Based on Fig 2.b in Fowler et al. [1]. + """Construct a circuit for the Quantum Fourier Transform using linear + neighbor connectivity. - Note that this method *reverts* the order of qubits in the circuit, - compared to the original :class:`.QFT` code. - Hence, the default value of the ``do_swaps`` parameter is ``True`` - since it produces a circuit with fewer CX gates. + The construction is based on Fig 2.b in Fowler et al. [1]. + + .. note:: + + With the default value of ``do_swaps = True``, this synthesis algorithm creates a + circuit that faithfully implements the QFT operation. When ``do_swaps = False``, + this synthesis algorithm creates a circuit that corresponds to "QFT-with-reversal": + applying the QFT and reversing the order of its output qubits. Args: - num_qubits: The number of qubits on which the QFT acts. + num_qubits: The number of qubits on which the Quantum Fourier Transform acts. approximation_degree: The degree of approximation (0 for no approximation). - do_swaps: Whether to include the final swaps in the QFT. + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in https://arxiv.org/abs/quant-ph/9601018 or + https://arxiv.org/abs/quant-ph/0403071. + do_swaps: Whether to synthesize the "QFT" or the "QFT-with-reversal" operation. Returns: - A circuit implementation of the QFT circuit. + A circuit implementing the QFT operation. References: 1. A. G. Fowler, S. J. Devitt, and L. C. L. Hollenberg, diff --git a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py index bbc986621050..f1de2b8f2136 100644 --- a/qiskit/transpiler/passes/synthesis/high_level_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/high_level_synthesis.py @@ -131,6 +131,32 @@ ACGSynthesisPermutation KMSSynthesisPermutation TokenSwapperSynthesisPermutation + + +QFT Synthesis +''''''''''''' + +.. list-table:: Plugins for :class:`.QFTGate` (key = ``"qft"``) + :header-rows: 1 + + * - Plugin name + - Plugin class + - Targeted connectivity + * - ``"full"`` + - :class:`~.QFTSynthesisFull` + - all-to-all + * - ``"line"`` + - :class:`~.QFTSynthesisLine` + - linear + * - ``"default"`` + - :class:`~.QFTSynthesisFull` + - all-to-all + +.. autosummary:: + :toctree: ../stubs/ + + QFTSynthesisFull + QFTSynthesisLine """ from typing import Optional, Union, List, Tuple, Callable @@ -157,6 +183,7 @@ ControlModifier, PowerModifier, ) +from qiskit.circuit.library import QFTGate from qiskit.synthesis.clifford import ( synth_clifford_full, synth_clifford_layers, @@ -176,6 +203,10 @@ synth_permutation_acg, synth_permutation_depth_lnn_kms, ) +from qiskit.synthesis.qft import ( + synth_qft_full, + synth_qft_line, +) from .plugin import HighLevelSynthesisPluginManager, HighLevelSynthesisPlugin @@ -887,6 +918,107 @@ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, ** return decomposition +class QFTSynthesisFull(HighLevelSynthesisPlugin): + """Synthesis plugin for QFT gates using all-to-all connectivity. + + This plugin name is :``qft.full`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following additional options: + + * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, + which is the default) or the "QFT-with-reversal" operation (if ``True``). + Some implementation of the ``QFTGate`` include a layer of swap gates at the end + of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` + itself is the last gate in the circuit. + * approximation_degree (int): The degree of approximation (0 for no approximation). + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in [1] or [2]. + * insert_barriers (bool): If True, barriers are inserted as visualization improvement. + * inverse (bool): If True, the inverse Fourier transform is constructed. + * name (str): The name of the circuit. + + References: + 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, + *Approximate Quantum Fourier Transform and Decoherence*, + Physical Review A (1996). + `arXiv:quant-ph/9601018 [quant-ph] `_ + 2. Donny Cheung, + *Improved Bounds for the Approximate QFT* (2004), + `arXiv:quant-ph/0403071 [quant-ph] `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given QFTGate.""" + if not isinstance(high_level_object, QFTGate): + raise TranspilerError( + "The synthesis plugin 'qft.full` only applies to objects of type QFTGate." + ) + + reverse_qubits = options.get("reverse_qubits", False) + approximation_degree = options.get("approximation_degree", 0) + insert_barriers = options.get("insert_barriers", False) + inverse = options.get("inverse", False) + name = options.get("name", None) + + decomposition = synth_qft_full( + num_qubits=high_level_object.num_qubits, + do_swaps=not reverse_qubits, + approximation_degree=approximation_degree, + insert_barriers=insert_barriers, + inverse=inverse, + name=name, + ) + return decomposition + + +class QFTSynthesisLine(HighLevelSynthesisPlugin): + """Synthesis plugin for QFT gates using linear connectivity. + + This plugin name is :``qft.line`` which can be used as the key on + an :class:`~.HLSConfig` object to use this method with :class:`~.HighLevelSynthesis`. + + The plugin supports the following additional options: + + * reverse_qubits (bool): Whether to synthesize the "QFT" operation (if ``False``, + which is the default) or the "QFT-with-reversal" operation (if ``True``). + Some implementation of the ``QFTGate`` include a layer of swap gates at the end + of the synthesized circuit, which can in principle be dropped if the ``QFTGate`` + itself is the last gate in the circuit. + * approximation_degree (int): the degree of approximation (0 for no approximation). + It is possible to implement the QFT approximately by ignoring + controlled-phase rotations with the angle beneath a threshold. This is discussed + in more detail in [1] or [2]. + + References: + 1. Adriano Barenco, Artur Ekert, Kalle-Antti Suominen, and Päivi Törmä, + *Approximate Quantum Fourier Transform and Decoherence*, + Physical Review A (1996). + `arXiv:quant-ph/9601018 [quant-ph] `_ + 2. Donny Cheung, + *Improved Bounds for the Approximate QFT* (2004), + `arXiv:quant-ph/0403071 [quant-ph] `_ + """ + + def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): + """Run synthesis for the given QFTGate.""" + if not isinstance(high_level_object, QFTGate): + raise TranspilerError( + "The synthesis plugin 'qft.line` only applies to objects of type QFTGate." + ) + + reverse_qubits = options.get("reverse_qubits", False) + approximation_degree = options.get("approximation_degree", 0) + + decomposition = synth_qft_line( + num_qubits=high_level_object.num_qubits, + do_swaps=not reverse_qubits, + approximation_degree=approximation_degree, + ) + return decomposition + + class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): """The permutation synthesis plugin based on the token swapper algorithm. @@ -917,7 +1049,6 @@ class TokenSwapperSynthesisPermutation(HighLevelSynthesisPlugin): For more details on the token swapper algorithm, see to the paper: `arXiv:1902.09102 `__. - """ def run(self, high_level_object, coupling_map=None, target=None, qubits=None, **options): diff --git a/releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml b/releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml new file mode 100644 index 000000000000..46b60c8b7025 --- /dev/null +++ b/releasenotes/notes/add-qft-gate-fd4e08f6721a9da4.yaml @@ -0,0 +1,22 @@ +--- +features: + - | + Added a new class :class:`~qiskit.circuit.library.QFTGate` for + natively representing Quantum Fourier Transforms (QFTs). The older way + of representing QFTs via quantum circuits, see + :class:`~qiskit.circuit.library.QFT`, remains for backward compatibility. + The new way of representing a QFT via a gate avoids synthesizing its + definition circuit when the gate is declared, delaying the actual synthesis to + the transpiler. It also allows to easily choose between several different + algorithms for synthesizing QFTs, which are available as high-level-synthesis + plugins. + - | + Added a synthesis method :func:`.synth_qft_full` for constructing a QFT circuit + assuming a fully-connected architecture. + - | + Added two high-level-synthesis plugins for synthesizing a + :class:`~qiskit.circuit.library.QFTGate`. + The class :class:`.QFTSynthesisFull` is based on :func:`.synth_qft_full` and synthesizes + a QFT gate assuming all-to-all connectivity. + The class :class:`.QFTSynthesisLine` is based on :func:`.synth_qft_line` and synthesizes + a QFT gate assuming linear nearest neighbor connectivity. diff --git a/test/python/circuit/library/test_qft.py b/test/python/circuit/library/test_qft.py index 078b5af04ea9..1f5c9715dd7a 100644 --- a/test/python/circuit/library/test_qft.py +++ b/test/python/circuit/library/test_qft.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (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 @@ -12,15 +12,19 @@ """Test library of QFT circuits.""" +import io + import unittest import warnings import numpy as np from ddt import ddt, data, unpack from qiskit import transpile -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import QFT +from qiskit.circuit import QuantumCircuit, QuantumRegister +from qiskit.circuit.library import QFT, QFTGate from qiskit.quantum_info import Operator +from qiskit.qpy import dump, load +from qiskit.qasm2 import dumps from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -206,5 +210,114 @@ def __init__(self, *_args, **_kwargs): qft._build() +@ddt +class TestQFTGate(QiskitTestCase): + """Test the QFT Gate.""" + + @data(2, 3, 4, 5, 6) + def test_array_equivalent_to_decomposition(self, num_qubits): + """Test that the provided __array__ method and that the provided basic + definition are equivalent. + """ + qft_gate = QFTGate(num_qubits=num_qubits) + qft_gate_decomposition = qft_gate.definition + self.assertEqual(Operator(qft_gate), Operator(qft_gate_decomposition)) + + @data(2, 3, 4, 5, 6) + def test_gate_equivalent_to_original(self, num_qubits): + """Test that the Operator can be constructed out of a QFT gate, and is + equivalent to the Operator constructed out of a QFT circuit. + """ + qft_gate = QFTGate(num_qubits=num_qubits) + qft_circuit = QFT(num_qubits=num_qubits) + self.assertEqual(Operator(qft_gate), Operator(qft_circuit)) + + def test_append_to_circuit(self): + """Test adding a QFTGate to a quantum circuit.""" + qc = QuantumCircuit(5) + qc.append(QFTGate(4), [1, 2, 0, 4]) + self.assertIsInstance(qc.data[0].operation, QFTGate) + + @data(2, 3, 4, 5, 6) + def test_circuit_with_gate_equivalent_to_original(self, num_qubits): + """Test that the Operator can be constructed out of a circuit containing a QFT gate, and is + equivalent to the Operator constructed out of a QFT circuit. + """ + qft_gate = QFTGate(num_qubits=num_qubits) + circuit_with_qft_gate = QuantumCircuit(num_qubits) + circuit_with_qft_gate.append(qft_gate, range(num_qubits)) + qft_circuit = QFT(num_qubits=num_qubits) + self.assertEqual(Operator(circuit_with_qft_gate), Operator(qft_circuit)) + + def test_inverse(self): + """Test that inverse can be constructed for a circuit with a QFTGate.""" + qc = QuantumCircuit(5) + qc.append(QFTGate(4), [1, 2, 0, 4]) + qci = qc.inverse() + self.assertEqual(Operator(qci), Operator(qc).adjoint()) + + def test_reverse_ops(self): + """Test reverse_ops works for a circuit with a QFTGate.""" + qc = QuantumCircuit(5) + qc.cx(1, 3) + qc.append(QFTGate(4), [1, 2, 0, 4]) + qc.h(0) + qcr = qc.reverse_ops() + expected = QuantumCircuit(5) + expected.h(0) + expected.append(QFTGate(4), [1, 2, 0, 4]) + expected.cx(1, 3) + self.assertEqual(qcr, expected) + + def test_conditional(self): + """Test adding conditional to a QFTGate.""" + qc = QuantumCircuit(5, 1) + qc.append(QFTGate(4), [1, 2, 0, 4]).c_if(0, 1) + self.assertIsNotNone(qc.data[0].operation.condition) + + def test_qasm(self): + """Test qasm for circuits with QFTGates.""" + qr = QuantumRegister(5, "q0") + qc = QuantumCircuit(qr) + qc.append(QFTGate(num_qubits=4), [1, 2, 0, 4]) + qc.append(QFTGate(num_qubits=3), [0, 1, 2]) + qc.h(qr[0]) + qc_qasm = dumps(qc) + reconstructed = QuantumCircuit.from_qasm_str(qc_qasm) + self.assertEqual(Operator(qc), Operator(reconstructed)) + + def test_qpy(self): + """Test qpy for circuits with QFTGates.""" + qc = QuantumCircuit(6, 1) + qc.append(QFTGate(num_qubits=4), [1, 2, 0, 4]) + qc.append(QFTGate(num_qubits=3), [0, 1, 2]) + qc.h(0) + + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circuit = load(qpy_file)[0] + self.assertEqual(qc, new_circuit) + + def test_gate_equality(self): + """Test checking equality of QFTGates.""" + self.assertEqual(QFTGate(num_qubits=3), QFTGate(num_qubits=3)) + self.assertNotEqual(QFTGate(num_qubits=3), QFTGate(num_qubits=4)) + + def test_circuit_with_gate_equality(self): + """Test checking equality of circuits with QFTGates.""" + qc1 = QuantumCircuit(5) + qc1.append(QFTGate(num_qubits=3), [1, 2, 0]) + + qc2 = QuantumCircuit(5) + qc2.append(QFTGate(num_qubits=3), [1, 2, 0]) + + qc3 = QuantumCircuit(5) + qc3.append(QFTGate(num_qubits=4), [1, 2, 0, 4]) + + self.assertEqual(qc1, qc2) + self.assertNotEqual(qc1, qc3) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_gate_definitions.py b/test/python/circuit/test_gate_definitions.py index c5df22a0e8a1..950b0c478ed8 100644 --- a/test/python/circuit/test_gate_definitions.py +++ b/test/python/circuit/test_gate_definitions.py @@ -294,6 +294,7 @@ class TestGateEquivalenceEqual(QiskitTestCase): "_DefinedGate", "_SingletonGateOverrides", "_SingletonControlledGateOverrides", + "QFTGate", } # Amazingly, Python's scoping rules for class bodies means that this is the closest we can get diff --git a/test/python/synthesis/test_qft_synthesis.py b/test/python/synthesis/test_qft_synthesis.py index d208d8c9fcd2..6cfe111ae07f 100644 --- a/test/python/synthesis/test_qft_synthesis.py +++ b/test/python/synthesis/test_qft_synthesis.py @@ -15,10 +15,10 @@ import unittest from test import combine -from ddt import ddt +from ddt import ddt, data from qiskit.circuit.library import QFT -from qiskit.synthesis.qft import synth_qft_line +from qiskit.synthesis.qft import synth_qft_line, synth_qft_full from qiskit.quantum_info import Operator from qiskit.synthesis.linear.linear_circuits_utils import check_lnn_connectivity from test import QiskitTestCase # pylint: disable=wrong-import-order @@ -57,5 +57,52 @@ def test_qft_lnn_approximated(self, num_qubits, do_swaps, approximation_degree): self.assertTrue(check_lnn_connectivity(qft_lnn)) +@ddt +class TestQFTFull(QiskitTestCase): + """Tests for QFT synthesis using all-to-all connectivity.""" + + @data(2, 3, 4, 5, 6) + def test_synthesis_default(self, num_qubits): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits) + synthesized = synth_qft_full(num_qubits) + self.assertEqual(Operator(original), Operator(synthesized)) + + @combine(num_qubits=[5, 6, 7, 8], do_swaps=[False, True]) + def test_synthesis_do_swaps(self, num_qubits, do_swaps): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits, do_swaps=do_swaps) + synthesized = synth_qft_full(num_qubits, do_swaps=do_swaps) + self.assertEqual(Operator(original), Operator(synthesized)) + + @combine(num_qubits=[5, 6, 7, 8], approximation_degree=[0, 1, 2, 3]) + def test_synthesis_arpproximate(self, num_qubits, approximation_degree): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits, approximation_degree=approximation_degree) + synthesized = synth_qft_full(num_qubits, approximation_degree=approximation_degree) + self.assertEqual(Operator(original), Operator(synthesized)) + + @combine(num_qubits=[5, 6, 7, 8], inverse=[False, True]) + def test_synthesis_inverse(self, num_qubits, inverse): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits, inverse=inverse) + synthesized = synth_qft_full(num_qubits, inverse=inverse) + self.assertEqual(Operator(original), Operator(synthesized)) + + @combine(num_qubits=[5, 6, 7, 8], insert_barriers=[False, True]) + def test_synthesis_insert_barriers(self, num_qubits, insert_barriers): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits, insert_barriers=insert_barriers) + synthesized = synth_qft_full(num_qubits, insert_barriers=insert_barriers) + self.assertEqual(Operator(original), Operator(synthesized)) + + @data(5, 6, 7, 8) + def test_synthesis_name(self, num_qubits): + """Assert that the original and synthesized QFT circuits are the same.""" + original = QFT(num_qubits, name="SomeRandomName") + synthesized = synth_qft_full(num_qubits, name="SomeRandomName") + self.assertEqual(original.name, synthesized.name) + + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_high_level_synthesis.py b/test/python/transpiler/test_high_level_synthesis.py index a76ab08d90e1..fd6ae6a01cda 100644 --- a/test/python/transpiler/test_high_level_synthesis.py +++ b/test/python/transpiler/test_high_level_synthesis.py @@ -16,6 +16,7 @@ import itertools import unittest.mock import numpy as np +from ddt import ddt, data from qiskit.circuit import ( QuantumCircuit, @@ -40,19 +41,21 @@ UGate, CU3Gate, CU1Gate, + QFTGate, ) from qiskit.circuit.library.generalized_gates import LinearFunction from qiskit.quantum_info import Clifford from qiskit.synthesis.linear import random_invertible_binary_matrix -from qiskit.transpiler.passes.synthesis.plugin import ( - HighLevelSynthesisPlugin, - HighLevelSynthesisPluginManager, -) from qiskit.compiler import transpile from qiskit.exceptions import QiskitError from qiskit.converters import dag_to_circuit, circuit_to_dag, circuit_to_instruction from qiskit.transpiler import PassManager, TranspilerError, CouplingMap, Target from qiskit.transpiler.passes.basis import BasisTranslator +from qiskit.transpiler.passes.synthesis.plugin import ( + HighLevelSynthesisPlugin, + HighLevelSynthesisPluginManager, + high_level_synthesis_plugin_names, +) from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig from qiskit.circuit.annotated_operation import ( AnnotatedOperation, @@ -2098,5 +2101,41 @@ def test_leave_store_alone_with_target(self): self.assertEqual(pass_(qc), expected) +@ddt +class TestQFTSynthesisPlugins(QiskitTestCase): + """Tests related to plugins for QFTGate.""" + + def test_supported_names(self): + """Test that there is a default synthesis plugin for QFTGates.""" + supported_plugin_names = high_level_synthesis_plugin_names("qft") + self.assertIn("default", supported_plugin_names) + + @data("line", "full") + def test_qft_plugins_qft(self, qft_plugin_name): + """Test QFTSynthesisLine plugin for circuits with QFTGates.""" + qc = QuantumCircuit(4) + qc.append(QFTGate(3), [0, 1, 2]) + qc.cx(1, 3) + qc.append(QFTGate(3).inverse(), [0, 1, 2]) + hls_config = HLSConfig(qft=[qft_plugin_name]) + basis_gates = ["cx", "u"] + qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates) + self.assertEqual(Operator(qc), Operator(qct)) + ops = set(qct.count_ops().keys()) + self.assertEqual(ops, {"u", "cx"}) + + @data("line", "full") + def test_qft_line_plugin_annotated_qft(self, qft_plugin_name): + """Test QFTSynthesisLine plugin for circuits with annotated QFTGates.""" + qc = QuantumCircuit(4) + qc.append(QFTGate(3).inverse(annotated=True).control(annotated=True), [0, 1, 2, 3]) + hls_config = HLSConfig(qft=[qft_plugin_name]) + basis_gates = ["cx", "u"] + qct = transpile(qc, hls_config=hls_config, basis_gates=basis_gates) + self.assertEqual(Operator(qc), Operator(qct)) + ops = set(qct.count_ops().keys()) + self.assertEqual(ops, {"u", "cx"}) + + if __name__ == "__main__": unittest.main() From 1e8205e43d72cf2a186a8013072aa6b2c5cd1196 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Jul 2024 09:48:08 -0400 Subject: [PATCH 2/8] Avoid Python op creation in BasisTranslator (#12705) This commit updates the BasisTranslator transpiler pass. It builds off of #12692 and #12701 to adjust access patterns in the python transpiler path to avoid eagerly creating a Python space operation object. The goal of this PR is to mitigate the performance regression introduced by the extra conversion cost of #12459 on the BasisTranslator. --- crates/circuit/src/dag_node.rs | 5 ++ .../passes/basis/basis_translator.py | 61 +++++++++++-------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/crates/circuit/src/dag_node.rs b/crates/circuit/src/dag_node.rs index 55a40c83dc39..f347ec72c811 100644 --- a/crates/circuit/src/dag_node.rs +++ b/crates/circuit/src/dag_node.rs @@ -270,6 +270,11 @@ impl DAGOpNode { self.instruction.params.to_object(py) } + #[setter] + fn set_params(&mut self, val: smallvec::SmallVec<[crate::operations::Param; 3]>) { + self.instruction.params = val; + } + pub fn is_parameterized(&self) -> bool { self.instruction.is_parameterized() } diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index f2e752dd94f5..e69887a3b940 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -30,11 +30,13 @@ QuantumCircuit, ParameterExpression, ) -from qiskit.dagcircuit import DAGCircuit +from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit.circuit.equivalence import Key, NodeData from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES +from qiskit._accelerate.circuit import StandardGate logger = logging.getLogger(__name__) @@ -253,7 +255,7 @@ def apply_translation(dag, wire_map): node_qargs = tuple(wire_map[bit] for bit in node.qargs) qubit_set = frozenset(node_qargs) if node.name in target_basis or len(node.qargs) < self._min_qubits: - if isinstance(node.op, ControlFlowOp): + if node.name in CONTROL_FLOW_OP_NAMES: flow_blocks = [] for block in node.op.blocks: dag_block = circuit_to_dag(block) @@ -281,7 +283,7 @@ def apply_translation(dag, wire_map): continue if qubit_set in extra_instr_map: self._replace_node(dag, node, extra_instr_map[qubit_set]) - elif (node.op.name, node.op.num_qubits) in instr_map: + elif (node.name, node.num_qubits) in instr_map: self._replace_node(dag, node, instr_map) else: raise TranspilerError(f"BasisTranslator did not map {node.name}.") @@ -298,20 +300,29 @@ def apply_translation(dag, wire_map): return dag def _replace_node(self, dag, node, instr_map): - target_params, target_dag = instr_map[node.op.name, node.op.num_qubits] - if len(node.op.params) != len(target_params): + target_params, target_dag = instr_map[node.name, node.num_qubits] + if len(node.params) != len(target_params): raise TranspilerError( "Translation num_params not equal to op num_params." - f"Op: {node.op.params} {node.op.name} Translation: {target_params}\n{target_dag}" + f"Op: {node.params} {node.name} Translation: {target_params}\n{target_dag}" ) - if node.op.params: - parameter_map = dict(zip(target_params, node.op.params)) + if node.params: + parameter_map = dict(zip(target_params, node.params)) bound_target_dag = target_dag.copy_empty_like() for inner_node in target_dag.topological_op_nodes(): - if any(isinstance(x, ParameterExpression) for x in inner_node.op.params): + new_op = inner_node._raw_op + if not isinstance(inner_node._raw_op, StandardGate): new_op = inner_node.op.copy() + new_node = DAGOpNode( + new_op, + qargs=inner_node.qargs, + cargs=inner_node.cargs, + params=inner_node.params, + dag=bound_target_dag, + ) + if any(isinstance(x, ParameterExpression) for x in inner_node.params): new_params = [] - for param in new_op.params: + for param in new_node.params: if not isinstance(param, ParameterExpression): new_params.append(param) else: @@ -325,10 +336,10 @@ def _replace_node(self, dag, node, instr_map): if not new_value.parameters: new_value = new_value.numeric() new_params.append(new_value) - new_op.params = new_params - else: - new_op = inner_node.op - bound_target_dag.apply_operation_back(new_op, inner_node.qargs, inner_node.cargs) + new_node.params = new_params + if not isinstance(new_op, StandardGate): + new_op.params = new_params + bound_target_dag._apply_op_node_back(new_node) if isinstance(target_dag.global_phase, ParameterExpression): old_phase = target_dag.global_phase bind_dict = {x: parameter_map[x] for x in old_phase.parameters} @@ -353,7 +364,7 @@ def _replace_node(self, dag, node, instr_map): dag_op = bound_target_dag.op_nodes()[0].op # dag_op may be the same instance as other ops in the dag, # so if there is a condition, need to copy - if getattr(node.op, "condition", None): + if getattr(node, "condition", None): dag_op = dag_op.copy() dag.substitute_node(node, dag_op, inplace=True) @@ -370,8 +381,8 @@ def _extract_basis(self, circuit): def _(self, dag: DAGCircuit): for node in dag.op_nodes(): if not dag.has_calibration_for(node) and len(node.qargs) >= self._min_qubits: - yield (node.name, node.op.num_qubits) - if isinstance(node.op, ControlFlowOp): + yield (node.name, node.num_qubits) + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: yield from self._extract_basis(block) @@ -412,10 +423,10 @@ def _extract_basis_target( frozenset(qargs).issuperset(incomplete_qargs) for incomplete_qargs in self._qargs_with_non_global_operation ): - qargs_local_source_basis[frozenset(qargs)].add((node.name, node.op.num_qubits)) + qargs_local_source_basis[frozenset(qargs)].add((node.name, node.num_qubits)) else: - source_basis.add((node.name, node.op.num_qubits)) - if isinstance(node.op, ControlFlowOp): + source_basis.add((node.name, node.num_qubits)) + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: block_dag = circuit_to_dag(block) source_basis, qargs_local_source_basis = self._extract_basis_target( @@ -628,7 +639,7 @@ def _compose_transforms(basis_transforms, source_basis, source_dag): doomed_nodes = [ node for node in dag.op_nodes() - if (node.op.name, node.op.num_qubits) == (gate_name, gate_num_qubits) + if (node.name, node.num_qubits) == (gate_name, gate_num_qubits) ] if doomed_nodes and logger.isEnabledFor(logging.DEBUG): @@ -642,9 +653,7 @@ def _compose_transforms(basis_transforms, source_basis, source_dag): for node in doomed_nodes: - replacement = equiv.assign_parameters( - dict(zip_longest(equiv_params, node.op.params)) - ) + replacement = equiv.assign_parameters(dict(zip_longest(equiv_params, node.params))) replacement_dag = circuit_to_dag(replacement) @@ -666,8 +675,8 @@ def _get_example_gates(source_dag): def recurse(dag, example_gates=None): example_gates = example_gates or {} for node in dag.op_nodes(): - example_gates[(node.op.name, node.op.num_qubits)] = node.op - if isinstance(node.op, ControlFlowOp): + example_gates[(node.name, node.num_qubits)] = node + if node.name in CONTROL_FLOW_OP_NAMES: for block in node.op.blocks: example_gates = recurse(circuit_to_dag(block), example_gates) return example_gates From 61805c8e166800687aa64ef3bf8d7da7540f5f2f Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 11 Jul 2024 12:36:14 +0200 Subject: [PATCH 3/8] Some formatting issues in Pauli docs (#12753) * some formatting issues in Pauli docs * Update qiskit/quantum_info/operators/symplectic/pauli.py Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- .../operators/symplectic/pauli.py | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index 867867eeb98a..44ca095f8ed6 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -77,7 +77,7 @@ class Pauli(BasePauli): An :math:`n`-qubit Pauli may be represented by a string consisting of :math:`n` characters from ``['I', 'X', 'Y', 'Z']``, and optionally phase - coefficient in :math:`['', '-i', '-', 'i']`. For example: ``XYZ`` or + coefficient in ``['', '-i', '-', 'i']``. For example: ``'XYZ'`` or ``'-iZIZ'``. In the string representation qubit-0 corresponds to the right-most @@ -160,21 +160,23 @@ class initialization (``Pauli('-iXYZ')``). A ``Pauli`` object can be _CANONICAL_PHASE_LABEL = {"": 0, "-i": 1, "-": 2, "i": 3} def __init__(self, data: str | tuple | Pauli | ScalarOp | QuantumCircuit | None = None): - """Initialize the Pauli. + r"""Initialize the Pauli. When using the symplectic array input data both z and x arguments must be provided, however the first (z) argument can be used alone for string - label, Pauli operator, or ScalarOp input data. + label, Pauli operator, or :class:`.ScalarOp` input data. Args: data (str or tuple or Pauli or ScalarOp): input data for Pauli. If input is - a tuple it must be of the form ``(z, x)`` or (z, x, phase)`` where - ``z`` and ``x`` are boolean Numpy arrays, and phase is an integer from Z_4. + a tuple it must be of the form ``(z, x)`` or ``(z, x, phase)`` where + ``z`` and ``x`` are boolean Numpy arrays, and phase is an integer from + :math:`\mathbb{Z}_4`. If input is a string, it must be a concatenation of a phase and a Pauli string - (e.g. 'XYZ', '-iZIZ') where a phase string is a combination of at most three - characters from ['+', '-', ''], ['1', ''], and ['i', 'j', ''] in this order, - e.g. '', '-1j' while a Pauli string is 1 or more characters of 'I', 'X', 'Y' or 'Z', - e.g. 'Z', 'XIYY'. + (e.g. ``'XYZ', '-iZIZ'``) where a phase string is a combination of at most three + characters from ``['+', '-', '']``, ``['1', '']``, and ``['i', 'j', '']`` in this order, + e.g. ``''``, ``'-1j'`` while a Pauli string is 1 or more + characters of ``'I'``, ``'X'``, ``'Y'``, or ``'Z'``, + e.g. ``'Z'``, ``'XIYY'``. Raises: QiskitError: if input array is invalid shape. From 4c9ca6eddaec717b85a9b49798c2fe3f09a44fbc Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 11 Jul 2024 13:27:56 +0200 Subject: [PATCH 4/8] Randomized errors are failing because Aer uses deprecated functionality (#12722) --- test/randomized/test_transpiler_equivalence.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/randomized/test_transpiler_equivalence.py b/test/randomized/test_transpiler_equivalence.py index 3bd09d89348b..76bbf9ef08e3 100644 --- a/test/randomized/test_transpiler_equivalence.py +++ b/test/randomized/test_transpiler_equivalence.py @@ -48,6 +48,8 @@ """ import os +import warnings + from test.utils.base import dicts_almost_equal from math import pi @@ -292,7 +294,16 @@ def equivalent_transpile(self, kwargs): # Note that there's no transpilation here, which is why the gates are limited to only ones # that Aer supports natively. - aer_counts = self.backend.run(self.qc, shots=shots).result().get_counts() + with warnings.catch_warnings(): + # Safe to remove once https://github.com/Qiskit/qiskit-aer/pull/2179 is in a release version + # of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + module="qiskit_aer", + message="Treating CircuitInstruction as an iterable", + ) + aer_counts = self.backend.run(self.qc, shots=shots).result().get_counts() try: xpiled_qc = transpile(self.qc, **kwargs) From 99ae318d89337fdc04673b47435680141ecfc06a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 11 Jul 2024 07:58:47 -0400 Subject: [PATCH 5/8] Use rustworkx 0.15.0 features in DAGCircuit.remove_op_node (#12756) This commit updates the minimum rustworkx version to 0.15.0 to pull in the new PyDiGraph.remove_node_retain_edges_by_id() method introduced in that release. This new function is used for the DAGCircuit.remove_op_node() method instead of the PyDiGraph.remove_node_retain_edges() function. This new method has much better scaling characteristics and should improve the performance characteristics of removing very wide operations from a DAGCircuit. Fixes #11677 Part of #12156 --- qiskit/dagcircuit/dagcircuit.py | 4 +--- .../update-rustworkx-min-version-4f07aacfebccae80.yaml | 7 +++++++ requirements.txt | 2 +- 3 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 5d6739d72198..796e0bc2b700 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1986,9 +1986,7 @@ def remove_op_node(self, node): "node type was wrongly provided." ) - self._multi_graph.remove_node_retain_edges( - node._node_id, use_outgoing=False, condition=lambda edge1, edge2: edge1 == edge2 - ) + self._multi_graph.remove_node_retain_edges_by_id(node._node_id) self._decrement_op(node.name) def remove_ancestors_of(self, node): diff --git a/releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml b/releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml new file mode 100644 index 000000000000..7661f82aee8b --- /dev/null +++ b/releasenotes/notes/update-rustworkx-min-version-4f07aacfebccae80.yaml @@ -0,0 +1,7 @@ +--- +upgrade_misc: + - | + The minimum version of rustworkx required to run this release has been + increased from 0.14.0 to 0.15.0. This is required because Qiskit is now + using new functionality added in the rustworkx 0.15.0 release which + improves performance. diff --git a/requirements.txt b/requirements.txt index 539f9587994d..2dd5e49e2b3d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -rustworkx>=0.14.0 +rustworkx>=0.15.0 numpy>=1.17,<3 scipy>=1.5 sympy>=1.3 From a306cb67c8fbaab5e3329fa40613d22980aaea29 Mon Sep 17 00:00:00 2001 From: Sebastian Brandhofer <148463728+sbrandhsn@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:40:01 +0200 Subject: [PATCH 6/8] Barebone generic backend options (#12747) * Update generic_backend_v2.py * reno * Update barebone-backend-option-675c86df4382a443.yaml * ... * suggestions from code review * Update barebone-backend-option-675c86df4382a443.yaml * tests, typo --- .../fake_provider/generic_backend_v2.py | 63 +++++++++++++------ ...ebone-backend-option-675c86df4382a443.yaml | 8 +++ .../fake_provider/test_generic_backend_v2.py | 20 ++++++ 3 files changed, 72 insertions(+), 19 deletions(-) create mode 100644 releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml diff --git a/qiskit/providers/fake_provider/generic_backend_v2.py b/qiskit/providers/fake_provider/generic_backend_v2.py index 214754080e57..0da1df7eab65 100644 --- a/qiskit/providers/fake_provider/generic_backend_v2.py +++ b/qiskit/providers/fake_provider/generic_backend_v2.py @@ -112,6 +112,8 @@ def __init__( calibrate_instructions: bool | InstructionScheduleMap | None = None, dtm: float | None = None, seed: int | None = None, + pulse_channels: bool = True, + noise_info: bool = True, ): """ Args: @@ -159,6 +161,10 @@ def __init__( None by default. seed: Optional seed for generation of default values. + + pulse_channels: If true, sets default pulse channel information on the backend. + + noise_info: If true, associates gates and qubits with default noise information. """ super().__init__( @@ -175,6 +181,10 @@ def __init__( self._control_flow = control_flow self._calibrate_instructions = calibrate_instructions self._supported_gates = get_standard_gate_name_mapping() + self._noise_info = noise_info + + if calibrate_instructions and not noise_info: + raise QiskitError("Must set parameter noise_info when calibrating instructions.") if coupling_map is None: self._coupling_map = CouplingMap().from_full(num_qubits) @@ -198,7 +208,10 @@ def __init__( self._basis_gates.append(name) self._build_generic_target() - self._build_default_channels() + if pulse_channels: + self._build_default_channels() + else: + self.channels_map = {} @property def target(self): @@ -340,22 +353,31 @@ def _build_generic_target(self): """ # the qubit properties are sampled from default ranges properties = _QUBIT_PROPERTIES - self._target = Target( - description=f"Generic Target with {self._num_qubits} qubits", - num_qubits=self._num_qubits, - dt=properties["dt"], - qubit_properties=[ - QubitProperties( - t1=self._rng.uniform(properties["t1"][0], properties["t1"][1]), - t2=self._rng.uniform(properties["t2"][0], properties["t2"][1]), - frequency=self._rng.uniform( - properties["frequency"][0], properties["frequency"][1] - ), - ) - for _ in range(self._num_qubits) - ], - concurrent_measurements=[list(range(self._num_qubits))], - ) + if not self._noise_info: + self._target = Target( + description=f"Generic Target with {self._num_qubits} qubits", + num_qubits=self._num_qubits, + dt=properties["dt"], + qubit_properties=None, + concurrent_measurements=[list(range(self._num_qubits))], + ) + else: + self._target = Target( + description=f"Generic Target with {self._num_qubits} qubits", + num_qubits=self._num_qubits, + dt=properties["dt"], + qubit_properties=[ + QubitProperties( + t1=self._rng.uniform(properties["t1"][0], properties["t1"][1]), + t2=self._rng.uniform(properties["t2"][0], properties["t2"][1]), + frequency=self._rng.uniform( + properties["frequency"][0], properties["frequency"][1] + ), + ) + for _ in range(self._num_qubits) + ], + concurrent_measurements=[list(range(self._num_qubits))], + ) # Generate instruction schedule map with calibrations to add to target. calibration_inst_map = None @@ -380,8 +402,11 @@ def _build_generic_target(self): f"Provided basis gate {name} needs more qubits than {self.num_qubits}, " f"which is the size of the backend." ) - noise_params = self._get_noise_defaults(name, gate.num_qubits) - self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) + if self._noise_info: + noise_params = self._get_noise_defaults(name, gate.num_qubits) + self._add_noisy_instruction_to_target(gate, noise_params, calibration_inst_map) + else: + self._target.add_instruction(gate, properties=None, name=name) if self._control_flow: self._target.add_instruction(IfElseOp, name="if_else") diff --git a/releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml b/releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml new file mode 100644 index 000000000000..912970845042 --- /dev/null +++ b/releasenotes/notes/barebone-backend-option-675c86df4382a443.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added two parameters to :class:`.GenericBackendV2` to exclude error (`noise_info`) and + pulse channel information (`pulse_channels`) from the construction of the backend. These parameters + are true by default, replicating the initial default behavior of the constructor. A memory-sensitive + user may set these options to `False` to reduce the memory overhead by 40x when transpiling on large- + scale :class:`.GenericBackendV2`. diff --git a/test/python/providers/fake_provider/test_generic_backend_v2.py b/test/python/providers/fake_provider/test_generic_backend_v2.py index cd7c611b2212..33bf57cf3903 100644 --- a/test/python/providers/fake_provider/test_generic_backend_v2.py +++ b/test/python/providers/fake_provider/test_generic_backend_v2.py @@ -45,6 +45,26 @@ def test_ccx_2Q(self): with self.assertRaises(QiskitError): GenericBackendV2(num_qubits=2, basis_gates=["ccx", "id"]) + def test_calibration_no_noise_info(self): + """Test failing with a backend with calibration and no noise info""" + with self.assertRaises(QiskitError): + GenericBackendV2( + num_qubits=2, + basis_gates=["ccx", "id"], + calibrate_instructions=True, + noise_info=False, + ) + + def test_no_noise(self): + """Test no noise info when parameter is false""" + backend = GenericBackendV2(num_qubits=2, noise_info=False) + self.assertEqual(backend.target.qubit_properties, None) + + def test_no_pulse_channels(self): + """Test no/empty pulse channels when parameter is false""" + backend = GenericBackendV2(num_qubits=2, pulse_channels=False) + self.assertTrue(len(backend.channels_map) == 0) + def test_operation_names(self): """Test that target basis gates include "delay", "measure" and "reset" even if not provided by user.""" From 41267ecf5d20bf022a8d4fcb1580a88f56d1843c Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Thu, 11 Jul 2024 17:10:27 +0300 Subject: [PATCH 7/8] Add clifford gates to collect_cliffords (#12750) * add clifford gates to collect_cliffords * replace hard coded clifford names by clifford_circuit names * move import * replace hard coded clifford names in random_clifford_circuit * add release notes * add permutation to collect_clifford gate list --- qiskit/circuit/random/utils.py | 6 ++++-- .../passes/optimization/collect_cliffords.py | 21 ++++++------------- ...fix-collect-clifford-83af26d98b8c69e8.yaml | 6 ++++++ 3 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml diff --git a/qiskit/circuit/random/utils.py b/qiskit/circuit/random/utils.py index 3bcdbeaef4ac..5caba8ca2ae0 100644 --- a/qiskit/circuit/random/utils.py +++ b/qiskit/circuit/random/utils.py @@ -18,6 +18,7 @@ from qiskit.circuit import Reset from qiskit.circuit.library import standard_gates from qiskit.circuit.exceptions import CircuitError +from qiskit.quantum_info.operators.symplectic.clifford_circuits import _BASIS_1Q, _BASIS_2Q def random_circuit( @@ -312,8 +313,9 @@ def random_clifford_circuit(num_qubits, num_gates, gates="all", seed=None): QuantumCircuit: constructed circuit """ - gates_1q = ["i", "x", "y", "z", "h", "s", "sdg", "sx", "sxdg"] - gates_2q = ["cx", "cz", "cy", "swap", "iswap", "ecr", "dcx"] + gates_1q = list(set(_BASIS_1Q.keys()) - {"v", "w", "id", "iden", "sinv"}) + gates_2q = list(_BASIS_2Q.keys()) + if gates == "all": if num_qubits == 1: gates = gates_1q diff --git a/qiskit/transpiler/passes/optimization/collect_cliffords.py b/qiskit/transpiler/passes/optimization/collect_cliffords.py index c0e9641923cd..40acd21c6855 100644 --- a/qiskit/transpiler/passes/optimization/collect_cliffords.py +++ b/qiskit/transpiler/passes/optimization/collect_cliffords.py @@ -22,6 +22,7 @@ ) from qiskit.quantum_info.operators import Clifford +from qiskit.quantum_info.operators.symplectic.clifford_circuits import _BASIS_1Q, _BASIS_2Q class CollectCliffords(CollectAndCollapse): @@ -69,21 +70,11 @@ def __init__( ) -clifford_gate_names = [ - "x", - "y", - "z", - "h", - "s", - "sdg", - "cx", - "cy", - "cz", - "swap", - "clifford", - "linear_function", - "pauli", -] +clifford_gate_names = ( + list(_BASIS_1Q.keys()) + + list(_BASIS_2Q.keys()) + + ["clifford", "linear_function", "pauli", "permutation"] +) def _is_clifford_gate(node): diff --git a/releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml b/releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml new file mode 100644 index 000000000000..48eac19acc9d --- /dev/null +++ b/releasenotes/notes/fix-collect-clifford-83af26d98b8c69e8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Add more Clifford gates to the :class:`.CollectCliffords()` transpiler pass. + In particular, we have added the gates :class:`ECRGate()`, :class:`DCXGate()`, + :class:`iSWAPGate()`, :class:`SXGate()` and :class:`SXdgGate()` to this transpiler pass. From 99032fc042b5f4f57956f451578c3476a672ddb9 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 11 Jul 2024 17:40:08 +0200 Subject: [PATCH 8/8] Remove some of the entries in allow_DeprecationWarning (#12721) * remove some of the entries * fixing obscure expcetion handleing for comparison * remove all the modules * ignore "Treating CircuitInstruction as an iterable is deprecated" in Aer * remove allow_DeprecationWarning_module and revert ignore/default * revert --- qiskit/circuit/instruction.py | 7 ++++--- test/utils/base.py | 22 ---------------------- 2 files changed, 4 insertions(+), 25 deletions(-) diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 1b5fca3f738b..4bebf812e185 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -188,11 +188,12 @@ def __eq__(self, other): return False for self_param, other_param in zip_longest(self.params, other.params): - try: + if isinstance(self_param, numpy.ndarray): + if numpy.array_equal(self_param, other_param): + continue + else: if self_param == other_param: continue - except ValueError: - pass try: self_asarray = numpy.asarray(self_param) diff --git a/test/utils/base.py b/test/utils/base.py index e4dbd7f67072..0b02f3e58488 100644 --- a/test/utils/base.py +++ b/test/utils/base.py @@ -129,30 +129,8 @@ def setUpClass(cls): module=r"qiskit_aer(\.[a-zA-Z0-9_]+)*", ) - allow_DeprecationWarning_modules = [ - "test.python.pulse.test_builder", - "test.python.pulse.test_block", - "test.python.quantum_info.operators.symplectic.test_legacy_pauli", - "qiskit.quantum_info.operators.pauli", - "pybobyqa", - "numba", - "qiskit.utils.measurement_error_mitigation", - "qiskit.circuit.library.standard_gates.x", - "qiskit.pulse.schedule", - "qiskit.pulse.instructions.instruction", - "qiskit.pulse.instructions.play", - "qiskit.pulse.library.parametric_pulses", - "qiskit.quantum_info.operators.symplectic.pauli", - ] - for mod in allow_DeprecationWarning_modules: - warnings.filterwarnings("default", category=DeprecationWarning, module=mod) allow_DeprecationWarning_message = [ - r"elementwise comparison failed.*", - r"The jsonschema validation included in qiskit-terra.*", - r"The DerivativeBase.parameter_expression_grad method.*", r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", - # Caused by internal scikit-learn scipy usage - r"The 'sym_pos' keyword is deprecated and should be replaced by using", ] for msg in allow_DeprecationWarning_message: warnings.filterwarnings("default", category=DeprecationWarning, message=msg)