Source code for qrisp.algorithms.grover.grover_tools

"""********************************************************************************
* Copyright (c) 2026 the Qrisp authors
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License, v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is
* available at https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************
"""

from collections.abc import Callable, Sequence
from typing import Any

import numpy as np

from qrisp import (
    IterationEnvironment,
    QuantumArray,
    QuantumFloat,
    QuantumVariable,
    conjugate,
    control,
    gate_wrap,
    h,
    mcp,
    mcx,
    mcz,
    merge,
    p,
    recursive_qs_search,
    x,
    z,
)
from qrisp.alg_primitives.reflection import reflection
from qrisp.jasp import check_for_tracing_mode, jrange
from qrisp.typing import FloatLike


# Applies the grover diffuser onto the (sequence of) quantum variable input_object
[docs] def diffuser( input_object: QuantumVariable | QuantumArray | Sequence[QuantumVariable | QuantumArray], phase: FloatLike = np.pi, state_function: Callable | None = None, reflection_indices: list[int] | None = None, ): r"""Applies the Grover diffuser onto (multiple) QuantumVariables. Parameters ---------- input_object : QuantumVariable | QuantumArray | Sequence[QuantumVariable | QuantumArray] The (list of) QuantumVariables to apply the Grover diffuser on. phase : FloatLike, optional Specifies the phase shift. The default is $\pi$, i.e. a multi-controlled Z gate. state_function : function, optional A Python function preparing the initial state. By default, the function prepares the uniform superposition state. reflection_indices : list[int], optional A list indicating with respect to which variables the reflection is performed. By default, the reflection is performed with respect to all variables in ``input_object``. Examples -------- We apply the Grover diffuser onto several QuantumChars: >>> from qrisp import QuantumChar >>> from qrisp.grover import diffuser >>> q_ch_list = [QuantumChar(), QuantumChar(), QuantumChar()] >>> diffuser(q_ch_list) >>> print(q_ch_list[0].qs) .. code-block:: none ┌────────────┐ q_ch_0.0: ┤0 ├ │ │ q_ch_0.1: ┤1 ├ │ │ q_ch_0.2: ┤2 ├ │ │ q_ch_0.3: ┤3 ├ │ │ q_ch_0.4: ┤4 ├ │ │ q_ch_1.0: ┤5 ├ │ │ q_ch_1.1: ┤6 ├ │ │ q_ch_1.2: ┤7 diffuser ├ │ │ q_ch_1.3: ┤8 ├ │ │ q_ch_1.4: ┤9 ├ │ │ q_ch_2.0: ┤10 ├ │ │ q_ch_2.1: ┤11 ├ │ │ q_ch_2.2: ┤12 ├ │ │ q_ch_2.3: ┤13 ├ │ │ q_ch_2.4: ┤14 ├ └────────────┘ """ if state_function is None: def _state_function(*qargs): for arg in qargs: h(arg) state_function = _state_function reflection(input_object, state_function, phase=phase, reflection_indices=reflection_indices)
[docs] def tag_state( tag_specificator: dict[QuantumVariable, Any], binary_values: bool = False, phase: FloatLike = np.pi, ): r"""Applies a phase tag to (multiple) QuantumVariables. The tagged state is specified in the dictionary ``tag_specificator``. This dictionary should contain the QuantumVariables as keys and the labels of the states which should be tagged as values. Parameters ---------- tag_specificator : dict A dictionary specifying which state should be tagged. binary_values : bool, optional If set to True, the values in the tag_specificator dict have to be bitstrings instead of labels. The default is False. phase : FloatLike, optional Specify the phase shift the tag should perform. The default is $\pi$, i.e. a multi-controlled Z gate. Examples -------- We construct an oracle that tags the states -3 and 2 on two QuantumFloats :: from qrisp.grover import tag_state def test_oracle(qf_list): tag_dic = {qf_list[0] : -3, qf_list[1] : 2} tag_state(tag_dic) """ qv_list = list(tag_specificator.keys()) if check_for_tracing_mode(): states = [qv.encoder(tag_specificator[qv]) for qv in qv_list] def conjugator(qv_list, temp_qf): for i in range(len(qv_list)): mcx(qv_list[i], temp_qf[i], method="balauca", ctrl_state=states[i]) m = len(qv_list) temp_qf = QuantumFloat(m) if m == 1: with conjugate(conjugator)(qv_list, temp_qf): with control(phase == np.pi): z(temp_qf) with control(phase != np.pi): p(phase, temp_qf) else: with conjugate(conjugator)(qv_list, temp_qf): with control(phase == np.pi): h(temp_qf[-1]) mcx(temp_qf[: m - 1], temp_qf[-1], method="balauca", ctrl_state=-1) h(temp_qf[-1]) with control(phase != np.pi): mcp(phase, temp_qf, method="balauca", ctrl_state=-1) temp_qf.delete() else: states = [tag_specificator[qv] for qv in qv_list] if not states: states = ["1" * qv.size for qv in qv_list] bit_string = "" from qrisp.misc import bin_rep for i in range(len(qv_list)): if binary_values: bit_string += states[i][::-1] else: bit_string += bin_rep(qv_list[i].encoder(states[i]), qv_list[i].size)[::-1] qubit_list = sum([list(qv.reg) for qv in qv_list], []) state = bit_string if state[-1] == "0": x(qubit_list[-1]) # The control state is the state we are looking for without the base qubit ctrl_state = state[:-1] if phase == np.pi: mcz(qubit_list, ctrl_state=ctrl_state + "1") else: mcp(phase, qubit_list, ctrl_state=ctrl_state + "1") # Apply the final x gate if state[-1] == "0": x(qubit_list[-1])
[docs] def grovers_alg( args: QuantumVariable | QuantumArray | Sequence[QuantumVariable | QuantumArray], oracle_function: Callable, kwargs: dict[str, Any] | None = None, iterations: int | None = None, winner_state_amount: int | None = None, exact: bool = False, ): r"""Applies Grover's algorithm to a given oracle (in the form of a Python function). Parameters ---------- args : QuantumVariable | QuantumArray | Sequence[QuantumVariable | QuantumArray] The quantum variable, array, or collection thereof that defines the search space for Grover's algorithm. oracle_function : Callable The quantum oracle function. This callable must accept ``args`` as its argument and apply a phase flip to tag the target winner state(s). Must uncompute any auxiliary QuantumVariables it uses. If ``exact`` is set to True, the oracle function must also support the keyword argument ``phase``, which specifies how much the winner states are phase-shifted (in standard Grover, this would be $\pi$). kwargs : dict, optional A dictionary containing keyword arguments to be passed to the oracle. The default is None. iterations : int, optional The exact amount of Grover iterations to perform. winner_state_amount : int, optional If ``iterations`` is not specified, the optimal number of iterations is calculated using the established mathematical formula based on the number of winner states. The default assumption (if omitted) is 1. exact : bool, optional If set to True, the `exact version <https://arxiv.org/pdf/quant-ph/0106071.pdf>`_ of Grover's algorithm will be evaluated. For this, the correct ``winner_state_amount`` must be supplied, and the oracle must support the keyword argument ``phase`` to apply the calculated fractional phase shift. Raises ------ ValueError If ``exact`` is set to True but ``winner_state_amount`` is not specified. ZeroDivisionError If ``winner_state_amount`` is 0. Examples -------- We construct an oracle that tags the states -3 and 2 on two QuantumFloats and apply Grover's algorithm. :: from qrisp import QuantumFloat #Create list of QuantumFloats qf_list = [QuantumFloat(2, signed = True), QuantumFloat(2, signed = True)] from qrisp.grover import tag_state, grovers_alg def test_oracle(qf_list): tag_dic = {qf_list[0] : -3, qf_list[1] : 2} tag_state(tag_dic) grovers_alg(qf_list, test_oracle) >>> from qrisp.misc import multi_measurement >>> print(multi_measurement(qf_list)) {(-3, 2): 0.9966, (0, 0): 0.0001, (0, 1): 0.0001, (0, 2): 0.0001, (0, 3): 0.0001, (0, -4): 0.0001, (0, -3): 0.0001, (0, -2): 0.0001, (0, -1): 0.0001, (1, 0): 0.0001, (1, 1): 0.0001, (1, 2): 0.0001, (1, 3): 0.0001, (1, -4): 0.0001, (1, -3): 0.0001, (1, -2): 0.0001, (1, -1): 0.0001, (2, 0): 0.0001, (2, 1): 0.0001, (2, 2): 0.0001, (2, 3): 0.0001, (2, -4): 0.0001, (2, -3): 0.0001, (2, -2): 0.0001, (2, -1): 0.0001, (3, 0): 0.0001, (3, 1): 0.0001, (3, 2): 0.0001, (3, 3): 0.0001, (3, -4): 0.0001, (3, -3): 0.0001, (3, -2): 0.0001, (3, -1): 0.0001, (-4, 0): 0.0001, (-4, 1): 0.0001, (-4, 2): 0.0001, (-4, 3): 0.0001, (-4, -4): 0.0001, (-4, -3): 0.0001, (-4, -2): 0.0001, (-4, -1): 0.0001, (-3, 0): 0.0001, (-3, 1): 0.0001, (-3, 3): 0.0001, (-3, -4): 0.0001, (-3, -3): 0.0001, (-3, -2): 0.0001, (-3, -1): 0.0001, (-2, 0): 0.0001, (-2, 1): 0.0001, (-2, 2): 0.0001, (-2, 3): 0.0001, (-2, -4): 0.0001, (-2, -3): 0.0001, (-2, -2): 0.0001, (-2, -1): 0.0001, (-1, 0): 0.0001, (-1, 1): 0.0001, (-1, 2): 0.0001, (-1, 3): 0.0001, (-1, -4): 0.0001, (-1, -3): 0.0001, (-1, -2): 0.0001, (-1, -1): 0.0001} **Exact Grovers Algorithm** In the next example, we will showcase the ``exact`` functionality. For this we create an oracle, which tags all the states of a QuantumVariable, that contain 3 ones and 2 zeros. To count the amount of ones we use quantum phase estimation on the operator .. math:: U = \text{exp}\left(\frac{i 2 \pi}{2^k} \sum_{i = 0}^{n-1} ( 1 - \sigma_{z}^i )\right) :: from qrisp import QPE, p, QuantumVariable, lifted from qrisp.grover import grovers_alg, tag_state import numpy as np def U(qv, prec = None, iter = 1): for i in range(qv.size): p(iter*2*np.pi/2**prec, qv[i]) @lifted def count_ones(qv): prec = int(np.ceil(np.log2(qv.size+1))) res = QPE(qv, U, precision = prec, iter_spec = True, kwargs = {"prec" : prec}) res <<= prec return res Quick test: >>> qv = QuantumVariable(5) >>> qv[:] = {"11000" : 1, "11010" : 1, "11110" : 1} >>> count_qf = count_ones(qv) >>> count_qf.qs.statevector() sqrt(3)*(|11000>*|2> + |11010>*|3> + |11110>*|4>)/3 We now define the oracle :: def counting_oracle(qv, phase = np.pi, k = 1): count_qf = count_ones(qv) tag_state({count_qf : k}, phase = phase) count_qf.uncompute() And evaluate Grover's algorithm :: n = 5 k = 3 qv = QuantumVariable(n) import math grovers_alg(qv, counting_oracle, exact = True, winner_state_amount = math.comb(n,k), kwargs = {"k" : k}) # noqa >>> print(qv) {'11100': 0.1, '11010': 0.1, '10110': 0.1, '01110': 0.1, '11001': 0.1, '10101': 0.1, '01101': 0.1, '10011': 0.1, '01011': 0.1, '00111': 0.1} We see that contrary to regular Grover's algorithm, the states which have not been tagged by the oracle have zero percent measurement probability. """ # Necessary to prevent errors in recursive_qs_search when applied to jax arrays. if check_for_tracing_mode(): import jax.numpy as jnp else: import numpy as jnp if kwargs is None: kwargs = {} if exact and winner_state_amount is None: raise ValueError("Exact Grover's algorithm requires 'winner_state_amount' to be specified.") elif winner_state_amount is None: winner_state_amount = 1 if isinstance(args, Sequence): flat_qvs: list[QuantumVariable] = [] for arg in args: if isinstance(arg, QuantumArray): flat_qvs.extend(list(arg.flatten())) else: flat_qvs.append(arg) N = 2 ** jnp.sum(jnp.array([qv.size for qv in flat_qvs])) elif isinstance(args, QuantumArray): N = 2 ** jnp.sum(jnp.array([qv.size for qv in args.flatten()])) elif isinstance(args, QuantumVariable): N = 2**args.size else: raise TypeError(f"Unsupported type for args: {type(args)}") if exact: # Implementation for phase calculation for exact grovers alg as in # https://arxiv.org/pdf/quant-ph/0106071.pdf theta = jnp.arcsin(jnp.sqrt(winner_state_amount / N)) iterations = jnp.int64(jnp.ceil(jnp.pi / (4 * theta) - 0.5)) phi = 2 * jnp.arcsin(jnp.sin(jnp.pi / (4 * (iterations - 1) + 6)) * (N / winner_state_amount) ** 0.5) elif iterations is None: iterations = jnp.pi / 4 * jnp.sqrt(N / winner_state_amount) iterations = jnp.int64(jnp.round(iterations)) if isinstance(args, Sequence): for qv in args: h(qv) else: h(args) if check_for_tracing_mode(): for _ in jrange(iterations): if exact: oracle_function(args, phase=phi, **kwargs) diffuser(args, phase=phi) else: oracle_function(args, **kwargs) diffuser(args) elif iterations > 0: merge(args) qs = recursive_qs_search(args)[0] # qv_amount = len(qs.qv_list) with IterationEnvironment(qs, iterations): if exact: oracle_function(args, phase=phi, **kwargs) diffuser(args, phase=phi) else: oracle_function(args, **kwargs) diffuser(args)
# NOTE: We could check here whether the oracle introduced new QuantumVariables without uncomputing/deleting them, which would be a common mistake. # This check was deactivated, be cause it raises an unjustified error in some cases, e.g., when the oracle acts on a QuantumVariable that is not part of the input `args`. See #586. # if qv_amount != len(qs.qv_list): # raise Exception( # "Applied oracle introducing new QuantumVariables without uncomputing/deleting" # ) # Workaround to keep the docstring but still gatewrap temp = diffuser.__doc__ diffuser = gate_wrap(permeability=[], is_qfree=False)(diffuser) diffuser.__doc__ = temp temp = tag_state.__doc__ tag_state = gate_wrap(permeability="args", is_qfree=True)(tag_state) tag_state.__doc__ = temp