Source code for qrisp.block_encodings.constructors.from_projector

# ********************************************************************************
# * 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
# ********************************************************************************

"""Builds a BlockEncoding of a projector or its kernel projector between two states."""

from typing import Callable, Optional, Tuple, Union

from qrisp.block_encodings.block_encoding_base import BlockEncoding
from qrisp.core.gate_application_functions import mcx, x
from qrisp.environments import invert
from qrisp.qtypes import QuantumBool


def build_from_projector(
    cls: type[BlockEncoding],
    left: Union[int, Tuple[int, ...], Callable],
    right: Optional[Union[int, Tuple[int, ...], Callable]] = None,
    kernel: bool = False,
    num_ops: int = 1,
) -> BlockEncoding:
    r"""Constructs a BlockEncoding of a projector.

    Parameters
    ----------
    cls : type[BlockEncoding]
        The class on which this constructor is invoked.
    left : int | tuple of int | Callable
        An integer or a tuple of integers representing a computational basis state $\ket{\phi}$,
        or a function ``left(*operands)`` preparing a state $\ket{\phi}$ from $\ket{0}$.
    right : int | tuple of int | Callable
        An integer or a tuple of integers representing a computational basis state $\ket{\psi}$,
        or a function ``right(*operands)`` preparing a state $\ket{\psi}$ from $\ket{0}$.
        Defaults to ``left``.
    kernel : bool
        If `True`, the kernel projector $\mathbb I - \ket{\phi}\bra{\phi}$ is block-encoded.
        If `False`, the projector $\ket{\phi}\bra{\psi}$ is block-encoded. Defaults to `False`.
    num_ops : int
        The number of operand quantum variables.
        Automatically inferred when ``left`` or ``right`` is an integer or tuple of integers.
        Defaults to 1.

    Returns
    -------
    BlockEncoding
        A BlockEncoding representing either the projector $\ket{\phi}\bra{\psi}$
        or the kernel projector $\mathbb I - \ket{\phi}\bra{\phi}$, depending on the value of ``kernel``.

    Examples
    --------
    **Example 1: Computational basis states**

    Define a block-encoding for the projector $P=\ket{1}\bra{3}$.

    ::

        from qrisp import *
        from qrisp.block_encodings import BlockEncoding

        P = BlockEncoding.from_projector(1, 3)

        # Prepare operand in superposition state
        def operand_prep():
            operand = QuantumFloat(2)
            h(operand)
            return operand

        @terminal_sampling
        def main():
            operand = P.apply_rus(operand_prep)()
            return operand

        res_dict = main()
        print(res_dict)
        # {1.0: 1.0}

    **Example 2: Custom states**

    Define a block-encoding for the projector $P=\ket{\psi}\bra{\psi}$ where $\ket{\psi}\propto\ket{0}+\ket{1}+\ket{2}+\ket{3}$.

    ::

        from qrisp import *
        from qrisp.block_encodings import BlockEncoding

        def prep_psi(qv):
            h(qv)

        P = BlockEncoding.from_projector(prep_psi)

        # Prepare operand in |0> state
        def operand_prep():
            operand = QuantumFloat(2)
            return operand

        @terminal_sampling
        def main():
            operand = P.apply_rus(operand_prep)()
            return operand

        res_dict = main()
        print(res_dict)
        # {0.0: 0.25, 1.0: 0.25, 2.0: 0.25, 3.0: 0.25}

    """
    if kernel or (right is None):
        right = left

    # left
    num_left = 1
    if isinstance(left, int):

        def prep_left(arg):
            arg.encode(left, permit_dirtyness=True)

    elif isinstance(left, tuple):
        num_left = len(left)

        def prep_left(*args):
            for i, arg in enumerate(args):
                arg.encode(left[i], permit_dirtyness=True)

    elif callable(left):
        prep_left = left
    else:
        return NotImplemented

    # right
    num_right = 1
    if isinstance(right, int):

        def prep_right(arg):
            arg.encode(right, permit_dirtyness=True)

    elif isinstance(right, tuple):
        num_right = len(right)

        def prep_right(*args):
            for i, arg in enumerate(args):
                arg.encode(right[i], permit_dirtyness=True)

    elif callable(right):
        prep_right = right
    else:
        return NotImplemented

    if not (isinstance(left, Callable) or isinstance(right, Callable)):
        if num_left != num_right:
            raise ValueError(f"Size mismatch: left has {num_left} elements, but right has {num_right}.")
    num_ops = max(num_left, num_right, num_ops)

    def unitary(*args):
        anc = args[0]
        operands = args[1:]

        if not kernel:
            x(anc)

        with invert():
            prep_right(*operands)

        qubits = sum([operand.reg for operand in operands], [])
        mcx(qubits, anc[0], ctrl_state=0)

        prep_left(*operands)

    return cls(1, [QuantumBool().template()], unitary, num_ops=num_ops)