BlockEncoding#

class BlockEncoding(alpha: ArrayLike, ancillas: list[QuantumVariable | QuantumVariableTemplate], unitary: Callable[..., None], num_ops: int = 1, is_hermitian: bool = False)[source]#

Central structure for representing block-encodings.

Block-encoding is a foundational technique that enables the implementation of non-unitary operations on a quantum computer by embedding them into a larger unitary operator. Given an operator \(A\), we embed a scaled version \(A/\alpha\) into the upper-left block of a unitary matrix \(U_{A}\):

\[\begin{split}U_A= \begin{pmatrix} A/\alpha & *\\ * & * \end{pmatrix}\end{split}\]

More formally, a block-encoding of an operator \(A\) (not necessarily unitary) acting on a Hilbert space \(\mathcal H_{s}\) is a unitary acting on \(\mathcal H_a\otimes H_s\) (for some auxiliary Hilbert space \(\mathcal H_a\)) such that

\[\|A - \alpha (\bra{0}_a \otimes \mathbb I_s) U_A (\ket{0}_a \otimes \mathbb I_s) \| \leq \epsilon\]

where

  • \(\alpha\geq \|A\|\) is a subnormalization factor (or scaling factor) that ensures \(A/\alpha\) has singular values within the unit disk.

  • \(\epsilon\geq 0\) represents the approximation error.

The block-encoding is termed exact if \(\epsilon=0\), meaning the upper-left block of \(U_{A}\) is exactly \(A/\alpha\).

Implementation mechanism

To apply the operator \(A\) to a quantum state \(\ket{\psi}\):

  • Prepare the system in state \(\ket{0}_a \otimes \ket{\psi}_s\).

  • Apply the unitary \(U_A\).

  • Post-select by measuring the ancillas. If the result is \(\ket{0}_a\), the remaining state is \(\dfrac{A\ket{\psi}}{\|A\ket{\psi}\|}\).

  • The success probability of this operation is given by

\[P_{\text{success}} = \dfrac{\|A\ket{\psi}\|^2}{\alpha^2}\]
Parameters:
alphaArrayLike

The scalar scaling factor.

ancillaslist[QuantumVariable | QuantumVariableTemplate]

A list of QuantumVariables or QuantumVariableTemplates. These serve as templates for the ancilla variables used in the block-encoding.

unitaryCallable

A function unitary(*ancillas, *operands) applying the block-encoding unitary. It receives the ancilla and operand QuantumVariables as arguments.

num_opsint

The number of operand quantum variables. The default is 1.

is_hermitianbool

Indicates whether the block-encoding unitary is Hermitian. The default is False.

Attributes:
alphaArrayLike

The scalar scaling factor.

unitaryCallable

A function unitary(*ancillas, *operands) applying the block-encoding unitary. It receives the ancilla and operand QuantumVariables as arguments.

num_opsint

The number of operand quantum variables.

num_ancsint

The number of ancilla quantum variables.

is_hermitianbool

Indicates whether the block-encoding unitary is Hermitian.

Notes

  • The shape of the block-encoded operator is determined by the size of the operand variables to which the block-encoding is applied. E.g., if a block-encoded \(4\times 4\) matrix \(A\) is applied to a 3-qubit QuantumVariable, then a block-encoding of the \(8\times 8\) matrix \(\tilde{A}=\mathbb{I}\otimes A\) is applied. This is consistent with the convention that non-occuring indices in a Pauli string are treated as identities. Static-shaped block-encodings may be introduced in a future release.

  • The is_hermitian attribute indicates whether the block-encoding unitary \(U_A\) is Hermitian. This is distinct from the operator \(A\) being being Hermitian. A Hermitian operator \(A\) can be block-encoded using a non-Hermitian unitary \(U_A\). Conversely, if the unitary \(U_A\) is Hermitian, then the encoded operator must also be Hermitian.

Examples

Example 1: Pauli Block Encoding

Define a QubitOperator repesenting a Heisenberg Hamiltonian, and construct a block-encoding based on LCU for its Pauli strings.

from qrisp import *
from qrisp.block_encodings import BlockEncoding
from qrisp.operators import X, Y, Z

H = sum(X(i)*X(i+1) + Y(i)*Y(i+1) + Z(i)*Z(i+1) for i in range(3))
BE = BlockEncoding.from_operator(H)

# Apply the Hermitian operator to an initial system state

# Prepare initial system state
def operand_prep():
    operand = QuantumFloat(4)
    h(operand[0])
    return operand

