Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/sphinx/api/python_api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ Trotter
Circuit primitives
==================

See :doc:`../guide/measured_qrom` for measured lookup cleanup strategies,
clean and borrowed workspace contracts, and logical resource accounting.

.. automodule:: cudaq_algorithms.primitives
:members:

Expand Down
86 changes: 86 additions & 0 deletions docs/sphinx/examples/python/measured_qrom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Measured clean QROM: verify a complex XOR map, then read a classical bit.

Run as a file with CUDA-Q 0.16: ``python measured_qrom.py``.
"""

# [Begin Documentation]
import os

import numpy as np
import cudaq
from cudaq_algorithms import state_from
from cudaq_algorithms.primitives import MeasuredQROM


def main():
cudaq.set_target(os.environ.get("CUDAQ_DEFAULT_SIMULATOR", "qpp-cpu"))
data = [2, 1, 3, 2, 0, 3, 1]
lookup = MeasuredQROM(data,
3,
2,
variant="select_swap",
block_size=2,
cache_uncompute="measure",
phase_block_size=2)
traversal = MeasuredQROM(data,
3,
2,
variant="select_swap",
block_size=2,
cache_uncompute="traverse")
automatic = MeasuredQROM(data,
3,
2,
variant="select_swap",
block_size=2,
cache_uncompute="auto")
print(f"cleanup strategies: {traversal.cache_uncompute}, "
f"{lookup.cache_uncompute}, {automatic.cache_uncompute}")
apply_lookup = lookup.kernel
n_ladder = lookup.num_ladder
rng = np.random.default_rng(27)
psi = rng.normal(size=32) + 1j * rng.normal(size=32)
psi /= np.linalg.norm(psi)
initial = state_from(psi)

@cudaq.kernel
def experiment(state: cudaq.State):
logical = cudaq.qvector(state)
ladder = cudaq.qvector(n_ladder)
apply_lookup(logical[0:3], ladder, logical[3:5])

# Address occupies the low three bits, then the output, then workspace.
# The independent reference preserves phases and pads missing entries.
reference = np.zeros(1 << (5 + n_ladder), complex)
for i, amplitude in enumerate(psi):
address = i & 7
word = data[address] if address < len(data) else 0
reference[i ^ (word << 3)] = amplitude
actual = np.asarray(cudaq.get_state(experiment, initial))
error = np.max(np.abs(actual - reference))
assert error < 1e-12

@cudaq.kernel
def read_bit() -> bool:
address = cudaq.qvector(3)
ladder = cudaq.qvector(n_ladder)
output = cudaq.qvector(2)
x(address[0])
x(address[1])
apply_lookup(address, ladder, output)
return mz(output[1]) # data[3] == 2: bit 1 is always one.

values = list(cudaq.run(read_bit, shots_count=16))
assert values == [True] * 16
print(lookup)
print(f"max complex-vector error: {error:.3e}")
print(
f"clean ancillas: {lookup.num_ladder}; tree ANDs: {lookup.tree_and_count}; "
f"persistent outcome bits: {lookup.resources.classical_bits}")
print(f"data[3] bit 1: {sum(values)}/{len(values)} ones")


if __name__ == "__main__":
main()
215 changes: 215 additions & 0 deletions docs/sphinx/guide/measured_qrom.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
Measured QROM
=============

``MeasuredQROM`` opts into measurement-based tree uncomputation and optionally
measurement-based cleanup of a known clean SELECT-SWAP cache.
It implements the same logical XOR lookup as ``QROM`` on clean workspace:

.. math::

|k\rangle |0\ldots0\rangle |y\rangle \longmapsto
|k\rangle |0\ldots0\rangle |y \mathbin{\mathrm{XOR}} d_k\rangle.

Addresses beyond the table read as zero. The address and output may have
arbitrary complex amplitudes and may be entangled. Registers are disjoint
little-endian views. The ``ladder`` view contains ``num_clean`` clean qubits followed by
``num_dirty`` borrowed qubits, with ``num_ladder`` their sum. The default
requires all workspace clean; blocked lookups also include cache registers.
Existing ``QROM`` consumers continue to use unitary kernels.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Existing QROM consumers continue to use unitary kernels."

House style in the docs is to say what a thing is rather than how it relates to what came before — e.g.
"QROM and unary_iteration_kernels are strictly unitary; MeasuredQROM and
measured_unary_iteration_kernels opt into measurement." Same for :125 ("preserves previous
choices"), primitives/__init__.py:15 ("stay strictly unitary"), and _qrom_measured.py:5-6, 20
("remain unitary", "preserves the original selection").


Construct and execute
---------------------

Mint the lookup outside the kernel and pass its kernel in. The complete
example below checks the raw complex output vector, including the clean
workspace, against an independent classical XOR permutation and then reads
one classical bit back with ``cudaq.run``:

.. literalinclude:: ../examples/python/measured_qrom.py
:language: python
:start-after: [Begin Documentation]

``cudaq.get_state`` and ``cudaq.observe`` are tested on CUDA-Q 0.16's
``qpp-cpu`` target. ``cudaq.run`` requires an entry kernel with a classical
return value, as above. ``cudaq.sample`` rejects measurement-feedback
kernels, direct or nested, on CUDA-Q 0.16; that rejection is CUDA-Q's
compiler check, not a check this package performs, and older CUDA-Q
releases accepted nested feedback while returning samples that are not the
logical lookup. A backend must support mid-circuit measurement and classical
feedback; these tests do not establish support on other backends or
hardware. The shared interpreter explicitly binds ``h``, ``mz``, ``x``,
``y``, ``z``, and ``cx`` so caller kernels with those names do not replace
its gates. Gate-name shadowing has dedicated regression coverage.

For a coherent external control, construct with ``controlled=True`` and call
``lookup(control, address, ladder, output)``, where ``control`` is a one-qubit
view; the control is folded into the tree root. Do not apply ``cudaq.control``
to a measured kernel: on CUDA-Q 0.16 it compiles and runs without any error,
but the mid-circuit measurements cannot be controlled, so they act on the
caller's control register: a fix-up that fires collapses a superposed control
to one, and one that does not fire attenuates the control-on branch, so the
resulting state differs from the controlled lookup and from run to run.
``cudaq.adjoint`` of a measured kernel raises. ``controlled=True`` and the
walker's ``kernel_adj`` are the only supported forms.

Applying the lookup twice restores the logical state when the workspace
satisfies the declared clean-prefix and borrowed-suffix contract.
``measured_unary_iteration_kernels`` also exposes ``kernel_adj`` for
noncommuting bodies. That kernel reverses the logical SELECT map on clean
workspace. It does not invert a quantum channel on arbitrary dirty inputs.

Constructions and resource counts
---------------------------------

``variant="select"`` directly walks the table and XORs each word into the
output. ``variant="select_swap"`` walks the high address bits, loads a clean
bank of ``B`` words, routes one word to slot zero using the low address bits,
copies it into the output, and reverses routing. The default
``cache_uncompute="traverse"`` runs a second measured walk to restore the bank. ``variant="auto"`` minimizes the emitted Toffoli count,
preferring SELECT on ties and otherwise the smaller block. An explicit block
size must be a power of two between 2 and ``2**(address_bits-1)``.

For ``n`` address bits, ``m`` entries, ``b`` output bits, and ``c`` equal to
1 for an external control and 0 otherwise, define

.. math::

C(n,m,c) = m + n - 2 + c - \operatorname{popcount}(m-1).

With ``h = n-log2(B)`` and ``M = ceil(m/B)``, the exact logical counts are:

.. list-table::
:header-rows: 1

* - Construction
- Toffolis
- Measurements / tree ANDs
- Clean / borrowed workspace
* - SELECT
- ``C(n,m,c)``
- ``C(n,m,c)``
- ``n / 0``
* - SELECT-SWAP
- ``2*C(h,M,c) + 2*b*(B-1)``
- ``2*C(h,M,c)``
- ``(h + B*b) / 0``
* - SELECT-SWAP, dirty bank
- ``2*C(h,M,c) + 4*b*(B-1)``
- ``2*C(h,M,c)``
- ``h / (B*b)``

Set ``dirty_banks=True`` only with explicit ``variant="select_swap"`` to
borrow its bank. The first ``h`` workspace lines still start and end at zero;
the bank suffix may be in any state, including entangled with the address,
output, control, or an external reference. Both portions are restored. An
additional routing/copy/unrouting echo removes the borrowed selected word
that the first copy included. In circuit order the sequence is
``W S C S^-1 W S C S^-1``. Writing ``D_r`` for the selected initial bank
word, the output receives ``D_r XOR data[k]`` and then ``D_r``, leaving only
the desired data. This basis-permutation identity also preserves arbitrary
entanglement. With the external control off, the two copies of ``D_r``
cancel. The echo doubles routing cost but leaves tree measurements unchanged.

Clean-cache measurement
-----------------------

For explicit ``variant="select_swap"``, ``cache_uncompute="measure"``
replaces the final bank-writing traversal by X-basis measurement, conditional
X reset, and a data-dependent phase correction. It requires a known clean
initial bank; ``dirty_banks=True`` rejects this mode. Measuring an unknown
borrowed bank would destroy its state and its entanglement. The dirty echo
above remains the implementation for borrowed banks.

``cache_uncompute="auto"`` considers measurement for clean blocked candidates
and chooses it only for a strict Toffoli saving; traversal wins cleanup ties.
The default ``"traverse"`` preserves previous choices. SELECT has no cache
and reports ``cache_uncompute == "none"``. Automatic variant selection still
prefers SELECT on ties and otherwise the smaller block.

Write ``w=B*b`` and choose ``K=2**s`` with
``1 <= K <= min(w, 2**(h-1))``. Explicit ``phase_block_size=K`` is accepted
only with explicit SELECT-SWAP and measurement cleanup. Otherwise measurement
chooses the K minimizing correction Toffolis, preferring smaller K on ties.
K=1 uses a direct phase walk; K>1 reuses K erased cache lines as a one-hot
register. No additional quantum lines are needed, but ``w`` classical
outcome slots persist through the correction, freshly initialized on every
invocation. This is retained buffer capacity, not total classical memory.

After unrouting, cache slot i contains the controlled table word at
``B*j+i``. Let F(j) pack these words little-endian, zero-padding missing
entries and blocks. For cache outcomes z, X-measurement contributes

.. math::

2^{-w/2}(-1)^{c\langle z,F(j)\rangle},

where here c is the quantum control bit (or one for an uncontrolled lookup).
The phase oracle applies the same sign to cancel it. Outcomes therefore reveal
no address or control information. It depends on the high address j, because
routing has already been undone. The output is never measured. One-hot
cleanup restores each split's parent with CX, then erases its temporary AND
with H, measurement, CZ feedback, and X reset. This remains valid after phase
correction because the one-hot support relations are unchanged.

For the resource formulas, c again denotes whether the construction has an
external control. Set ``L=C(h,M,c)``, ``P=C(h-s,ceil(M/K),c)``, and
``R=2*b*(B-1)``. Measurement cleanup emits:

* Toffolis: ``L+R+P+(K-1)``.
* Measurements: ``L+w+P+(K-1)``.
* Tree ANDs: ``L+P``; all temporary ANDs: ``L+P+(K-1)``.
* Clean workspace: ``h+w``; persistent classical bits: ``w``.

For ``n=8, m=256, b=2, B=4, K=8`` without external control,
``L=62, P=6, R=12``. The stages are load (62 Toffolis, 62 measurements),
route/copy/unroute (12, 0), cache erasure (0, 8), one-hot preparation
(7, 0), phase walk (6, 6), and one-hot cleanup (0, 7).
The total is **87 Toffolis, 83 measurements, and 14 clean qubits**, versus
**136 Toffolis, 124 measurements, and 14 clean qubits** for traversal.
These are exact emitted logical counts, not a hardware benchmark.

``resources`` exposes immutable totals and stage records, including
``classical_bits`` and ``temporary_and_count``. ``toffoli_count`` counts
all emitted Toffolis; ``tree_and_count`` excludes routing and one-hot
preparation. ``num_measurements`` includes cache and one-hot erasures.
The four-T estimate applies to proven temporary ANDs under compatible
synthesis; emitted ordinary Toffolis do not establish compiled T counts.
Conditional Z/CZ counts depend on outcomes. Compiler resource reports follow
a branch choice, so unconditional Toffoli and measurement totals are the
appropriate comparison. Automatic selection optimizes Toffolis only, not
measurement latency, classical parity work, Clifford counts, or hardware cost.
The K bound excludes zero-width correction trees and additional workspace;
no optimum over other architectures is claimed.

On a full tree, ``C = 2**n - 2 + c``. For a partial table, this implementation
preserves the full address space, including a no-op outside the table. The
partial-tree formula is therefore distinct from the ``L-1`` accounting under
an input promise restricting addresses to the visited range. A single entry
can still require ``n-1+c`` tree ANDs to test its full address.

Scope and evidence
------------------

Tests cover arbitrary complex XOR inputs, padded tables, explicit coherent
controls, borrowed-bank restoration with spectator entanglement,
noncommuting walker bodies and their logical inverse, controlled
work registers, and a nonzero address-coherence-sensitive observable. Small
walker and complete cache-erasure plans additionally enumerate every measurement
history and check each unnormalized Kraus isometry against independent direct
SELECT or XOR maps. Cache fixtures cover both coherent control modes, K=1 and
K=2, partial/full tables, and repeated application. This host all-history
evidence is distinct from native sampled trajectories.
Repeated native trajectories test the CUDA-Q implementation as a separate
layer. The runnable example is a statevector check, not a hardware benchmark.

The tree follows `Babbush et al., Section III.A and Figures 4–7
<https://arxiv.org/abs/1805.03662>`_; the temporary-AND erasure follows
`Gidney, Figure 3 <https://arxiv.org/abs/1709.06648>`_. The clean blocked lookup
composes the SELECT-SWAP construction of `Low, Kliuchnikov and Schaeffer
<https://arxiv.org/abs/1812.00954>`_ with those measured tree traversals.

The cache X-erasure, phase table, and one-hot cleanup follow the construction
in `Berry et al., Appendix C, Theorem 3 and Figures 5–10
<https://arxiv.org/abs/1902.02134>`_. The formulas above account separately
for this API's padding and coherent-control contract; they do not substitute
the paper's auxiliary-register bounds for this implementation's counts.
1 change: 1 addition & 0 deletions docs/sphinx/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ kernels.
guide/trotter
guide/qubitization_qsvt
guide/state_prep
guide/measured_qrom

.. toctree::
:caption: Examples
Expand Down
22 changes: 16 additions & 6 deletions python/cudaq_algorithms/primitives/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
unary-iteration SELECT or as the SELECT-SWAP / QROAM construction of
Low, Kliuchnikov and Schaeffer (`arXiv:1812.00954`).

Everything here is strictly unitary: the papers' headline Toffoli counts
assume measurement-based ancilla uncomputation, which this library does
not use (primitives must stay statevector-testable and
inverse-composable); the module docstrings state the coherent costs the
minted kernels actually have, and the resource tests pin them against
the compiler.
``QROM`` and the original unary-iteration factory stay strictly unitary.
``MeasuredQROM`` and ``measured_unary_iteration_kernels`` explicitly opt
into measurement-based tree uncomputation on clean tree ancillas, with
factory-built controls and, for SELECT-SWAP, an optional borrowed bank
that may start in any state, entangled or not. Their logical action is
unitary on the declared workspace subspace (clean prefix at zero, bank
arbitrary) — for ``MeasuredQROM`` a basis permutation — so coherence and
entanglement of the inputs are preserved; automatic control/adjoint and
sampling are outside the measured API contract. See
:doc:`../guide/measured_qrom` for execution and resource conventions.

The reversible integer arithmetic device kernels (:mod:`._arithmetic`)
are the in-place little-endian adders and comparators the lookup-based
Expand Down Expand Up @@ -51,11 +55,17 @@
subtract_constant, subtract_constant_qft,
subtract_register)
from ._qrom import QROM

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from ._qrom import QROM

QROM.resources and MeasuredQROM.resources return QROMResources (with QROMStageResources records) and
the guide cites their fields (measured_qrom.rst:171-174), but neither type is exported here, so
automodule :members: cannot render them. Export both (or document the fields in the guide).

from ._qrom_measured import MeasuredQROM
from ._unary_iteration import UnaryIterationKernels, unary_iteration_kernels
from ._unary_iteration_measured import (MeasuredUnaryIterationKernels,
measured_unary_iteration_kernels)

__all__ = [
"AliasSamplingPrepare",
"QROM",
"MeasuredQROM",
"MeasuredUnaryIterationKernels",
"measured_unary_iteration_kernels",
"UnaryIterationKernels",
"add_constant",
"add_constant_qft",
Expand Down
Loading
Loading