Skip to content

Add an externally controlled QROM lookup - #70

Merged
wsttiger merged 2 commits into
NVIDIA:mainfrom
wsttiger:features/controlled_qrom
Oct 29, 2026
Merged

wsttiger merged 2 commits into
NVIDIA:mainfrom
wsttiger:features/controlled_qrom

Conversation

@wsttiger

@wsttiger wsttiger commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

QROM gains controlled=True: the externally controlled lookup, with the
unary_iteration_kernels convention — a leading one-qubit control view,
identity at control |0>, the plain lookup at |1>, self-inverse. controlled
must be a bool (same guard and message as the measured-QROM work).

Why it is cheap. Only the two walk passes carry the control. In the composed
select_swap (#52) the walk sub-kernel is minted controlled=True and the parent
passes the control view to both walk passes; the routing network and the copy are
the plain kernels. At control |0> a controlled walk on a clean ladder writes
nothing, so the block registers stay |0>, the routing swaps |0> registers, the
copy fires from a |0> slot, and the un-route/un-write are the identity. The
Toffoli price is the controlled walk count in place of the plain one (the root
gadgets become Toffolis and the root crossings lose the unguarded shortcut: +4 for
the 3-bit, 5-entry test table on select, +6 on select_swap with B = 2), with
routing and copy unchanged — a fraction of cudaq.control over the composition,
which adds a control to every gate. variant="auto" prices the controlled
candidates against each other; the control shifts the optimum toward larger blocks
on small tables.
Compiled on CUDA-Q 0.16 for eleven 3-bit entries over four address bits, the native
controlled lookup costs 19 / 26 / 28 Toffolis (select, select_swap B = 2, B = 4)
against 15 / 18 / 22 plain, where cudaq.control over the plain composition costs 42
/ 77 / 83. Because the optimum moves to larger blocks under control, num_ladder can
exceed the plain lookup's; size the ladder from the instance.

What "identity at |0>" means. A clean-ladder statement: every ladder write of
a controlled walk is gated by the control or by a ladder line above it, so on a
dirty ladder the deeper Toffolis fire. The clean ladder is the kernel's
precondition in any case, but cudaq.control of the plain lookup is the identity
on every input, so the docstrings say which identity this is.

Who needs it. The alias PREPARE deliberately never puts QROM under control, so
the LCU encodings do not. The consumer is the sparse-access oracle path (#53):
from_general_oracles wraps the oracles in cudaq.control, which excludes
table-oracle bundles built on walks; a natively controlled lookup is what lets that
path accept them (follow-up in #53), under the clean-ladder precondition those
bundles already carry.

Tests (test_primitives_qrom.py +9, test_primitives_resources.py +3): control
conventions at |0>, |1> and |+> and the involution under a superposed control
over six geometries — both variants, a one-bit address, a single block-index bit
(B = 2^(address_bits − 1)), a zero-padded last block at B = 8 — on superposed
addresses with a preloaded output word, the |1> branch compared with the table
itself; Toffoli counts against the documented contract; auto pricing pinned on a
table whose optimum moves (plain B = 2, controlled B = 4); the bool guard; and
the resource suite's compiler-measured Toffoli cross-check over the controlled
lookups. Existing QROM, unary-iteration, composition and alias-sampling behaviour
unchanged.

Note for #64. The measured-QROM PR rewrites _qrom.py; its coherent half can
take the composed select_swap with this flag wholesale (MeasuredQROM already
carries a controlled flag of the same shape).

Files

  • python/cudaq_algorithms/primitives/_qrom.py
  • python/cudaq_algorithms/primitives/__init__.py (docstring line)
  • tests/python/test_primitives_qrom.py
  • tests/python/test_primitives_resources.py

@wsttiger wsttiger added enhancement New feature or request release - v0.2 Targeted for the v0.2 release labels Oct 21, 2026
@wsttiger
wsttiger requested review from kvmto and smcardleQ October 23, 2026 02:45
@wsttiger
wsttiger requested a review from ivanbasov October 26, 2026 18:32
wsttiger added a commit that referenced this pull request Oct 27, 2026
…ract (cudaq >= 0.16) (#52)

## Summary

Rebuilds the `select_swap` QROM as composed kernels and promises
composition as
a supported surface of the primitives subpackage.

1. **`select_swap` as composed kernels.** The lookup is a parent kernel
that
carves `low`/`high` address views and `lines`/`blocks` ancilla views out
of
   the public `(address, ladder, output)` signature and composes three
sub-kernels, `W S C S⁻¹ W`: a plain `unary_iteration_kernels` walk over
the
block index whose target view is the block registers, a minted Fredkin
routing network over `(low address bits, blocks)` and its reversed-list
inverse, and the copy CNOTs inline. The flat-tape splice — rebasing the
   walk's address operands and smuggling the block registers through the
walk's ladder view — and the `_OP_CX_LADDER_TARGET` opcode are deleted
end
to end. Public signature, `num_ladder` and `toffoli_count` are unchanged
   (`AliasSamplingPrepare` depends on them).

2. **Composition as a supported surface.** The package docstring states
the
contract: minted kernels are building blocks callable from user kernels,
and
`cudaq.control` over the whole composition yields the controlled
operation
(the `controlled=True` factory variants remain the cheaper route for a
controlled walk). `tests/python/test_primitives_composition.py` pins
both
halves: plain composition, and controlled composition three-way against
the
factory's controlled walk and the analytic state on a superposed
control.

3. **Version gate and docstrings.** Controlled composition is pinned on
the
supported CUDA-Q line through a `controlled_composition` pytest marker
(`conftest.py`): the tests skip only below 0.16.0, the package minimum,
and
fail on a 0.16.x that regresses control propagation. Docstrings describe
   what composition and the flat tape are.

## Validation

-
`test_primitives_{qrom,composition,interpreter,unary_iteration,resources,alias_sampling,arithmetic}.py`:
  274 passed on cudaq 0.16.0 (nothing skipped); 270 passed / 4 skipped
  (the marker-gated controlled-composition tests) on cudaq 0.15.1.
- The composed `select_swap` reproduces the previous construction
exactly:
full statevectors identical and equal Toffoli counts on five geometries
(block sizes 2–8, one-high-bit and zero-padded cases, 32 entries over 5
bits).
- `cudaq.estimate_resources` on the composed kernel equals
`toffoli_count`
  (resource suite, block sizes 2, 4, 8).

## Interplay

- #70 (`QROM(..., controlled=True)`) threads its flag through the
flat-tape
  `select_swap`; under this construction the controlled lookup is a
`controlled=True` walk sub-kernel taking `(control, high, lines,
blocks)` with
the routing and copy unchanged, no `cudaq.control` needed (verified:
identical
conventions and the documented controlled Toffoli price). Whichever
lands
second carries that ~15-line rewrite; #70's tests use only the public
API and
  transfer unchanged.
- #64 (measured QROM) rebuilds `_qrom.py` around a flat IR; its coherent
half
can take this composed `select_swap` wholesale, and its measured path
keeps
  its own opcode copies.

## Files
- `python/cudaq_algorithms/primitives/__init__.py`, `_qrom.py`,
`_unary_iteration.py`, `_alias_sampling.py`
- `tests/python/conftest.py`, `test_primitives_composition.py` (new),
`test_primitives_qrom.py`,
  `test_primitives_interpreter.py`, `test_primitives_arithmetic.py`

---------

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
`unary_iteration_kernels` mints externally controlled walks, but `QROM`
had no way to ask for one: both variants minted the plain walk.
`QROM(..., controlled=True)` now mints the controlled lookup with the
walk convention's signature, `(control, address, ladder, output)`,
identity at control |0> and the plain lookup at |1>. `controlled` must
be a bool (same guard and message as the measured QROM work).

Only the walk passes carry the control. In the composed select_swap the
walk sub-kernel is minted `controlled=True` and the parent passes the
control view to both walk passes, while the routing network and the
copy stay the plain kernels: at control |0> a controlled walk on a
clean ladder writes nothing, so the block registers stay |0>, the
routing Fredkins swap |0> registers, the copy CNOTs fire from a |0>
slot, and the un-route and un-write are the identity too. The price is
the controlled walk count in place of the plain one (the root gadgets
become Toffolis and the root crossings lose the unguarded shortcut),
with the routing and copy costs unchanged, at a fraction of the cost of
`cudaq.control` over the composition; `variant="auto"` prices the
controlled candidates against each other. The identity at |0> is a
clean-ladder statement, the kernel's precondition in any case.

Tests: the controlled lookup leaves a superposed address register with
a non-zero output word untouched at control |0>, equals the plain
lookup and the table itself at |1>, carries both branches at |+>, and
is self-inverse under a superposed control, over six geometries (both
variants, a one-bit address, a single block-index bit, a zero-padded
last block at B = 8); the Toffoli counts follow the documented contract,
auto pricing is pinned on a table whose optimum moves between plain and
controlled pricing, the bool guard raises, and the resource suite's
compiler cross-check covers the controlled lookups.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger
wsttiger force-pushed the features/controlled_qrom branch from 3b680d2 to 5a0566b Compare October 27, 2026 15:52
@wsttiger

Copy link
Copy Markdown
Collaborator Author

Rebased onto main now that #52 (composed select_swap) has landed, as agreed on #52. The controlled lookup is now the natural form under the composed construction: the walk sub-kernel is minted controlled=True and the parent passes the control view to both walk passes; the routing network and the copy are the plain kernels. Everything else is unchanged — the bool guard, controlled pricing, the controlled property, the six-geometry control-convention and self-inverse tests, the auto-pricing pin and the resource cross-check all carried over as-is. Single commit 5a0566b; verified locally on 0.15.1 (105 passed, 2 marker-skips) and 0.16 (107 passed), docs build clean.

@ivanbasov ivanbasov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Tests and a compiled-Toffoli comparison against cudaq.control run on 0.16.0, all consistent with the description; three optional notes inline.

… layout and the measured price

`variant="auto"` prices the controlled candidates against each other on
Toffolis alone, so the optimum can sit at a larger block than the plain
lookup's and `num_ladder` differs (5 against 4 for seven one-bit entries
over three address bits); the `controlled` parameter doc now says to size
the ladder from the instance rather than assume the plain and controlled
lookups share a layout.

The two select-swap parents are deliberate: they differ only in the walk
calls and share the route and copy sub-kernels, the plain one keeps the
three-view signature of the composition contract, and a dummy control
would cost every plain caller a qubit. A comment above them says so.

The module paragraph quotes the compiled comparison for eleven 3-bit
entries over four address bits: 19 / 26 / 28 native controlled Toffolis
(select, select_swap B = 2 and B = 4) against 15 / 18 / 22 plain, with
`cudaq.control` over the composition at two to three times the native
count, widest on select_swap because the wrapper also controls the
Fredkins and the copy CNOTs.

Docstrings and comments only.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger
wsttiger merged commit 135fc13 into NVIDIA:main Oct 29, 2026
22 checks passed
@wsttiger
wsttiger deleted the features/controlled_qrom branch October 29, 2026 17:34
wsttiger added a commit to wsttiger/cudaq-algorithms that referenced this pull request Oct 30, 2026
Brings in NVIDIA#52, NVIDIA#54, NVIDIA#55, NVIDIA#56, NVIDIA#61, NVIDIA#62, NVIDIA#63, NVIDIA#66, NVIDIA#68, NVIDIA#69, NVIDIA#70 and NVIDIA#75.
Conflicts: the package docstring and import list (union), the
BlockEncoding docstring (names all four provided implementations), and
the API page (combinators automodule, then the sparse section).
wsttiger added a commit that referenced this pull request Oct 31, 2026
## Summary

Adds `SparseOracleEncoding`, the sparse-access block encoding of real
sparse symmetric
matrices, three ways to feed it (`banded_oracles`, `qrom_oracles`, a
hand-written
`OracleKernels` bundle), and `encode_sparse()`, the data-level
constructor with validation,
optional Hermitian dilation and a cost report.

The implementation uses `cudaq_algorithms.primitives` and satisfies the
`BlockEncoding`
protocol, so `Walk` and `QSVT` consume it unchanged. The public API is
available from both
`cudaq_algorithms.sparse` and the package root.

## The encoding

Builds `U_A = T† S T` from location and value oracles supplied through
`OracleKernels`
(Camps, Lin, Van Beeumen & Yang, arXiv:2203.10236). Its normalization is
`alpha = d_padded * h`, with `d_padded` the padded number of structure
slots and `h` a bound
on the largest matrix-element magnitude.

`U_A` is self-adjoint and involutory on every state whose work scratch
enters `|0>`, the
domain the oracle contract guarantees and every consumer supplies; flat,
work-free oracles
(`banded_oracles`) make the involution a global matrix identity, table
oracles built on
unary-iteration walks (`qrom_oracles`) do not. Controlled application
controls only the
middle factor, so the supplied oracles never need controlled versions.
Negative off-diagonal
entries are supported; negative diagonal entries need a diagonal shift
or the Hermitian
dilation.

**The oracle contract.** Every nonzero entry belongs to exactly one
slot; a mapping that only
completes a slot's permutation loads zero, even where the matrix entry
is nonzero and owned
by another slot; reverse slots carry mirrored entries consistently.
`qrom_oracles` builds
bundles that satisfy this by construction, and a hand-written
four-by-four oracle pins the
rule in the tests.

## Inputs

- **`banded_oracles`**: arithmetic location and value oracles for
matrices given by
constant-valued bands, with boundary comparators preventing wraparound.
Offsets of any
magnitude are applied exactly: the Fourier-basis adder and comparator
take each constant
reduced per bit as an integer (`K mod 2^(t+1)`), the form
`primitives.phase_add_constant`
  uses. Band symmetry is judged relative to the band scale.
- **`qrom_oracles`**: table-based oracles from the entries; a
matching-based slot assignment
makes each location map its own inverse, so the same kernel serves as
its adjoint.
- **`from_general_oracles`**: the Hermitian dilation `[[0, A], [A.T,
0]]` of a real square
  `A` on one extra system qubit, from flat oracles of `A`.
- **`OracleKernels`** records the system width it was built for; the
encoding rejects a
  mismatched width.

Data-driven inputs accept dense arrays, SciPy sparse matrices, or
integer-indexed
`(rows, cols, vals)` triples; entries must be real and finite in both
parts.

`encode_sparse(matrix, value_bits=, dim=, dilate=, max_entries=)`
validates the data, prices
`alpha` and the register budget classically, builds the encoding over
`qrom_oracles`, and
attaches `encoding.report` (`alpha`, `qubits`, `nnz`, `max_row_nnz`,
`slots`, `dim`,
`dilated`, `value_bits`). A non-symmetric input or a negative diagonal
raises, naming the
way out; `dilate=True` encodes the Hermitian dilation at the data level,
where every entry
sits off the diagonal. `max_entries` bounds the QROM table; `alpha` must
be finite.

## Resource accounting

`num_ancilla` includes all ancillas, scratch included;
`num_block_ancilla` / `num_scratch`
expose the breakdown. The encoding provides no `select_observable`, so
`Walk.moment` serves
even orders only, and the shared `reflection_observable` raises above 20
ancillas rather than
expanding into `2^num_ancilla` Pauli terms; Chebyshev data at these
widths comes from
`walk_kernel` powers.

`state_from(ket, num_qubits=None)` gains an input-width check: with
`num_qubits` given, a ket
of any other length raises instead of silently widening the system
register (oversized) or
failing inside the simulator (undersized).

## Validation

Three test files, run through CUDA-Q on 0.15.1 and 0.16. Circuit blocks
are compared with the
exactly realized (quantized) matrices at 1e-10 and those with the
intended matrices within the
derived quantization bound. Covered: Laplacian and permutation blocks;
the involution on dirty
block ancillas with clean scratch (`qrom_oracles`) and on a fully dirty
register
(`banded_oracles`); walk powers against `T_k(-H/alpha)`; forward/adjoint
round trips;
controlled application and controlled walks at `|0>`, `|1>` and a
control in superposition;
QSVT against the signal model; protocol conformance; the dilation with
padded slots and
signed non-symmetric input under the consumers; QROM-backed oracles at
every shape (1×1, 2×2,
diagonal-only, non-power-of-two) with padded rows and columns zero; the
location and value
oracles on basis states (locations, inverses, angle / sign / upper
loads, fillers and padded
slots, clipped boundaries, clean work); two nontrivial reverse-slot
pairs and the one-qubit
slot register; band offsets near the register limit and overlapping
modular shifts; the
per-bit reduction checked analytically at 2^30 + 1 and 2^53 + 1 with the
54-qubit bundle
built; the hand-written-oracle contract with the double-counting
counterexample; scale-relative
band symmetry at 1e-13, 1 and 1e6; `encode_sparse` reports, dilation and
guards (fractional
offsets, non-finite entries, infinite `alpha`, table budget, input
width).

## Docs

API reference entries for the family, a "Sparse-access encodings"
section in the
block-encodings guide, a conventions entry, and a README line.

## Follow-ups

- Even moments from the all-zero-ancilla probability instead of a Pauli
expansion, which
  would remove the 20-ancilla guard for every wide encoding.
- Natively controlled `qrom_oracles` bundles for `from_general_oracles`
on top of the
controlled QROM lookup (#70); automatic QROM-variant selection; complex
matrices.

### References

Camps, Lin, Van Beeumen & Yang, *Explicit quantum circuits for block
encodings of certain
sparse matrices* ([arXiv:2203.10236](https://arxiv.org/abs/2203.10236));
the sparse-access
input model of Berry, Childs & Kothari
([arXiv:1501.01715](https://arxiv.org/abs/1501.01715));
Low & Chuang ([arXiv:1610.06546](https://arxiv.org/abs/1610.06546));
Gilyén, Su, Low & Wiebe
([arXiv:1806.01838](https://arxiv.org/abs/1806.01838)).

---------

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request release - v0.2 Targeted for the v0.2 release

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants