LCU and qubitization

Download Notebook - lcu_qubitization.ipynb

Build a block encoding from a Pauli Hamiltonian, then check that two quantum-walk steps encode its second Chebyshev polynomial. This example uses unary-iteration SELECT with temporary-AND uncomputation.

Run this notebook from a source checkout with the development dependencies installed. The repository default is little endian.

Hide code cell source

from typing import no_type_check

import zixy.qubit.pauli as zqp
from guppylang import guppy
from guppylang.std.builtins import array
from guppylang.std.quantum import qubit

from guppyalgos.primitives.gate_decompositions.and_op import (
    temp_and_compute,
    temp_and_uncompute,
)
from guppyalgos.primitives.gate_decompositions.cnx.cnx import cnx
from guppyalgos.algorithms.block_encoding.lcu import LCU, LCUData, build_unary_iteration_select
from guppyalgos.algorithms.block_encoding.qubitization.qubitization import Qubitization
from guppyalgos.primitives.subroutines.reflection import Reflection
from guppyalgos.algorithms.state_preparation import multiplexor_prep
from guppyalgos.tests.helpers import (
    Endianness,
    assert_allclose_ignorephase,
    chebyshev_power_matrix,
    get_unitary_projected,
)

1. Build PREPARE and SELECT

For the Hamiltonian

\[ H=0.4Z_0-0.2X_1+0.3Y_0Y_1+0.1Z_0X_1,\qquad\lambda=\sum_j|a_j|=1, \]

LCUData extracts the weights and Pauli terms. multiplexor_prep prepares amplitudes \(\sqrt{|a_j|/\lambda}\), while SELECT includes the coefficient signs. Together with inverse preparation, they give

\[ B=P^\dagger SP,\qquad\langle00|B|00\rangle=H/\lambda. \]

Two preparation qubits address the four terms. Unary iteration uses one additional work qubit internally.

hamiltonian = zqp.RealTermSum.from_str(
    "(0.4, Z0), (-0.2, X1), (0.3, Y0 Y1), (0.1, Z0 X1)",
    2,
)
lcu_data = LCUData.from_hamiltonian(hamiltonian)

prepare = multiplexor_prep(lcu_data.amplitudes)

select = build_unary_iteration_select(
    lcu_data,
    comp_and_op=temp_and_compute,
    uncomp_and_op=temp_and_uncompute,
)

dagger = object()

@guppy
@no_type_check
def unprepare(prep: array[qubit, 2]) -> None:
    with dagger:
        prepare(prep)

2. Apply two walk steps

Qubitization combines the LCU with the all-zero reflection:

\[ W=RB,\qquad R=I-2|00\rangle\langle00|. \]

For this Hermitian Pauli encoding, the projected second power is

\[ \langle00|W^2|00\rangle=T_2(H/\lambda)=2(H/\lambda)^2-I. \]

The check reserves the internal work qubit and compares the unnormalized block with the classical polynomial, allowing an overall global phase.

power = 2

@guppy
@no_type_check
def qubitize(
    prep: array[qubit, 2],
    state: array[qubit, 2],
) -> None:
    lcu = LCU(prepare, select, unprepare)
    reflection = Reflection[2, 1](cnx)
    Qubitization(lcu, reflection).power(prep, state, power)

projected_qubitization = get_unitary_projected(
    qubitize,
    lcu_data.n_state_qubits,
    {"prep": [False] * lcu_data.n_prep_qubits},
    n_extra_qubits=lcu_data.n_prep_qubits - 1,
    endianness=Endianness.LITTLE,
)

normalized_hamiltonian = (
    hamiltonian.to_sparse_matrix().toarray() / lcu_data.l1_norm
)
expected_chebyshev = chebyshev_power_matrix(normalized_hamiltonian, power)
assert_allclose_ignorephase(projected_qubitization, expected_chebyshev)