diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index e020bf63e47..203f52582d3 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -345,6 +345,8 @@ .. autofunction:: unitary_overlap .. autofunction:: phase_estimation +.. autofunction:: grover_operator + N-local circuits ================ @@ -596,6 +598,6 @@ from .hidden_linear_function import HiddenLinearFunction, hidden_linear_function from .iqp import IQP, iqp, random_iqp from .phase_estimation import PhaseEstimation, phase_estimation -from .grover_operator import GroverOperator +from .grover_operator import GroverOperator, grover_operator from .phase_oracle import PhaseOracle from .overlap import UnitaryOverlap, unitary_overlap diff --git a/qiskit/circuit/library/grover_operator.py b/qiskit/circuit/library/grover_operator.py index d40deefdf67..d55a59249a3 100644 --- a/qiskit/circuit/library/grover_operator.py +++ b/qiskit/circuit/library/grover_operator.py @@ -16,10 +16,266 @@ from typing import List, Optional, Union import numpy -from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister +from qiskit.circuit import QuantumCircuit, QuantumRegister, AncillaRegister, AncillaQubit from qiskit.exceptions import QiskitError from qiskit.quantum_info import Statevector, Operator, DensityMatrix +from qiskit.utils.deprecation import deprecate_func from .standard_gates import MCXGate +from .generalized_gates import DiagonalGate + + +def grover_operator( + oracle: QuantumCircuit | Statevector, + state_preparation: QuantumCircuit | None = None, + zero_reflection: QuantumCircuit | DensityMatrix | Operator | None = None, + reflection_qubits: list[int] | None = None, + insert_barriers: bool = False, + name: str = "Q", +): + r"""Construct the Grover operator. + + Grover's search algorithm [1, 2] consists of repeated applications of the so-called + Grover operator used to amplify the amplitudes of the desired output states. + This operator, :math:`\mathcal{Q}`, consists of the phase oracle, :math:`\mathcal{S}_f`, + zero phase-shift or zero reflection, :math:`\mathcal{S}_0`, and an + input state preparation :math:`\mathcal{A}`: + + .. math:: + \mathcal{Q} = \mathcal{A} \mathcal{S}_0 \mathcal{A}^\dagger \mathcal{S}_f + + In the standard Grover search we have :math:`\mathcal{A} = H^{\otimes n}`: + + .. math:: + \mathcal{Q} = H^{\otimes n} \mathcal{S}_0 H^{\otimes n} \mathcal{S}_f + = D \mathcal{S_f} + + The operation :math:`D = H^{\otimes n} \mathcal{S}_0 H^{\otimes n}` is also referred to as + diffusion operator. In this formulation we can see that Grover's operator consists of two + steps: first, the phase oracle multiplies the good states by -1 (with :math:`\mathcal{S}_f`) + and then the whole state is reflected around the mean (with :math:`D`). + + This class allows setting a different state preparation, as in quantum amplitude + amplification (a generalization of Grover's algorithm), :math:`\mathcal{A}` might not be + a layer of Hardamard gates [3]. + + The action of the phase oracle :math:`\mathcal{S}_f` is defined as + + .. math:: + \mathcal{S}_f: |x\rangle \mapsto (-1)^{f(x)}|x\rangle + + where :math:`f(x) = 1` if :math:`x` is a good state and 0 otherwise. To highlight the fact + that this oracle flips the phase of the good states and does not flip the state of a result + qubit, we call :math:`\mathcal{S}_f` a phase oracle. + + Note that you can easily construct a phase oracle from a bitflip oracle by sandwiching the + controlled X gate on the result qubit by a X and H gate. For instance + + .. parsed-literal:: + + Bitflip oracle Phaseflip oracle + q_0: ──■── q_0: ────────────■──────────── + ┌─┴─┐ ┌───┐┌───┐┌─┴─┐┌───┐┌───┐ + out: ┤ X ├ out: ┤ X ├┤ H ├┤ X ├┤ H ├┤ X ├ + └───┘ └───┘└───┘└───┘└───┘└───┘ + + There is some flexibility in defining the oracle and :math:`\mathcal{A}` operator. Before the + Grover operator is applied in Grover's algorithm, the qubits are first prepared with one + application of the :math:`\mathcal{A}` operator (or Hadamard gates in the standard formulation). + Thus, we always have operation of the form + :math:`\mathcal{A} \mathcal{S}_f \mathcal{A}^\dagger`. Therefore it is possible to move + bitflip logic into :math:`\mathcal{A}` and leaving the oracle only to do phaseflips via Z gates + based on the bitflips. One possible use-case for this are oracles that do not uncompute the + state qubits. + + The zero reflection :math:`\mathcal{S}_0` is usually defined as + + .. math:: + \mathcal{S}_0 = 2 |0\rangle^{\otimes n} \langle 0|^{\otimes n} - \mathbb{I}_n + + where :math:`\mathbb{I}_n` is the identity on :math:`n` qubits. + By default, this class implements the negative version + :math:`2 |0\rangle^{\otimes n} \langle 0|^{\otimes n} - \mathbb{I}_n`, since this can simply + be implemented with a multi-controlled Z sandwiched by X gates on the target qubit and the + introduced global phase does not matter for Grover's algorithm. + + Examples: + + We can construct a Grover operator just from the phase oracle: + + .. plot:: + :include-source: + :context: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(2) + oracle.z(0) # good state = first qubit is |1> + grover_op = grover_operator(oracle, insert_barriers=True) + grover_op.draw("mpl") + + We can also modify the state preparation: + + .. plot:: + :include-source: + :context: + + oracle = QuantumCircuit(1) + oracle.z(0) # the qubit state |1> is the good state + state_preparation = QuantumCircuit(1) + state_preparation.ry(0.2, 0) # non-uniform state preparation + grover_op = grover_operator(oracle, state_preparation) + grover_op.draw("mpl") + + In addition, we can also mark which qubits the zero reflection should act on. This + is useful in case that some qubits are just used as scratch space but should not affect + the oracle: + + .. plot:: + :include-source: + :context: + + oracle = QuantumCircuit(4) + oracle.z(3) + reflection_qubits = [0, 3] + state_preparation = QuantumCircuit(4) + state_preparation.cry(0.1, 0, 3) + state_preparation.ry(0.5, 3) + grover_op = grover_operator(oracle, state_preparation, reflection_qubits=reflection_qubits) + grover_op.draw("mpl") + + + The oracle and the zero reflection can also be passed as :mod:`qiskit.quantum_info` + objects: + + .. plot:: + :include-source: + :context: + + from qiskit.quantum_info import Statevector, DensityMatrix, Operator + + mark_state = Statevector.from_label("011") + reflection = 2 * DensityMatrix.from_label("000") - Operator.from_label("III") + grover_op = grover_operator(oracle=mark_state, zero_reflection=reflection) + grover_op.draw("mpl") + + For a large number of qubits, the multi-controlled X gate used for the zero-reflection + can be synthesized in different fashions. Depending on the number of available qubits, + the compiler will choose a different implementation: + + .. code-block:: python + + from qiskit import transpile, Qubit + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(10) + oracle.z(oracle.qubits) + grover_op = grover_operator(oracle) + + # without extra qubit space, the MCX synthesis is expensive + basis_gates = ["u", "cx"] + tqc = transpile(grover_op, basis_gates=basis_gates) + is_2q = lambda inst: len(inst.qubits) == 2 + print("2q depth w/o scratch qubits:", tqc.depth(filter_function=is_2q)) # > 350 + + # add extra bits that can be used as scratch space + grover_op.add_bits([Qubit() for _ in range(num_qubits)]) + tqc = transpile(grover_op, basis_gates=basis_gates) + print("2q depth w/ scratch qubits:", tqc.depth(filter_function=is_2q)) # < 100 + + Args: + oracle: The phase oracle implementing a reflection about the bad state. Note that this + is not a bitflip oracle, see the docstring for more information. + state_preparation: The operator preparing the good and bad state. + For Grover's algorithm, this is a n-qubit Hadamard gate and for amplitude + amplification or estimation the operator :math:`\mathcal{A}`. + zero_reflection: The reflection about the zero state, :math:`\mathcal{S}_0`. + reflection_qubits: Qubits on which the zero reflection acts on. + insert_barriers: Whether barriers should be inserted between the reflections and A. + name: The name of the circuit. + + References: + [1]: L. K. Grover (1996), A fast quantum mechanical algorithm for database search, + `arXiv:quant-ph/9605043 `_. + [2]: I. Chuang & M. Nielsen, Quantum Computation and Quantum Information, + Cambridge: Cambridge University Press, 2000. Chapter 6.1.2. + [3]: Brassard, G., Hoyer, P., Mosca, M., & Tapp, A. (2000). + Quantum Amplitude Amplification and Estimation. + `arXiv:quant-ph/0005055 `_. + """ + # We inherit the ancillas/qubits structure from the oracle, if it is given as circuit. + if isinstance(oracle, QuantumCircuit): + circuit = oracle.copy_empty_like(name=name, vars_mode="drop") + else: + circuit = QuantumCircuit(oracle.num_qubits, name=name) + + # (1) Add the oracle. + # If the oracle is given as statevector, turn it into a circuit that implements the + # reflection about the state. + if isinstance(oracle, Statevector): + diagonal = DiagonalGate((-1) ** oracle.data) + circuit.append(diagonal, circuit.qubits) + else: + circuit.compose(oracle, inplace=True) + + if insert_barriers: + circuit.barrier() + + # (2) Add the inverse state preparation. + # For this we need to know the target qubits that we apply the zero reflection to. + # If the reflection qubits are not given, we assume they are the qubits that are not + # of type ``AncillaQubit`` in the oracle. + if reflection_qubits is None: + reflection_qubits = [ + i for i, qubit in enumerate(circuit.qubits) if not isinstance(qubit, AncillaQubit) + ] + + if state_preparation is None: + circuit.h(reflection_qubits) # H is self-inverse + else: + circuit.compose(state_preparation.inverse(), inplace=True) + + if insert_barriers: + circuit.barrier() + + # (3) Add the zero reflection. + if zero_reflection is None: + num_reflection = len(reflection_qubits) + + circuit.x(reflection_qubits) + if num_reflection == 1: + circuit.z( + reflection_qubits[0] + ) # MCX does not support 0 controls, hence this is separate + else: + mcx = MCXGate(num_reflection - 1) + + circuit.h(reflection_qubits[-1]) + circuit.append(mcx, reflection_qubits) + circuit.h(reflection_qubits[-1]) + circuit.x(reflection_qubits) + + elif isinstance(zero_reflection, (Operator, DensityMatrix)): + diagonal = DiagonalGate(zero_reflection.data.diagonal()) + circuit.append(diagonal, circuit.qubits) + + else: + circuit.compose(zero_reflection, inplace=True) + + if insert_barriers: + circuit.barrier() + + # (4) Add the state preparation. + if state_preparation is None: + circuit.h(reflection_qubits) + else: + circuit.compose(state_preparation, inplace=True) + + # minus sign + circuit.global_phase = numpy.pi + + return circuit class GroverOperator(QuantumCircuit): @@ -150,6 +406,13 @@ class GroverOperator(QuantumCircuit): «state_2: ┤2 ├┤1 ├┤ UCRZ(pi/4) ├┤ H ├ « └─────────────────┘└───────────────┘└────────────┘└───┘ + .. seealso:: + + The :func:`.grover_operator` implements the same functionality but keeping the + :class:`.MCXGate` abstract, such that the compiler may choose the optimal decomposition. + We recommend using :func:`.grover_operator` for performance reasons, which does not + wrap the circuit into an opaque gate. + References: [1]: L. K. Grover (1996), A fast quantum mechanical algorithm for database search, `arXiv:quant-ph/9605043 `_. @@ -160,6 +423,11 @@ class GroverOperator(QuantumCircuit): `arXiv:quant-ph/0005055 `_. """ + @deprecate_func( + since="1.3", + additional_msg="Use qiskit.circuit.library.grover_operator instead.", + pending=True, + ) def __init__( self, oracle: Union[QuantumCircuit, Statevector], diff --git a/releasenotes/notes/clib-grover-op-cb032144e899ed0d.yaml b/releasenotes/notes/clib-grover-op-cb032144e899ed0d.yaml new file mode 100644 index 00000000000..93629bb1fa9 --- /dev/null +++ b/releasenotes/notes/clib-grover-op-cb032144e899ed0d.yaml @@ -0,0 +1,19 @@ +--- +features_circuits: + - | + Added :func:`.grover_operator` to construct a Grover operator circuit, used in e.g. + Grover's algorithm and amplitude estimation/amplification. This function is similar to + :class:`.GroverOperator`, but does not require choosing the implementation of the + multi-controlled X gate a-priori and let's the compiler choose the optimal decomposition + instead. In addition to this, it does not wrap the circuit into an opaque gate and is + faster as less decompositions are required to transpile. + + Example:: + + from qiskit.circuit import QuantumCircuit + from qiskit.circuit.library import grover_operator + + oracle = QuantumCircuit(2) + oracle.z(0) # good state = first qubit is |1> + grover_op = grover_operator(oracle, insert_barriers=True) + print(grover_op.draw()) diff --git a/test/python/circuit/library/test_grover_operator.py b/test/python/circuit/library/test_grover_operator.py index 5a4f74f161c..2c4171b9620 100644 --- a/test/python/circuit/library/test_grover_operator.py +++ b/test/python/circuit/library/test_grover_operator.py @@ -13,15 +13,18 @@ """Test the grover operator.""" import unittest +from ddt import ddt, data import numpy as np -from qiskit.circuit import QuantumCircuit -from qiskit.circuit.library import GroverOperator +from qiskit import transpile +from qiskit.circuit import QuantumCircuit, Qubit, AncillaQubit +from qiskit.circuit.library import GroverOperator, grover_operator from qiskit.converters import circuit_to_dag from qiskit.quantum_info import Operator, Statevector, DensityMatrix from test import QiskitTestCase # pylint: disable=wrong-import-order +@ddt class TestGroverOperator(QiskitTestCase): """Test the Grover operator.""" @@ -43,12 +46,14 @@ def assertGroverOperatorIsCorrect(self, grover_op, oracle, state_in=None, zero_r expected = state_in.dot(zero_reflection).dot(state_in.adjoint()).dot(oracle) self.assertTrue(Operator(grover_op).equiv(expected)) - def test_grover_operator(self): + @data(True, False) + def test_grover_operator(self, use_function): """Test the base case for the Grover operator.""" + grover_constructor = grover_operator if use_function else GroverOperator with self.subTest("single Z oracle"): oracle = QuantumCircuit(3) oracle.z(2) # good state if last qubit is 1 - grover_op = GroverOperator(oracle) + grover_op = grover_constructor(oracle) self.assertGroverOperatorIsCorrect(grover_op, oracle) with self.subTest("target state x0x1"): @@ -57,19 +62,23 @@ def test_grover_operator(self): oracle.z(1) oracle.x(1) oracle.z(3) - grover_op = GroverOperator(oracle) + grover_op = grover_constructor(oracle) self.assertGroverOperatorIsCorrect(grover_op, oracle) - def test_quantum_info_input(self): + @data(True, False) + def test_quantum_info_input(self, use_function): """Test passing quantum_info.Operator and Statevector as input.""" + grover_constructor = grover_operator if use_function else GroverOperator + mark = Statevector.from_label("001") diffuse = 2 * DensityMatrix.from_label("000") - Operator.from_label("III") - grover_op = GroverOperator(oracle=mark, zero_reflection=diffuse) + grover_op = grover_constructor(oracle=mark, zero_reflection=diffuse) self.assertGroverOperatorIsCorrect( grover_op, oracle=np.diag((-1) ** mark.data), zero_reflection=diffuse.data ) - def test_stateprep_contains_instruction(self): + @data(True, False) + def test_stateprep_contains_instruction(self, use_function): """Test wrapping works if the state preparation is not unitary.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -81,18 +90,24 @@ def test_stateprep_contains_instruction(self): stateprep = QuantumCircuit(1) stateprep.append(instr, [0]) - grover_op = GroverOperator(oracle, stateprep) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, stateprep) self.assertEqual(grover_op.num_qubits, 1) - def test_reflection_qubits(self): + @data(True, False) + def test_reflection_qubits(self, use_function): """Test setting idle qubits doesn't apply any operations on these qubits.""" oracle = QuantumCircuit(4) oracle.z(3) - grover_op = GroverOperator(oracle, reflection_qubits=[0, 3]) + + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, reflection_qubits=[0, 3]) + dag = circuit_to_dag(grover_op.decompose()) self.assertEqual(set(dag.idle_wires()), {dag.qubits[1], dag.qubits[2]}) - def test_custom_state_in(self): + @data(True, False) + def test_custom_state_in(self, use_function): """Test passing a custom state_in operator.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -101,10 +116,13 @@ def test_custom_state_in(self): sampling_probability = 0.2 bernoulli.ry(2 * np.arcsin(np.sqrt(sampling_probability)), 0) - grover_op = GroverOperator(oracle, bernoulli) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, bernoulli) + self.assertGroverOperatorIsCorrect(grover_op, oracle, bernoulli) - def test_custom_zero_reflection(self): + @data(True, False) + def test_custom_zero_reflection(self, use_function): """Test passing in a custom zero reflection.""" oracle = QuantumCircuit(1) oracle.z(0) @@ -114,7 +132,8 @@ def test_custom_zero_reflection(self): zero_reflection.rz(np.pi, 0) zero_reflection.x(0) - grover_op = GroverOperator(oracle, zero_reflection=zero_reflection) + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, zero_reflection=zero_reflection) with self.subTest("zero reflection up to phase works"): self.assertGroverOperatorIsCorrect(grover_op, oracle) @@ -125,9 +144,10 @@ def test_custom_zero_reflection(self): expected.h(0) # state_in is H expected.compose(zero_reflection, inplace=True) expected.h(0) - self.assertEqual(expected, grover_op.decompose()) + self.assertEqual(expected, grover_op if use_function else grover_op.decompose()) - def test_num_mcx_ancillas(self): + @data(True, False) + def test_num_mcx_ancillas(self, use_function): """Test the number of ancilla bits for the mcx gate in zero_reflection.""" # # q_0: ──■────────────────────── @@ -152,9 +172,50 @@ def test_num_mcx_ancillas(self): oracle.ccx(4, 5, 6) oracle.h(6) oracle.x(6) - grover_op = GroverOperator(oracle, reflection_qubits=[0, 1]) + + grover_constructor = grover_operator if use_function else GroverOperator + grover_op = grover_constructor(oracle, reflection_qubits=[0, 1]) self.assertEqual(grover_op.width(), 7) + def test_mcx_allocation(self): + """The the automatic allocation of auxiliary qubits for MCX.""" + num_qubits = 10 + oracle = QuantumCircuit(num_qubits) + oracle.z(oracle.qubits) + + grover_op = grover_operator(oracle) + + # without extra qubit space, the MCX gates are synthesized without ancillas + basis_gates = ["u", "cx"] + + is_2q = lambda inst: len(inst.qubits) == 2 + + with self.subTest(msg="no auxiliaries"): + tqc = transpile(grover_op, basis_gates=basis_gates) + depth = tqc.depth(filter_function=is_2q) + self.assertLess(depth, 500) + self.assertGreater(depth, 100) + + # add extra bits that can be used as scratch space + grover_op.add_bits([Qubit() for _ in range(num_qubits)]) + with self.subTest(msg="with auxiliaries"): + tqc = transpile(grover_op, basis_gates=basis_gates) + depth = tqc.depth(filter_function=is_2q) + self.assertLess(depth, 100) + + def test_ancilla_detection(self): + """Test AncillaQubit objects are correctly identified in the oracle.""" + qubits = [AncillaQubit(), Qubit()] + oracle = QuantumCircuit() + oracle.add_bits(qubits) + oracle.z(qubits[1]) # the "good" state is qubit 1 being in state |1> + + grover_op = grover_operator(oracle) + + expected_h = 2 # would be 4 if the ancilla is not detected + + self.assertEqual(expected_h, grover_op.count_ops().get("h", 0)) + if __name__ == "__main__": unittest.main()