@terminal_sampling
def main():
    return BE.apply_rus(operand_prep)()

main()
# {0.0: 0.6428571295525347, 2.0: 0.2857142963579722, 1.0: 0.07142857408949305}

Example 2: Custom LCU Block Encoding

Define a block-encoding for a discrete Laplace operator in one dimension with periodic boundary conditions.

import numpy as np

N = 8
I = np.eye(N)
A = 2*I - np.eye(N, k=1) - np.eye(N, k=-1)
A[0, N-1] = -1
A[N-1, 0] = -1

print(A)
#[[ 2. -1.  0.  0.  0.  0.  0. -1.]
# [-1.  2. -1.  0.  0.  0.  0.  0.]
# [ 0. -1.  2. -1.  0.  0.  0.  0.]
# [ 0.  0. -1.  2. -1.  0.  0.  0.]
# [ 0.  0.  0. -1.  2. -1.  0.  0.]
# [ 0.  0.  0.  0. -1.  2. -1.  0.]
# [ 0.  0.  0.  0.  0. -1.  2. -1.]
# [-1.  0.  0.  0.  0.  0. -1.  2.]]

This matrix is decomposed as linear combination of three unitaries: the identity \(I\), and two shift operators \(V\colon\ket{k}\rightarrow-\ket{k+1\mod N}\) and \(V^{\dagger}\colon\ket{k}\rightarrow-\ket{k-1\mod N}\).

from qrisp import *
from qrisp.block_encodings import BlockEncoding

def I(qv):
    pass

def V(qv):
    qv += 1
    gphase(np.pi, qv[0])

def V_dg(qv):
    qv -= 1
    gphase(np.pi, qv[0])

unitaries = [I, V, V_dg]
coeffs = np.array([2.0, 1.0, 1.0])

BE = BlockEncoding.from_lcu(coeffs, unitaries)

Apply the operator to the initial system state \(\ket{0}\).

# Prepare initial system state |0>
def operand_prep():
    return QuantumFloat(3)

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

main()
# {0.0: 0.6666666567325588, 7.0: 0.16666667908430155, 1.0: 0.1666666641831397}

To perform quantum resource estimation for the quantum program (not counting repetitions), replace the @terminal_sampling decorator with @count_ops(meas_behavior="0"):

@count_ops(meas_behavior="0")
def main():
    operand = BE.apply_rus(operand_prep)()
    return operand

main()
# {'s': 4, 'gphase': 2, 'u3': 6, 't': 14, 't_dg': 16, 'x': 5, 'cx': 54, 
# 'p': 2, 'h': 16, 'measure': 10}

To perform resource estimations for the block-encoding use resources():

BE.resources(QuantumFloat(3))
# {'gate counts': {'s': 4, 't_dg': 16, 'h': 16, 't': 14, 'gphase': 2, 
# 'p': 2, 'x': 5, 'cx': 54, 'u3': 6, 'measure': 4}, 'depth': 48, 'qubits': 9}

Constructors#

from_array()

Constructs a BlockEncoding from a 2-D array.

from_lcu()

Constructs a BlockEncoding using the Linear Combination of Unitaries (LCU) protocol.

from_operator()

Constructs a BlockEncoding from an operator.

Utilities#

apply()

Applies the BlockEncoding unitary to the given operands.

apply_rus()

Applies the BlockEncoding using Repeat-Until-Success.

chebyshev()

Returns a BlockEncoding representing \(k\)-th Chebyshev polynomial of the first kind applied to the operator.

create_ancillas()

Returns a list of ancilla QuantumVariables for the BlockEncoding.

expectation_value()

Measures the expectation value of the operator using the Hadamard test protocol.

qubitization()

Returns a BlockEncoding representing the qubitization walk operator.

resources()

Estimate the quantum resources required for the BlockEncoding.

Arithmetic#

__add__()

Returns a BlockEncoding of the sum of two operators.

__matmul__()

Returns a BlockEncoding of the product of two operators.

__mul__()

Returns a BlockEncoding of the scaled operator.

__neg__()

Returns a BlockEncoding of the negated operator.

__sub__()

Returns a BlockEncoding of the difference between two operators.

kron()

Returns a BlockEncoding of the Kronecker product (tensor product) of two operators.

Algorithms & Applications#

inv()

Returns a BlockEncoding approximating the matrix inversion of the operator.

poly()

Returns a BlockEncoding representing a polynomial transformation of the operator.

sim()

Returns a BlockEncoding approximating Hamiltonian simulation of the operator.