{ "cells": [ { "cell_type": "markdown", "id": "block-demo-00", "metadata": {}, "source": [ "# **Block Encoding: From LCU to QSVT**\n", "\n", "**Download Notebook** - {nb-download}`block_encoding_demo.ipynb`\n", "\n", "- Encode a two-term Hamiltonian with LCU.\n", "- Reuse the block encoding for qubitization and a polynomial transformation with QSVT.\n", "- Extend the construction to eight terms and alias-sampling preparation with configurable QROM fanout.\n", "\n", "- The examples use [guppy](https://docs.quantinuum.com/guppy/language_guide/language_guide_index.html). Run from a source checkout with development dependencies installed; the checks use `guppyalgos.tests.helpers`.\n", "- The repository default is little endian.\n" ] }, { "cell_type": "code", "execution_count": 1, "id": "block-demo-01", "metadata": { "tags": [ "hide-input" ] }, "outputs": [], "source": [ "from math import sqrt\n", "\n", "import numpy as np\n", "import zixy.qubit.pauli as zqp\n", "from guppylang import guppy\n", "from guppylang.std.builtins import array, comptime, dagger\n", "from guppylang.std.quantum import qubit\n", "\n", "from guppyalgos.algorithms.block_encoding.lcu import (\n", " LCU, LCUData, build_single_cntrl_select, build_unary_iteration_select,\n", ")\n", "from guppyalgos.algorithms.block_encoding.qubitization import Qubitization\n", "from guppyalgos.algorithms.state_preparation import multiplexor_prep\n", "from guppyalgos.primitives.subroutines.reflection import Reflection\n", "from guppyalgos.primitives.gate_decompositions.cnx.cnx import cnx\n", "from guppyalgos.tests.helpers import (\n", " Endianness, assert_allclose_ignorephase, get_unitary_projected,\n", ")\n", "from guppyalgos.algorithms.block_encoding.qsvt import QSVT\n" ] }, { "cell_type": "markdown", "id": "bc40af07", "metadata": {}, "source": [ "## Projecting out the encoded block\n", "\n", "A block encoding stores a matrix inside a larger unitary. Initialize the preparation qubits at zero and project them back onto zero to extract the system block:\n", "\n", "$$\n", "M=(\\langle0^a|\\otimes I)\\,U\\,(|0^a\\rangle\\otimes I).\n", "$$\n", "\n", "- Each example passes an explicit projection dictionary to `get_unitary_projected`. Its entries follow the circuit's preparation-register order; QSVT also projects its signal qubit onto zero.\n", "- Projection preserves the block's scale. For normalized $|\\psi\\rangle$, the post-selection probability is $\\|M|\\psi\\rangle\\|^2$.\n", "- Compare the extracted matrix with the expected block, allowing an overall global phase.\n", "\n", "- Reference polynomials use Zixy Pauli sums: `A * A` is the operator product. Convert to a matrix only when comparing with the simulated block.\n" ] }, { "cell_type": "markdown", "id": "block-demo-02", "metadata": {}, "source": [ "## 1. LCU: encode two terms\n", "\n", "Choose $H=0.6X+0.4Y$, with normalization $\\lambda=0.6+0.4=1$.\n", "One preparation qubit selects between X and Y:\n", "\n", "$$\n", "P|0\\rangle=\\sqrt{0.6}|0\\rangle+\\sqrt{0.4}|1\\rangle,\n", "\\qquad S=|0\\rangle\\langle0|\\otimes X+|1\\rangle\\langle1|\\otimes Y.\n", "$$\n", "\n", "`LCU` applies PREPARE, SELECT, then UNPREPARE. The resulting unitary $B$ contains the Hamiltonian in its zero-ancilla block:\n", "\n", "$$\n", "\\langle0|B|0\\rangle=H/\\lambda\n", "=\\begin{pmatrix}0&0.6-0.4i\\\\0.6+0.4i&0\\end{pmatrix}.\n", "$$" ] }, { "cell_type": "code", "execution_count": 2, "id": "block-demo-03", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "LCU block matches H / normalization.\n" ] } ], "source": [ "hamiltonian = zqp.RealTermSum.from_str(\"(0.6, X0), (0.4, Y0)\", 1)\n", "data = LCUData.from_hamiltonian(hamiltonian)\n", "prepare = multiplexor_prep(data.amplitudes)\n", "select = build_single_cntrl_select(data)\n", "A = zqp.ComplexTermSum.from_str(hamiltonian.to_str(), 1) / data.l1_norm\n", "\n", "\n", "@guppy\n", "def unprepare(prep_qreg: array[qubit, 1]) -> None:\n", " with dagger:\n", " prepare(prep_qreg)\n", "\n", "\n", "@guppy\n", "def encode(prep_qreg: array[qubit, 1], state_qreg: array[qubit, 1]) -> None:\n", " LCU(prepare, select, unprepare).compose(prep_qreg, state_qreg)\n", "\n", "\n", "encoded_block = get_unitary_projected(\n", " encode, 1, {\"prep\": [False]},\n", " endianness=Endianness.LITTLE,\n", " n_extra_qubits=2,\n", ")\n", "assert_allclose_ignorephase(\n", " encoded_block, A.to_sparse_matrix().toarray(), threshold=1e-7,\n", ")\n", "print(\"LCU block matches H / normalization.\")" ] }, { "cell_type": "markdown", "id": "block-demo-04", "metadata": {}, "source": [ "## 2. Qubitization: reuse the LCU\n", "\n", "Add a reflection on the preparation qubit to make a quantum walk:\n", "\n", "$$\n", "W=RB,\\qquad R=I-2|0\\rangle\\langle0|.\n", "$$\n", "\n", "`Qubitization` takes the same LCU and the reflection. Two steps encode\n", "\n", "$$\n", "\\langle0|W^2|0\\rangle=T_2(A)=2A^2-I=0.04I,\n", "\\qquad A=H/\\lambda.\n", "$$\n", "\n", "Use `.compose(...)` for one step or `.power(..., d)` for $d$ steps. Keep the preparation register coherent between them." ] }, { "cell_type": "code", "execution_count": 3, "id": "block-demo-05", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Two walk steps encode 2A² - I.\n" ] } ], "source": [ "@guppy\n", "def walk_squared(prep_qreg: array[qubit, 1], state_qreg: array[qubit, 1]) -> None:\n", " Qubitization(\n", " LCU(prepare, select, unprepare), Reflection[1, 0](cnx)\n", " ).power(prep_qreg, state_qreg, 2)\n", "\n", "\n", "walk_block = get_unitary_projected(\n", " walk_squared, 1, {\"prep\": [False]},\n", " endianness=Endianness.LITTLE,\n", " n_extra_qubits=2,\n", ")\n", "\n", "identity = zqp.ComplexTermSum.from_str(\"(1, I0)\", 1)\n", "walk_polynomial = 2 * (A * A) - identity\n", "assert_allclose_ignorephase(\n", " walk_block, walk_polynomial.to_sparse_matrix().toarray(),\n", " threshold=1e-7,\n", ")\n", "print(\"Two walk steps encode 2A² - I.\")" ] }, { "cell_type": "markdown", "id": "block-demo-06", "metadata": {}, "source": [ "## 3. QSVT: choose a polynomial\n", "\n", "`QSVT` combines the LCU, its adjoint and a phase sequence. Here $B^\\dagger=B$, so both arguments use the same LCU. The three half-turn phases `[0.5, 0.5, 0.5]` give the cubic\n", "\n", "$$\n", "p(x)=\\frac{x-2x^3}{\\sqrt{2}},\\qquad\n", "\\langle0|_s\\langle0|_p V|0\\rangle_s|0\\rangle_p=p(A)=-\\frac{0.04}{\\sqrt{2}}A.\n", "$$\n", "\n", "- Project the signal and preparation qubits onto zero to extract the transformed block.\n", "- Since $A^2=0.52I$, $p(A)=-0.04A/\\sqrt{2}$: the result is a small, sign-flipped copy of $A$, not the identity." ] }, { "cell_type": "code", "execution_count": 4, "id": "block-demo-07", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "QSVT block matches (A - 2A³) / √2.\n" ] } ], "source": [ "phases = [0.5, 0.5, 0.5]\n", "\n", "\n", "@guppy\n", "def transform(\n", " prep_qreg: array[qubit, 1], signal_qreg: array[qubit, 1],\n", " state_qreg: array[qubit, 1],\n", ") -> None:\n", " QSVT(\n", " LCU(prepare, select, unprepare),\n", " LCU(prepare, select, unprepare),\n", " comptime(phases),\n", " ).compose(signal_qreg[0], prep_qreg, state_qreg)\n", "\n", "\n", "qsvt_block = get_unitary_projected(\n", " transform, 1, {\"prep\": [False], \"signal\": [False]},\n", " endianness=Endianness.LITTLE,\n", " n_extra_qubits=2,\n", ")\n", "qsvt_polynomial = (A - 2 * (A * A * A)) / sqrt(2)\n", "assert_allclose_ignorephase(\n", " qsvt_block, qsvt_polynomial.to_sparse_matrix().toarray(),\n", " threshold=1e-7,\n", ")\n", "print(\"QSVT block matches (A - 2A³) / √2.\")" ] }, { "cell_type": "markdown", "id": "block-demo-08", "metadata": {}, "source": [ "## 4. Replace the Hamiltonian\n", "\n", "For an arbitrary Hermitian Pauli sum $H=\\sum_j a_jP_j$, use `LCUData` to obtain $\\lambda=\\sum_j|a_j|$ and the register sizes. `build_unary_iteration_select` handles the term selection. The QSVT composition stays the same.\n", "\n", "For example, take\n", "\n", "$$\n", "\\begin{aligned}\n", "H={}&0.25Z_0-0.125X_1+0.125Y_0Y_1+0.125Z_0X_1\\\\\n", "&+0.125X_0+0.125Z_1+0.0625X_0Z_1-0.0625Z_0Z_1.\n", "\\end{aligned}\n", "$$\n", "\n", "- Eight terms use three preparation qubits and two system qubits.\n", "- The LCU block is checked numerically against $H/\\lambda$, a nontrivial four-by-four matrix.\n", "- The same phase sequence defines $p(H/\\lambda)=[H/\\lambda-2(H/\\lambda)^3]/\\sqrt{2}$.\n", "- The complete QSVT composition is then compiled." ] }, { "cell_type": "code", "execution_count": 5, "id": "block-demo-09", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Eight-term LCU block verified and its QSVT composition compiled.\n" ] } ], "source": [ "larger_hamiltonian = zqp.RealTermSum.from_str(\n", " \"(0.25, Z0), (-0.125, X1), (0.125, Y0 Y1), (0.125, Z0 X1), \"\n", " \"(0.125, X0), (0.125, Z1), (0.0625, X0 Z1), (-0.0625, Z0 Z1)\", 2\n", ")\n", "larger_data = LCUData.from_hamiltonian(larger_hamiltonian)\n", "n_prep = larger_data.n_prep_qubits\n", "n_state = larger_data.n_state_qubits\n", "larger_prepare = multiplexor_prep(larger_data.amplitudes)\n", "larger_select = build_unary_iteration_select(larger_data)\n", "\n", "\n", "@guppy\n", "def larger_unprepare(prep_qreg: array[qubit, n_prep]) -> None:\n", " with dagger:\n", " larger_prepare(prep_qreg)\n", "\n", "\n", "@guppy\n", "def larger_encode(\n", " prep_qreg: array[qubit, n_prep], state_qreg: array[qubit, n_state],\n", ") -> None:\n", " LCU(larger_prepare, larger_select, larger_unprepare).compose(\n", " prep_qreg, state_qreg,\n", " )\n", "\n", "\n", "@guppy\n", "def larger_transform(\n", " prep_qreg: array[qubit, n_prep], signal_qreg: array[qubit, 1],\n", " state_qreg: array[qubit, n_state],\n", ") -> None:\n", " QSVT(\n", " LCU(larger_prepare, larger_select, larger_unprepare),\n", " LCU(larger_prepare, larger_select, larger_unprepare),\n", " comptime(phases),\n", " ).compose(signal_qreg[0], prep_qreg, state_qreg)\n", "\n", "\n", "larger_A = zqp.ComplexTermSum.from_str(larger_hamiltonian.to_str(), n_state) / larger_data.l1_norm\n", "larger_block = get_unitary_projected(\n", " larger_encode, n_state, {\"prep\": [False] * n_prep},\n", " endianness=Endianness.LITTLE,\n", " n_extra_qubits=n_prep - 1,\n", ")\n", "assert_allclose_ignorephase(\n", " larger_block, larger_A.to_sparse_matrix().toarray(),\n", " threshold=1e-7,\n", ")\n", "larger_transform.compile_function()\n", "print(\"Eight-term LCU block verified and its QSVT composition compiled.\")\n" ] }, { "cell_type": "markdown", "id": "alias-intro", "metadata": {}, "source": [ "## 5. LCU with alias-sampling PREPARE\n", "\n", "Alias sampling prepares the weights of a larger table of terms. Unlike rotation-based preparation, it retains workspace $|g_j\\rangle$ alongside the index $|j\\rangle$:\n", "\n", "$$\n", "P|0\\rangle=\\sum_j\\sqrt{\\widetilde p_j}|j\\rangle|g_j\\rangle,\n", "\\qquad p_j=|a_j|/\\lambda.\n", "$$\n", "\n", "- `SELECT` acts on the index and system registers.\n", "- `UNPREPARE` reverses alias sampling on **all** preparation registers.\n", "- Together they give\n", "\n", "$$\n", "\\langle0|P^\\dagger SP|0\\rangle\n", "=\\sum_j\\widetilde p_j\\operatorname{sgn}(a_j)P_j.\n", "$$\n", "\n", "- This example reuses the eight-term Hamiltonian above.\n", "- Its weights are exactly representable with four probability bits, so $\\widetilde p_j=p_j$ and the block is $H$ because $\\lambda=1$.\n", "- Other weights are rounded at the chosen precision.\n", "- See the {doc}`alias-sampling notebook ` for the preparation routine in isolation." ] }, { "cell_type": "code", "execution_count": null, "id": "alias-code", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Alias LCU with persistent preparation workspace compiled.\n" ] } ], "source": [ "from guppyalgos.algorithms.state_preparation.alias_sampling import (\n", " AliasSamplingRegs, alias_samp_prep,\n", ")\n", "from guppyalgos.primitives.subroutines.fanout import (\n", " fanout_basic, fanout_measurement_parity,\n", ")\n", "\n", "alias_hamiltonian = larger_hamiltonian\n", "alias_data = LCUData.from_hamiltonian(alias_hamiltonian)\n", "alias_probabilities = np.abs(alias_data.coeffs) / alias_data.l1_norm\n", "unary_it_select = build_unary_iteration_select(alias_data)\n", "n_precision_qubits = 4\n", "alias_precision = 1 / 2**n_precision_qubits\n", "n_alias_index = alias_data.n_prep_qubits\n", "n_alias_state = alias_data.n_state_qubits\n", "\n", "\n", "def build_alias_lcu(fanout_op=fanout_basic):\n", " alias_prepare = alias_samp_prep(\n", " alias_probabilities, precision=alias_precision, fanout_op=fanout_op,\n", " )\n", "\n", " @guppy\n", " def prepare_alias(\n", " prep_qregs: AliasSamplingRegs[n_alias_index, n_precision_qubits],\n", " ) -> None:\n", " alias_prepare(\n", " prep_qregs.index, prep_qregs.alternative, prep_qregs.keep,\n", " prep_qregs.comparison, prep_qregs.comparison_result, False,\n", " )\n", "\n", " @guppy\n", " def select_alias(\n", " prep_qregs: AliasSamplingRegs[n_alias_index, n_precision_qubits],\n", " state_qreg: array[qubit, n_alias_state],\n", " ) -> None:\n", " unary_it_select(prep_qregs.index, state_qreg)\n", "\n", " @guppy\n", " def unprepare_alias(\n", " prep_qregs: AliasSamplingRegs[n_alias_index, n_precision_qubits],\n", " ) -> None:\n", " alias_prepare(\n", " prep_qregs.index, prep_qregs.alternative, prep_qregs.keep,\n", " prep_qregs.comparison, prep_qregs.comparison_result, True,\n", " )\n", "\n", " @guppy\n", " def encode_alias(\n", " prep_qregs: AliasSamplingRegs[n_alias_index, n_precision_qubits],\n", " state_qreg: array[qubit, n_alias_state],\n", " ) -> None:\n", " LCU(prepare_alias, select_alias, unprepare_alias).compose(\n", " prep_qregs, state_qreg,\n", " )\n", "\n", " return encode_alias\n", "\n", "\n", "alias_encode = build_alias_lcu()\n", "\n", "alias_encode.compile_function()\n", "print(\"Alias LCU with persistent preparation workspace compiled.\")\n" ] }, { "cell_type": "markdown", "id": "alias-check-intro", "metadata": {}, "source": [ "- `AliasSamplingRegs[3, 4]` contains:\n", " - a three-qubit index,\n", " - a three-qubit alternative index,\n", " - two four-qubit probability registers, and\n", " - one comparison flag.\n", "- These make **15 preparation qubits** in total.\n", "- Initialize all preparation qubits to zero and retain them through `SELECT` and `UNPREPARE`.\n", "- The encoded block projects all 15 preparation qubits onto zero.\n", "- `UNPREPARE` does not necessarily clear the workspace after `SELECT`. For this Hamiltonian acting on $|00\\rangle$, the comparison register remains nonzero with probability $3/16$.\n", "- Keep the workspace through subsequent coherent operations. Discarding it would remove coherence.\n" ] }, { "cell_type": "markdown", "id": "alias-fanout-intro", "metadata": {}, "source": [ "## 6. Change the alias QROM fanout\n", "\n", "Alias PREPARE uses QROM to load an alternative index and a keep threshold for each address. Its lookup XORs these words into two workspace registers:\n", "\n", "$$\n", "|j\\rangle|u\\rangle|v\\rangle\n", "\\longmapsto\n", "|j\\rangle|u\\oplus\\mathrm{alt}_j\\rangle|v\\oplus\\mathrm{keep}_j\\rangle.\n", "$$\n", "\n", "`alias_samp_prep(..., fanout_op=...)` chooses how the active address flag controls the stored one-bits. Its `build_alias_fanout` adapter gathers selected bits from both registers into one fanout call.\n", "\n", "- `fanout_basic`: sequential CNOTs.\n", "- `fanout_measurement_parity`: measurement-assisted fanout with feed-forward; four or more selected bits use extra ancillas. See the {doc}`parity LAQCC notebook ` for the underlying construction.\n", "\n", "Both implement the same lookup on arbitrary workspace states. This lets us change the QROM implementation without changing the probabilities, SELECT or LCU interface. UNPREPARE uses the alias routine's explicit inverse mode, including the same QROM choice." ] }, { "cell_type": "code", "execution_count": 7, "id": "alias-fanout-check", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Alias LCU with measurement-assisted QROM compiled.\n" ] } ], "source": [ "# Choose the fanout when constructing alias PREPARE.\n", "measurement_prepare = alias_samp_prep(\n", " alias_probabilities,\n", " precision=alias_precision,\n", " fanout_op=fanout_measurement_parity,\n", ")\n", "\n", "# The LCU factory forwards the same option to PREPARE and UNPREPARE.\n", "measurement_alias_encode = build_alias_lcu(\n", " fanout_op=fanout_measurement_parity,\n", ")\n", "measurement_alias_encode.compile_function()\n", "print(\"Alias LCU with measurement-assisted QROM compiled.\")" ] }, { "cell_type": "markdown", "id": "alias-fanout-note", "metadata": {}, "source": [ "`fanout_log` is another option when every alias-table row selects at least one bit. The current implementation does not support empty fanouts, so it is not a drop-in choice for arbitrary alias tables. Use the basic or parity implementation when rows may be all zero." ] } ], "metadata": { "kernelspec": { "display_name": "guppyalgos (3.14.x)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.14.7" } }, "nbformat": 4, "nbformat_minor": 5 }