{ "cells": [ { "cell_type": "markdown", "id": "title", "metadata": {}, "source": [ "# THC qubitized phase estimation\n", "\n", "**Download Notebook** - {nb-download}`thc_qpe.ipynb`\n", "\n", "This example composes canonical quantum phase estimation (QPE) with the controlled qubitization walk for the BLISS-THC Hamiltonian in [Caesura et al., arXiv:2501.06165](https://arxiv.org/abs/2501.06165). It uses 16 spatial orbitals and THC rank 16: a non-trivial instance containing 152 alias-sampled coefficients and 15 neighboring Givens rotations in each orbital transformation.\n", "\n", "The implementation is adapted from the PREPARE and Select circuit construction in the original paper. Here, that construction is expressed using the repository's reusable alias-sampling, `SelectTHCCntrl`, controlled-LCU, qubitization, and QPE components.\n", "\n", "The complete circuit is too large for practical statevector simulation. Its constituent algorithms are tested numerically on smaller systems, while this notebook serves as an integration example: it assembles the full workflow at a realistic size and checks that the resulting Guppy program compiles.\n", "\n", "The construction is layered rather than monolithic:\n", "\n", "```text\n", "THC coefficients and rotations\n", " ↓\n", "PREPARE + controlled SELECT + UNPREPARE\n", " ↓\n", "LCUCntrl\n", " ↓\n", "controlled qubitization = controlled LCU + controlled reflection\n", " ↓\n", "QPE powered oracle\n", "```\n", "\n", "Each layer has a typed interface. The classical THC data can come from a chemistry workflow, and any signature-compatible PREPARE or SELECT can replace the generated oracle. All alias and Select registers remain coherent between walk steps. The reflection tests the full alias workspace. The SELECT record is cleared by its inverse QROM lookup before alias UNPREPARE; the phase-gradient resource stays outside the reflection." ] }, { "cell_type": "code", "execution_count": 1, "id": "imports", "metadata": {}, "outputs": [], "source": [ "from typing import no_type_check\n", "\n", "from guppylang import guppy\n", "from guppylang.std.builtins import array, nat, output\n", "from guppylang.std.quantum import (\n", " collect_measurements,\n", " discard,\n", " discard_array,\n", " h,\n", " measure_array,\n", " qubit,\n", " x,\n", " cx,\n", ")\n", "\n", "from guppyalgos.primitives.gate_decompositions.cnx.cnx import cnx\n", "from guppyalgos.algorithms.block_encoding.lcu import LCUCntrl\n", "from guppyalgos.algorithms.phase_estimation import qpe\n", "from guppyalgos.primitives.gate_decompositions.cnx.cnz import cnz\n", "from guppyalgos.primitives.rotations import GivensCascadePhaseGradient\n", "from guppyalgos.algorithms.state_preparation.alias_sampling import (\n", " AliasSamplingRegs,\n", " alias_samp_prep,\n", ")\n", "from guppyalgos.primitives.state_preparation.phase_gradient import Convention, phase_gradient\n", "from guppyalgos.algorithms.block_encoding.thc import (\n", " SelectTHCCntrl,\n", " SelectTHCCntrlRegs,\n", " THCWalkTargetRegs,\n", " build_thc_lcu_data,\n", " generate_thc_parameters,\n", " load_select_registers,\n", ")\n", "from guppyalgos.utils import qarray, transversal" ] }, { "cell_type": "markdown", "id": "input-description", "metadata": {}, "source": [ "## 1. Supply classical THC data\n", "\n", "`THCParameters` is the boundary between classical preprocessing and circuit construction. The coefficient arrays specify the one- and two-body weights; each row of the rotation arrays contains the neighboring-Givens angles for one orbital transformation. Angles are expressed as fractions of a full turn.\n", "\n", "To place both kinds of coefficient in one alias-sampling table, picture the one-body terms as an extra column appended to the symmetric two-body matrix. The public term table represents entries in this column with `nu=None`; only the internal QROM encoding replaces `None` with the paper's reserved integer `nu=M`. A one-body term therefore still has only the meaningful index `mu`.\n", "\n", "For this standalone example, `generate_thc_parameters` supplies deterministic non-zero data. A chemistry application would replace this one call with coefficients and rotations obtained from its classical factorization workflow. With 16 orbitals and rank 16, the flattened LCU contains 136 two-body coefficients and 16 one-body coefficients." ] }, { "cell_type": "code", "execution_count": 2, "id": "parameters", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "{'orbitals': 16,\n", " 'THC rank': 16,\n", " 'LCU coefficients': 152,\n", " 'Givens rotations per transform': 15,\n", " 'alias index qubits': 8}" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n_orbitals = 16\n", "thc_rank = 16\n", "parameters = generate_thc_parameters(n_orbitals, thc_rank, seed=26)\n", "\n", "thc_lcu_data = build_thc_lcu_data(\n", " parameters,\n", " rotation_precision_bits=8,\n", " alias_precision_bits=12,\n", ")\n", "\n", "{\n", " \"orbitals\": n_orbitals,\n", " \"THC rank\": thc_rank,\n", " \"LCU coefficients\": len(thc_lcu_data.alias_probabilities),\n", " \"Givens rotations per transform\": thc_lcu_data.n_givens,\n", " \"alias index qubits\": thc_lcu_data.n_alias_qubits,\n", "}" ] }, { "cell_type": "markdown", "id": "components-description", "metadata": {}, "source": [ "## 2. Inspect the composable LCU oracles\n", "\n", "`build_thc_lcu_data` is limited to classical preprocessing:\n", "\n", "- It normalizes the THC coefficient magnitudes.\n", "- It encodes the Select records and Givens angles as QROM tables.\n", "- It returns the resulting Guppy QROMs and register dimensions.\n", "- It does **not** choose a rotation method or construct PREPARE, SELECT, or UNPREPARE.\n", "\n", "This notebook makes the quantum choices explicitly: alias-sampling PREPARE and a phase-gradient Givens cascade. Another compatible Select or rotator could use the same preprocessed data. The main returned values are:\n", "\n", "- `alias_probabilities` contains the normalized coefficient magnitudes used to construct alias-sampling PREPARE below.\n", "- `select_data_loader` is the precomputed QROM for the sampled THC indices and flags.\n", "- `qrom_1_and_2_body` and `qrom_2_body` load the combined and two-body Givens angles.\n", "- The standard `AliasSamplingRegs` and `SelectTHCCntrlRegs` types compose into the PREPARE register below.\n", "\n", "Below, `alias_samp_prep` constructs PREPARE from the normalized magnitudes. The phase-gradient preparation and cascade are also selected here, outside the data builder." ] }, { "cell_type": "code", "execution_count": 3, "id": "components", "metadata": {}, "outputs": [], "source": [ "alias_prepare = alias_samp_prep(\n", " thc_lcu_data.alias_probabilities,\n", " thc_lcu_data.alias_precision,\n", ")\n", "select_data_qrom = thc_lcu_data.select_data_loader\n", "qrom_1_and_2_body = thc_lcu_data.qrom_1_and_2_body\n", "qrom_2_body = thc_lcu_data.qrom_2_body\n", "\n", "n_alias_qubits = thc_lcu_data.n_alias_qubits\n", "n_index_qubits = thc_lcu_data.n_index_qubits\n", "n_keep_qubits = thc_lcu_data.n_keep_qubits\n", "n_modes = thc_lcu_data.n_modes\n", "n_givens = thc_lcu_data.n_givens\n", "n_phase_qubits = thc_lcu_data.rotation_precision_bits\n", "\n", "prepare_phase_gradient = phase_gradient(\n", " n_phase_qubits,\n", " convention=Convention.Standard,\n", ")\n", "\n", "@guppy.struct\n", "class THCPrepareRegs:\n", " \"\"\"Persistent preparation registers and phase-gradient resource.\"\"\"\n", "\n", " alias_sampling: AliasSamplingRegs[n_alias_qubits, n_keep_qubits]\n", " select: SelectTHCCntrlRegs[n_index_qubits]\n", " phase_gradient: array[qubit, n_phase_qubits]\n", "\n", "@guppy\n", "@no_type_check\n", "def prepare(regs: THCPrepareRegs) -> None:\n", " \"\"\"Prepare the alias distribution and load its THC Select record.\"\"\"\n", " alias_prepare(\n", " regs.alias_sampling.index,\n", " regs.alias_sampling.alternative,\n", " regs.alias_sampling.keep,\n", " regs.alias_sampling.comparison,\n", " regs.alias_sampling.comparison_result,\n", " False,\n", " )\n", " load_select_registers(select_data_qrom, regs.alias_sampling.index, regs.select)\n", "\n", "@guppy\n", "@no_type_check\n", "def cntrl_select(\n", " control: qubit,\n", " prep_regs: THCPrepareRegs,\n", " target_regs: THCWalkTargetRegs[n_modes],\n", ") -> None:\n", " \"\"\"Apply the THC Select using the prepared phase-gradient state.\"\"\"\n", " cascade = GivensCascadePhaseGradient(prep_regs.phase_gradient)\n", " select = SelectTHCCntrl(qrom_1_and_2_body, qrom_2_body, cascade, cnx)\n", " select.compose(control, prep_regs.select, target_regs)\n", " prep_regs.phase_gradient = select.cascade.phase_gradient\n", "\n", "@guppy\n", "@no_type_check\n", "def unprepare(regs: THCPrepareRegs) -> None:\n", " \"\"\"Reverse the Select-record lookup and alias preparation.\"\"\"\n", " load_select_registers(select_data_qrom, regs.alias_sampling.index, regs.select)\n", " alias_prepare(\n", " regs.alias_sampling.index,\n", " regs.alias_sampling.alternative,\n", " regs.alias_sampling.keep,\n", " regs.alias_sampling.comparison,\n", " regs.alias_sampling.comparison_result,\n", " True,\n", " )\n", "\n", "@guppy.struct\n", "class THCQPERegs:\n", " \"\"\"Persistent quantum registers used by THC QPE.\"\"\"\n", "\n", " prep: THCPrepareRegs\n", " target: THCWalkTargetRegs[n_modes]" ] }, { "cell_type": "markdown", "id": "block-encoding-description", "metadata": {}, "source": [ "## 3. Build the controlled block encoding\n", "\n", "`THCQPERegs` retains the complete preparation bundle, spin registers and phase-gradient state. `LCUCntrl` applies PREPARE, controlled SELECT and UNPREPARE to that bundle.\n", "\n", "UNPREPARE reverses the preparation circuit, but SELECT can leave its workspace entangled with the system. It does **not** generally return the alias workspace to zero. We therefore allocate the registers once and retain them throughout QPE; nothing is discarded between walk steps.\n" ] }, { "cell_type": "code", "execution_count": 4, "id": "block-encoding", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "@no_type_check\n", "def cntrl_block_encoding(control: qubit, regs: THCQPERegs) -> None:\n", " LCUCntrl(prepare, cntrl_select, unprepare).compose(\n", " control, regs.prep, regs.target\n", " )" ] }, { "cell_type": "markdown", "id": "walk-description", "metadata": {}, "source": [ "## 4. Reflect the alias preparation state\n", "\n", "- The reflection flips the phase when **the full alias workspace is zero** and the external control is one:\n", "\n", "$$\n", "R=I-2\\Pi,\\qquad\n", "\\Pi=|0\\rangle\\langle0|_{\\mathrm{alias}}\\otimes I_{\\mathrm{SELECT,system,gradient}}.\n", "$$\n", "\n", "### Why SELECT registers are excluded\n", "\n", "- PREPARE loads the SELECT record from the alias index using `load_select_registers`.\n", "- `SelectTHCCntrl` preserves that record: it reverses its temporary index swaps and restores its control flags.\n", "- UNPREPARE repeats the QROM lookup with the unchanged alias index, clearing the SELECT record **before** reversing alias preparation:\n", "\n", "$$\n", "|j\\rangle|d(j)\\rangle|\\psi_j\\rangle\n", "\\longmapsto |j\\rangle|0\\rangle|\\psi_j\\rangle.\n", "$$\n", "\n", "- The SELECT registers therefore return to zero at each reflection boundary and need no zero-test flags in this construction.\n", "- Alias PREPARE entangles the index with its workspace (the “junk” registers). SELECT acts differently on each selected term, so applying alias UNPREPARE afterward does not generally clear that workspace: it can remain entangled with the system.\n", "- The walk must therefore reflect about the **joint all-zero alias state**, including the index and every junk register. Reflecting only on the index would also flip components whose index is zero but whose workspace is nonzero, giving a different walk operator.\n", "- The five flags implement this joint test while keeping the registers separate. They are computed coherently and then uncomputed; measuring them would collapse the superposition. Flags are an implementation choice—the requirement is to test the full alias workspace.\n", "- The phase-gradient resource is excluded because it is prepared in a phase-gradient state, not the all-zero state.\n", "\n", "### Five temporary zero-test flags\n", "\n", "| Flag | Alias register tested |\n", "| :-- | :-- |\n", "| 0 | Index. |\n", "| 1 | Alternative index. |\n", "| 2 | Keep threshold. |\n", "| 3 | Comparison register. |\n", "| 4 | Comparison-result qubit. |\n", "\n", "- Each flag starts at zero and becomes one exactly when its associated register is zero:\n", "\n", "$$\n", "|r_j\\rangle|0\\rangle_{f_j}\\longmapsto\n", "|r_j\\rangle|[r_j=0]\\rangle_{f_j}.\n", "$$\n", "\n", "- `zero_flag` uses X gates to turn an all-zero condition into an all-one condition for `cnx`, then restores the input bits. A single qubit uses X on its flag followed by CX.\n", "- **Compute** the five flags without measuring the preparation registers.\n", "- **Apply** `cnz(flags, control, cnx)`, contributing the phase $(-1)^{c f_0f_1f_2f_3f_4}$.\n", "- **Uncompute** by repeating the flag routine, restoring the flags to zero before discarding them. The phase change remains.\n" ] }, { "cell_type": "code", "execution_count": 5, "id": "walk", "metadata": {}, "outputs": [], "source": [ "@guppy\n", "def zero_flag_array[n: nat](qreg: array[qubit, n], flag: qubit) -> None:\n", " transversal(x, qreg)\n", " cnx(qreg, flag)\n", " transversal(x, qreg)\n", "\n", "\n", "@guppy\n", "def zero_flag_qubit(q: qubit, flag: qubit) -> None:\n", " x(flag)\n", " cx(q, flag)\n", "\n", "\n", "@guppy.overload(zero_flag_array, zero_flag_qubit)\n", "def zero_flag():\n", " \"\"\"Toggle a flag when a qubit or register is zero.\"\"\"\n", "\n", "\n", "@guppy\n", "def preparation_zero_flags(regs: THCPrepareRegs, flags: array[qubit, 5]) -> None:\n", " zero_flag(regs.alias_sampling.index, flags[0])\n", " zero_flag(regs.alias_sampling.alternative, flags[1])\n", " zero_flag(regs.alias_sampling.keep, flags[2])\n", " zero_flag(regs.alias_sampling.comparison, flags[3])\n", " zero_flag(regs.alias_sampling.comparison_result, flags[4])\n", "\n", "\n", "\n", "@guppy\n", "def reflect_preparation(control: qubit, regs: THCPrepareRegs) -> None:\n", " flags = qarray(5)\n", " preparation_zero_flags(regs, flags)\n", " cnz(flags, control, cnx)\n", " preparation_zero_flags(regs, flags)\n", " discard_array(flags)\n", "\n", "\n", "@guppy\n", "def cntrl_walk_power(control: qubit, unitary_regs: THCQPERegs, power: int) -> None:\n", " for _ in range(power):\n", " cntrl_block_encoding(control, unitary_regs)\n", " reflect_preparation(control, unitary_regs.prep)\n" ] }, { "cell_type": "markdown", "id": "compile-description", "metadata": {}, "source": [ "## 5. Compose with canonical QPE\n", "\n", "A three-qubit phase register asks the powered oracle for walk powers 1, 2, and 4 before applying the inverse QFT. A useful energy estimate requires preparing an eigenstate, or a state with significant overlap with an eigenstate, in the spin registers. This example leaves them in the computational zero state because its purpose is to demonstrate and type-check the complete THC-QPE composition.\n", "\n", "The resulting circuit is intentionally too large for a useful notebook statevector test. `check()` validates types and qubit ownership. It does not prove that a register is zero or that the compiled circuit implements the intended Hamiltonian. Registers are discarded only after QPE and the phase measurement have finished." ] }, { "cell_type": "code", "execution_count": 6, "id": "compile", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "'THC QPE with persistent preparation workspace type checked successfully.'" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "n_qpe_qubits = 3\n", "\n", "\n", "@guppy\n", "@no_type_check\n", "def main() -> None:\n", " phase_qreg = qarray(n_qpe_qubits)\n", " alias_regs = AliasSamplingRegs(\n", " qarray(n_alias_qubits), qarray(n_alias_qubits),\n", " qarray(n_keep_qubits), qarray(n_keep_qubits), qubit(),\n", " )\n", " select_regs = SelectTHCCntrlRegs(\n", " qubit(), qubit(), qarray(n_index_qubits), qarray(n_index_qubits),\n", " )\n", " gradient_qreg = qarray(n_phase_qubits)\n", " prepare_phase_gradient(gradient_qreg)\n", " unitary_regs = THCQPERegs(\n", " THCPrepareRegs(alias_regs, select_regs, gradient_qreg),\n", " THCWalkTargetRegs(qarray(n_modes), qarray(n_modes)),\n", " )\n", " transversal(h, phase_qreg)\n", " qpe(phase_qreg, unitary_regs, cntrl_walk_power)\n", " output(\"phase\", collect_measurements(measure_array(phase_qreg)))\n", "\n", " # QPE is finished: these registers are no longer needed.\n", " discard_array(unitary_regs.prep.alias_sampling.index)\n", " discard_array(unitary_regs.prep.alias_sampling.alternative)\n", " discard_array(unitary_regs.prep.alias_sampling.keep)\n", " discard_array(unitary_regs.prep.alias_sampling.comparison)\n", " discard(unitary_regs.prep.alias_sampling.comparison_result)\n", " discard(unitary_regs.prep.select.one_body_flag)\n", " discard(unitary_regs.prep.select.coefficient_sign)\n", " discard_array(unitary_regs.prep.select.first_index_qreg)\n", " discard_array(unitary_regs.prep.select.second_index_qreg)\n", " discard_array(unitary_regs.prep.phase_gradient)\n", " discard_array(unitary_regs.target.spin_up)\n", " discard_array(unitary_regs.target.spin_down)\n", "\n", "\n", "main.check()\n", "\"THC QPE with persistent preparation workspace type checked successfully.\"" ] } ], "metadata": { "kernelspec": { "display_name": "guppyalgos (3.12.13)", "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.12.13" } }, "nbformat": 4, "nbformat_minor": 5 }