Skip to content

Add the sparse-access block encoding - #53

Merged
wsttiger merged 40 commits into
NVIDIA:mainfrom
wsttiger:features/sparse_access_block_encoding
Oct 31, 2026
Merged

wsttiger merged 40 commits into
NVIDIA:mainfrom
wsttiger:features/sparse_access_block_encoding

Conversation

@wsttiger

@wsttiger wsttiger commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

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 (Add an externally 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); the sparse-access
input model of Berry, Childs & Kothari (arXiv:1501.01715);
Low & Chuang (arXiv:1610.06546); Gilyén, Su, Low & Wiebe
(arXiv:1806.01838).

wsttiger and others added 30 commits August 28, 2026 16:00
Promote the sparse branch's unary-iteration emitter into a package
subpackage and upgrade the circuit: sibling transitions reuse the parent
ladder line with a CNOT and deeper transitions fuse the uncompute of the
old path into the compute of the new one through a single guard Toffoli
on CNOT-conjugated address wires (arXiv:1805.03662, Fig. 7). A full
uncontrolled tree of N addresses now costs exactly 3N/2 - 5 Toffolis
(3N/2 - 1 controlled) instead of the previous 2N - 4; exhaustive circuit
search over CNOT/Toffoli circuits (arbitrary conjugated controls, dirty
address wires, extra ancillas) shows these counts are optimal at the
small sizes it can cover. The paper's N - 1 assumes measurement-based
AND uncomputation, which this library's strictly unitary primitives do
not use; the gap is documented where the costs are stated.

The factory surface, the trace-time body-callback emitter, the extended
body vocabulary (free/AND-ladder/work/sign gates), the controlled
variant, the hand-written reversed-tape inverse and the keep-alive
kernel registry are kept from the seed. Tests verify the emitted tape
classically and exhaustively (every width up to 6, every partial-tree
size, controlled and uncontrolled, via Boolean truth tables), pin the
count formulas, and pin the minted kernels on the statevector against a
dense NumPy SELECT reference plus the seed's behavioral suite.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
One QROM(data, address_bits, output_bits, *, variant, block_size)
surface over two constructions behind the same (address, ladder, output)
kernel signature: the plain unary-iteration lookup (arXiv:1805.03662)
and SELECT-SWAP/QROAM (arXiv:1812.00954) — a fused walk over the block
index writes each visited block into clean block registers carried in
the ladder view, a binary cswap network on the low address bits routes
the selected register into slot 0, CNOTs copy it out, and the routing
and writes are undone unitarily. Both variants are exactly self-inverse.

Toffoli accounting is a documented contract: W(address_bits, N) for
select and 2 W(high_bits, ceil(N/B)) + 2 b (B - 1) for select_swap
(Fredkin = one Toffoli + two CNOTs; both the writes and the routing run
twice because uncomputation stays unitary, unlike the papers'
measurement-assisted headline counts). variant="auto" prices every
power-of-two block size with the exact model and takes the cheapest;
the default block size therefore optimizes the coherent cost
(~sqrt(3N/2b)) rather than the papers' ~sqrt(N/b).

Tests read out both variants exhaustively on random tables (including
out-of-range addresses of short tables, which must read zero), pin the
self-inverse property on superposed addresses, check select_swap equals
select amplitude-for-amplitude on the same table, and cover the auto
dispatch and the loud factory validation.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
cudaq.estimate_resources counts the ccx operations actually synthesized
(via Resources.count_controls("x", 2) — count("ccx") is a known trap
returning 0) and must equal both the emitter's bookkeeping and the
documented closed forms: 3N/2 - 5 on full uncontrolled trees (3N/2 - 1
controlled), exact 3N/2 per-doubling increments (increments, not
ratios — affine costs approach the asymptote from above), the QROAM
formula 2 W(high_bits, blocks) + 2 b (B - 1) for every block size, and
the auto dispatch beating the losing variant on both sides of the
select/select_swap crossover. Skipif-gated on estimate_resources
availability; every assertion is an exact integer comparison.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The Toffoli comparison (3N/2 fused vs 2N naive unitary) flatters the
fused walk: its dirty-target Toffolis cost 7 T where clean-target ANDs
cost 4 T, so in T gates the coherent advantage is ~7.5N vs ~8N (~6%),
against the measured walk's 4N. Stating both metrics in the docstring
keeps comparisons against measured-accounting papers honest; a t_count
attribute is planned.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The minted kernel is a generic interpreter, so reading its source shows
nothing about any particular circuit; the tape it replays is the
circuit. UnaryIterationKernels now retains the tape and describe()
decodes it one gate per line in execution order, with the Toffolis
tagged so the listing reconciles against toffoli_count by eye.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Superposed (phased) address, one application: the state must be the sum
over addresses of each branch's own payload unitary acting on a random
complex product state — every payload an independently-built dense
64x64 NumPy product of non-commuting Paulis on a 6-qubit target. Order,
phase, and ladder-residue errors all misassemble the sum. Rides along:
payload size must not change the Toffoli count (leaf-controlled Paulis
are two-qubit gates), pinned against emitter and compiler. The file
parametrizes over every walker importable on the branch, so the same
file serves the coherent and measured branches without conflict.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
- Materialize QROM data before validation: a single-pass iterable was
  consumed by the int conversion, so the float check ran on an empty
  iterator and silently truncated (1.5 -> 1). Regression-tested both
  ways (rejection and valid generator).
- Reject variant='select_swap' at address_bits=1 with a clear error
  instead of a bare min()-of-empty ValueError.
- Make QROM.kernel a property, matching every sibling accessor.
- Add cudaq_algorithms.primitives to the Sphinx API reference.
- Drop the payload test's import scaffolding for an unshipped module
  and the docstring's promise of a planned t_count attribute.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The four minted interpreter kernels compared the tape opcode against
bare integer literals. CUDA-Q kernel code cannot reference Python enums
or module globals, but it does capture enclosing-scope ints, so bind
the module's _OP_* constants as factory locals and dispatch on the
names. No functional change; the compiled kernels are identical.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Bare |0> in a docstring parses as an unterminated RST substitution
reference, failing the warnings-as-errors docs build now that the
primitives subpackage is on the API reference page.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Review follow-ups (PR 40): every documented body gate gets a behavioral
pin — each test states its expected action as a bits -> (bits, phase)
function applied at the active address, checked on a phased superposed
address exhaustively over target basis states, with work registers
verified back to |0>. The free-gate tests double as pins of the
conjugation-cancellation soundness condition on inactive addresses.
A resource test holds toffoli_count equal to the compiler's count when
the body itself contributes Toffolis.

External control: the same walk built plain-then-wrapped in
cudaq.control and built with controlled=True must produce identical
states on a superposed control — pinned against each other and against
the analytic reference, making the docstring's 'cudaq.control-
compatible by construction' a tested contract.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Review feedback: 'ladder' reads as the walk's ladder but for
select_swap it bundles the walk lines with the block registers, and
the ladder-to-ladder CNOT opcode serves three distinct jobs (sibling
crossings, rebased block writes, Fredkin outer CNOTs). Spell out the
register layout, the rebase step, the opcode reuse, and the five-step
W S C S^-1 W program in the module docstring. Docs only.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Review feedback on _mint_interpreter: the four variants dispatch
overlapping opcode subsets, and an opcode outside the minted variant's
set would be silently skipped by the if-chain — the worst available
failure mode. To the agreed bar:

- Declare the dispatch coverage as module data (_BASE_OPS plus the
  _CONTROL_OPS/_WORK_OPS bundles a signature carries) and reject
  out-of-set tapes at mint time with a ValueError.
- Replace the numeric opcode literals in the truth-table verifier
  with the module's _OP_* names, so the test no longer keeps its own
  copy of the numbering.
- Add per-opcode semantic pins (test_primitives_interpreter.py): each
  of the 21 opcodes runs as a one-instruction tape under every
  signature variant that supports it, on every basis state of the
  touched qubits, against its documented action including phases —
  70 (opcode, variant) cases. A missing dispatch arm in any variant
  now fails a pin, and a spec-vs-sets consistency check ensures new
  opcodes land in both places.

The four fixed signatures stay as they are (CUDA-Q kernels cannot
take optional or empty register views).

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The local format script run silently no-oped (macOS bash 3.2 lacks
mapfile) and the local yapf was 0.40.2; CI pins 0.43.0. Formatting
change only.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The reviewer is right that the fused unitary walk is not novel: it is
the select(V) construction of Childs et al. (arXiv:1711.10980, Appendix
G.4, Lemma G.7) — same in-place fused transitions, same 3N/2 + O(1)
count, no measurement — verified against the paper. The module
docstring now attributes it there and drops the exhaustive-search
narrative; what remains ours is the presentation, the priced controlled
variant, and non-power-of-2 handling.

Also per review: document what body stores (the SELECT payload table,
sum_k |k><k| (x) U_k) at the factory docstring; name the transition
helpers (sibling_flip, unguarded_root_crossing) so _emit_walk reads as
its three cases; strengthen the adjoint test with an explicit
non-involution premise assertion (forward-forward != identity, so the
reversed-tape check cannot silently become vacuous); and run the
controlled-variant test on a superposed control (ry(0.7)) alongside the
classical branches, where a branch-relative phase error would be
invisible.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
…e docstring

Second review round: state the Childs et al. attribution at the package
docstring too; replace the double-negation ceiling division with the
additive idiom; rewrite the select_swap rebase comment to say what the
construction IS (textbook walk-over-blocks + low-bit routing network)
and why the emitter is reused instead of a minted kernel (one flat tape
— CUDA-Q kernels cannot call kernels), with 'ladder' explicitly named
as the wire bundle rather than a role; note at the routing network that
the Fredkin outer CNOTs act purely between data blocks. Docs only.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
CDKM/Cuccaro ripple-carry family (register and constant add/subtract,
>=-constant comparator) and the ancilla-free Draper QFT family (qft/iqft,
Fourier-basis constant add/subtract, the comparator pair with its
hand-written adjoint), all little-endian in-place kernels with
hand-written inverses.

Tests are exhaustive over all inputs for widths up to 5 via the
superposition harness, pin every inverse by op-then-inverse identity,
and hold the documented gate prices against the compiler with
cudaq.estimate_resources: exactly 2n Toffolis for every CDKM operation
(0 at K = 0), and zero Toffolis for the QFT family with exact
controlled-r1 / r1 / h budgets (n(n-1) + n for the adder, n(n+1) + n + 1
per comparator side).

Review fixes: document the 0 <= K <= 2^n precondition on
cmp_ge_constant_qft (the borrow wraps mod 2^(n+1) for larger K) and the
exact constant_bits/complement_bits length preconditions on add_constant
and cmp_ge_constant; note in the module docstring that operands of any
one op must be pairwise disjoint; version-scope the cudaq.adjoint
limitation (broken as of CUDA-Q 0.15, still unresolved on the 0.16
pre-release) instead of stating it timelessly.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
…amily

cmp_ge_register(a, b, carry, out) XOR-loads (a >= b) (unsigned,
equal-width little-endian registers) into the out qubit and restores
a, b and the carry ancilla: complement b in place (2^n - b = ~b + 1,
carry-in set to 1), MAJ sweep, copy the top ripple carry into out,
reverse the sweep and undo the complement. Because b doubles as the
constant-load register, no n-qubit work register is needed — only the
one-qubit carry — and the price is the family contract of exactly 2n
Toffolis, pinned against cudaq.estimate_resources with runtime-argument
harnesses at widths 1-5 and 8. cmp_gt_register is the free strict
variant (a > b <=> not (b >= a): one X plus the >= comparator with the
roles swapped), same price. Both are self-inverse (compute-copy-
uncompute operand action, XOR-accumulated flag), pinned by
apply-twice-is-identity tests on random entangled states, alongside
exhaustive truth tables (widths 1-4, flag initially 0 and 1) and
superposed-register statevector checks against dense NumPy references.

This is the comparator the alias-sampling conditional (keep-value vs
alt-value test) and the first-quantized momentum comparisons need
(colleague review feedback; GAP_first_quantized_lcu.md).

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
AliasSamplingPrepare: the Babbush-style (arXiv:1805.03662, Sec. III.D)
PREPARE-with-garbage whose index-register marginal realizes the
integer-Vose table distribution exactly (per-bin discretization bound
1.5 / (K 2^mu), derived not assumed), with a hand-written adjoint.

The (alias, keep) lookup is the variants QROM under variant="auto":
the ladder slice inside the garbage register is qrom.num_ladder wide,
so num_garbage = m + 2mu + 4 + qrom.num_ladder (the historical
2m + 2mu + 4 whenever the plain select walk wins the pricing). No
variant is pinned - every variant restores its ladder and self-inverts
on the clean-ladder sector, which is what the adjoint's second lookup
uses. The new qrom property exposes the priced lookup.

Tests pin the index marginal against an independent brute-force branch
enumeration, the table distribution against the ideal within the derived
bound, prepare-then-adjoint identity, degenerate weights and validation
messages, plus compiler-pinned resource contracts: each PREPARE side
costs exactly qrom.toffoli_count + 4 (mu + 1) Toffolis (the lookup at
the QROM's own reported price plus the two CDKM comparator adders) and
num_index Fredkins, adjoint == forward gate-for-gate.

Review fixes: pass variant/block_size through to the QROM constructor
as keyword-only parameters (they shape num_garbage via qrom.num_ladder;
QROM validation errors propagate as-is) and pin the variant-agnostic
claim with a forced select_swap test asserting the exact marginal, the
prepare-then-adjoint identity and the Toffoli cost identity; reject
bool mu explicitly while keeping integral numpy ints; document that the
per-bin garbage vectors are not mutually orthogonal (only the index
amplitude magnitudes are guaranteed - exactly the qubitization
<0|PREP' SELECT PREP|0> contract); say "the available QROM variants"
instead of naming constructions this base does not ship; version-scope
the cudaq.control nested-call rejection (through CUDA-Q 0.15, lifted in
0.16); mark the h-count assertion as incidental and the
toffoli_count cross-check as a deliberate bookkeeping-vs-compiler pin.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Bare |0...0> kets and the |table - ideal| absolute value in the
AliasSamplingPrepare docstrings parse as unterminated RST substitution
references and fail the warnings-as-errors docs build now that the
class is on the API reference page. Same fix as the unary-iteration
module received on the QROM PR.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Implements the reviewer-suggested consolidation: the PREPARE's
keep-vs-reference conditional now calls the arithmetic family's
cmp_ge_register (flag <- (ref >= keep) on the bare mu-bit operands)
instead of the bespoke two-adder weave (subtract keep on a (mu+1)-bit
extension, copy the borrow, negate, add back).

- Toffoli count per PREPARE side drops from 4(mu+1) to 2mu: the cost
  identity is now qrom.toffoli_count + 2 mu Toffolis + num_index
  Fredkins, re-derived in the docstrings and re-pinned by the resource
  tests against cudaq.estimate_resources.
- The (mu+1)-bit extension's two pad qubits (keep_pad, ref_pad) are
  gone: cmp_ge_register complements its second operand in place and
  needs only the one-qubit carry the layout already had. num_garbage
  shrinks by 2 to num_index + 2 mu + 2 + qrom.num_ladder, and
  ladder_offset moves down accordingly.
- The hand-written adjoint stays the literal gate-reversal:
  cmp_ge_register is palindromic around its self-inverse flag copy, so
  re-applying it is the reversed gate sequence.

All marginal/bound/identity tests pass unchanged; only the layout and
cost accounting assertions are re-derived.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Re-applies the novel content of experimental/sparse_access_block_encoding
(tip bb54617) onto features/primitives_alias_sampling, where unary
iteration, QROM, alias sampling, and reversible arithmetic now live as
cudaq_algorithms.primitives. The branch's four seed modules
(sparse/_unary_iteration, _qrom, _alias_sampling, _arithmetic) are not
carried over; the sparse package keeps only the encodings themselves.

SparseOracleEncoding (V1) block-encodes a real symmetric d-sparse matrix
from user location/value oracles as U = T-dagger S T over a slot
register, a flag pair, and a dual index register: exactly self-adjoint
and involutory, controlled variants for free (only the encoding-owned S
factor carries the control). Signs ride an oracle-supplied sign/upper
bit pair as +-i phases; negative diagonals are unrepresentable, with
from_general_oracles (Hermitian dilation) as the documented escape
hatch. banded_oracles compiles band offsets/values into flat oracle
kernels (inlined Draper adders/comparators, since cudaq.control cannot
synthesize control variants of kernels that call other kernels).

SparseLCUEncoding (V2) is the alias-sampled LCU encoding: PREPARE over
the term weights, unary-iteration SELECT over Hermitian involutory term
unitaries, alpha = the term one-norm. Each off-diagonal pair splits
into a transposition and its reflected partner at half weight each;
each diagonal into identity and reflection halves; signs ride the
SELECT branch as a leaf-line Z phase. U_A = PREPARE-dagger SELECT
PREPARE is self-adjoint and involutory on clean-ancilla states.

qrom_oracles compiles matrix entries into an OracleKernels bundle via
greedy edge coloring into matchings (every slot map an involution), and
encode_sparse prices both constructions classically and dispatches to
the cheaper one, with prefer= overrides and a pricing report on the
returned encoding.

Adaptations for the promoted primitives:
- _sparse_lcu and _from_data import AliasSamplingPrepare /
  unary_iteration_kernels from cudaq_algorithms.primitives.
- SparseLCUEncoding pins AliasSamplingPrepare(..., variant="select"),
  freezing num_garbage = 2*num_index + 2*mu + 4 and the
  ladder_offset/num_ladder == num_index geometry that sparse_lcu_apply
  relies on when SELECT reuses the PREPARE QROM ladder — and that
  _factory's closed register-budget formula prices (commented there).
  Adopting variant="auto" would require pricing via the QROM cost model.
- sparse/__init__ exports only the encodings; the primitives'
  canonical import path is cudaq_algorithms.primitives.
- Docstrings no longer quote the naive walk's 2(K-1) tree-Toffoli
  figure (the promoted fused walk is cheaper); they defer to the
  primitives' documented counts and toffoli_count.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Re-applies the qubitization guard from
experimental/sparse_access_block_encoding (originally part of 32e252d)
onto the promoted-primitives base, resolving the one-hunk textual
conflict with the base's reformatted docstring.

The reflection observable expands to 2^num_ancilla Pauli terms, and
past ~20 ancillas building it hangs rather than erroring.
reflection_observable now raises with a pointer at walk_kernel powers —
the route wide-ancilla encodings (e.g. the sparse-access family) must
take for their Chebyshev data.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Re-applies the branch tests from experimental/sparse_access_block_encoding
that cover the novel encodings: test_sparse_oracle.py and
test_sparse_factory.py verbatim, test_sparse_lcu.py plus one relocated
test. The seed-primitive test files (test_sparse_alias_sampling,
test_sparse_arithmetic, test_sparse_qrom, and the walk-cost portion of
test_sparse_resources) are not carried over — they are superseded by
the test_primitives_* suites already on this base.

The tests validate against exact dense references at 1e-10 and against
the ideal matrices within derived discretization/value_bits bounds, pin
the involution and controlled conventions, the classical term
enumeration, the encode_sparse dispatch decision table and priced qubit
budgets, and drive both encodings through Walk and QSVT.

test_wide_ancilla_even_moment_refuses_fast (originally in
test_sparse_resources.py, from bb54617) moves into test_sparse_lcu.py:
it pins the reflection_observable ancilla guard through
SparseLCUEncoding's genuinely wide ancilla register.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Bare |0> kets, |H_ij| norms and the pipe-separated ancilla layout parse
as broken RST substitution references / unterminated inline literals
the moment these modules join the API reference; verified clean by
building the docs with the subpackage temporarily exposed to autodoc.
Same fix the primitives modules received on the QROM and alias PRs.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Repair the RST damage the notation sweep itself introduced (backticks
inside a literal block, nested inline literals) — re-verified by
building the docs with the subpackage temporarily exposed to autodoc,
zero warnings. Add the two missing state_prep-injection tests (LCU
encode_kernel and oracle walk_kernel both pinned against their dense
references on a product-state prep), and document that
from_general_oracles requires flat oracle kernels through CUDA-Q 0.15,
so qrom_oracles bundles cannot feed the dilation path.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
The PREPARE conditional now consumes the family register comparator
(one carry qubit, no pad pair), so AliasSamplingPrepare's num_garbage
for the pinned select variant is 2*num_index + 2*mu + 2; the classical
qubit budget in encode_sparse follows.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Re-export the sparse public API (SparseOracleEncoding, SparseLCUEncoding,
OracleKernels, banded_oracles, qrom_oracles, encode_sparse) from
cudaq_algorithms, alongside the existing cudaq_algorithms.sparse subpackage,
matching PauliLCU / BlockEncoding. Drop the 'experimental' framing from the
subpackage docstring.
The SparseLCUEncoding class docstring listed the PREPARE garbage register
as 2m + 2*mu + 4 qubits; the actual width (and the encode_sparse budget
formula in _factory.py) is 2m + 2*mu + 2. Documentation only.
test_apply_is_involution only fed the |0...0>-ancilla block, so it did not
operationally distinguish SparseOracleEncoding's *global* involution
(U_A^2 = I on the full register) from SparseLCUEncoding's, which holds only
on the |0>-ancilla domain. Add a companion that loads an arbitrary dense
state over system and ancilla (scratch qubits non-zero) and checks U_A^2
restores it exactly.
…t check, reflection docstring

Three findings from an external review of the sparse-access encodings:

- encode_sparse mis-classified a uniformly tiny but genuinely asymmetric
  matrix as symmetric: the symmetry tolerance had a fixed 1e-12 absolute
  floor (max(1.0, |value|)), so matrices with all entries below it were
  symmetrized (LCU) or crashed on a missing mirror entry (oracle KeyError).
  _first_asymmetry now scales the tolerance to max|H| and treats missing
  mirror support as an asymmetry, so such inputs are dilated. Test added
  at three overall scales x both paths.

- The LCU register width was priced by building every SELECT gate body
  even when the term count already exceeded max_terms (4160 bodies for a
  64x64 all-ones input at max_terms=1). The budget is now checked on the
  cheap term count first; bodies are materialized only for an eligible
  path. Over-budget qubits are reported as None. Test pins zero bodies built.

- The num_ancilla / module docstrings claimed the full-register reflection
  equals an index-only reflection. It does not: SELECT entangles the PREPARE
  garbage with the system, so the index-|0> sector is not garbage-free and
  the full reflection is required (the code already reflects the full
  register). Corrected, and the difference from _sparse_oracle -- whose
  scratch is a pure function of the system -- is now stated.
wsttiger and others added 5 commits September 22, 2026 18:46
…ings

The oracle construction is the standard sparse-access input model; attribute
it in the SparseOracleEncoding docstring (Berry-Childs-Kothari 1501.01715,
Low-Chuang qubitization 1610.06546, Gilyen-Su-Low-Wiebe QSVT 1806.01838).
Cite Camps, Lin, Van Beeumen and Yang (2203.10236) in banded_oracles as the
explicit-circuit reference for structured/banded sparse matrices, the layer
these prefab oracles occupy above the generic interface.
Attribute the PREPARE-dagger . SELECT . PREPARE structure to the LCU block
encoding (Childs-Wiebe 1202.5822; Berry-Childs-Cleve-Kothari-Somma
1412.4687), and point to the primitives layer where the coherent alias
sampling and unary iteration gadgets (Babbush et al. 1805.03662) are cited.
The SparseOracleEncoding assembler follows Camps, Lin, Van Beeumen and Yang
(arXiv:2203.10236): T is their diffusion . O_A . O_c sandwich (Sec. 4), S the
O-dagger . SWAP . O Hermitian symmetrization (Thm. 5.1 / 8.1), and alpha =
d_padded * h their sparsity normalization s. banded_oracles implements their
Sec. 4.2 banded construction (shift/adder O_c + Ry O_A). Demote
Berry-Childs-Kothari / Low-Chuang / Gilyen to the oracle-model and
block-encoding/QSVT context they actually supply. The earlier docstrings framed
Camps as banded-only related work and mis-credited the construction to the
oracle-model ancestors.
Take main's reviewed primitives (renamed QFT comparator, corrected
Toffoli counts, dispatch pruning) and their tests wholesale; the sparse
package consumes only unary_iteration_kernels and AliasSamplingPrepare,
both unchanged in name.
… document

The oracle construction's involution was documented as a global matrix
identity and the table oracles as their own inverses. That holds for flat,
work-free oracles (banded_oracles) but not for qrom_oracles bundles: their
oracles are unary-iteration walks, which invert only when the ladder and
work scratch enter |0>, so U_A^2 = I fails on a dirty work register
(defect ~2e-3 on a 4x4 example). Walk and QSVT only ever see clean
scratch, so nothing they compute was wrong; the docstrings, the
OracleKernels contract and the LCU module's comparison now state the real
guarantee, and the dirty-ancilla tests cover a qrom_oracles bundle (dirty
block ancillas, clean work) and a fully dirty banded bundle.

Three inputs that silently produced a different operator now raise:

- encode_sparse Hermitian-dilated any non-symmetric input, including a
  symmetric matrix stored as one triangle and one with rounding noise
  above 1e-12 * max|H|, doubling the system register with only
  report["dilated"] as a signal, while the constructors raised for the
  same input. Dilation is now opt-in (dilate=True); the default raises
  naming the asymmetry and the way out.
- An OracleKernels bundle carried no width, so one built for n system
  qubits was accepted by an encoding on n+1 and ran as a block-diagonal
  tiling. The bundle now records num_system (set by banded_oracles,
  qrom_oracles and the dilation) and the encoding rejects a mismatch.
- A 3-tuple was always read as (rows, cols, vals), so a dense matrix given
  as a tuple of three rows became a single entry. Triples are recognized
  by shape and integer index dtype; anything else is a dense matrix.

The pricing report moves onto the public classes (report is None on
direct constructions, with the repr suffix only when set), replaces the
ambiguous "d" with max_row_nnz and slots, and quantized_angle is exported
as part of the oracle contract.

Tests: the LCU ideal-matrix check used alpha * num_bins * bound, which at
mu=2 admits the zero matrix; the alias tables are now pinned bin by bin
against the ideal weights, off-diagonal elements against the two-term
bound and diagonals against the all-terms bound, plus a mu sweep. The
oracle references use a test-local quantizer instead of importing the one
under test. New: controlled walk at control |0> and |1> for both
encodings, an exact alpha tie resolved towards the oracle path, the K3
report case, the non-symmetric and width guards, and the tuple-of-rows
case. Column-by-column loops become single dense actions and value_bits
drops from 12 to 10, cutting the suite from ~10 to ~5 minutes.

Docs: the sparse family in the API reference, a guide section (which
construction when, encode_sparse and its report, the oracle contract,
widths and moments), a conventions entry, a README line; the protocol
docstring and the bring-your-own text no longer present sparse-oracle
encodings as hypothetical. Stale text fixed: the "+4" garbage comment, a
reference to a docs file that does not exist, plan-stage labels, and
compiler-version narrative in the flatness rationale.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger wsttiger added enhancement New feature or request release - v0.2 Targeted for the v0.2 release labels Oct 21, 2026
wsttiger added a commit that referenced this pull request Oct 29, 2026
`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`

---------

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
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).
@smcardleQ

Copy link
Copy Markdown
Collaborator

SparseOracleEncoding looks great, and is architected in a natural way - see minor comments below.
I think we should remove SparseLCUEncoding. It is not clear to me that this is an efficient approach in general, and I am not aware of an algorithm in the literature that uses this method.

Requested changes

1. Fix scale-dependent symmetry validation in banded_oracles

The absolute 1e-12 tolerance accepts substantially asymmetric matrices when their entries are small.

For example, this currently passes validation:

oracles = banded_oracles(
    [-1, 1],
    [1e-13, 0.0],
    num_system=1,
    value_bits=6,
)
encoding = SparseOracleEncoding(oracles, num_system=1)

The requested matrix divided by alpha contains an off-diagonal entry of 0.5, but the encoded block is zero.

  • Replace the absolute symmetry tolerance with a scale-relative check, consistent with the QROM input validation.
  • Test asymmetric bands at several scales, including 1e-13, 1.0, and 1e6.
  • Include missing-value and opposite-sign cases.
  • Verify that equal tiny bands remain accepted and produce the expected quantized block.

Source: [symmetry validation](

if abs(v - by_offset[-o]) > 1e-12:
).

2. Preserve integer precision in the banded Fourier arithmetic

The current rotations convert the entire offset to a floating-point angle before reducing it modulo (2\pi). This loses precision for large valid offsets. For num_system=54 and offset=2**53 + 1, analytical evaluation gives only approximately 4.4% probability on the correct location-oracle output.

  • Reduce each integer offset modulo 2**(t + 1) before converting it into the rotation angle.
  • Apply this consistently to location, inverse-location, and boundary-comparison arithmetic.
  • Test positive and negative offsets, including offsets near the register limit.
  • Add analytical large-width tests, including 2**30 + 1 and 2**53 + 1, without requiring a large statevector simulation.

The existing arithmetic primitive already implements the appropriate reduction.

Sources: [banded arithmetic](

r1.ctrl(6.283185307179586 * offset / (1 << (t + 1)), slot,
), [existing integer reduction](
# Only ``K mod 2^(t+1)`` affects reg[t]'s phase. Reduce it as an integer
).

3. Correct the custom-oracle contract for filler mappings

The documented instruction to load H[x, cinv(s, x)] unconditionally can double-count matrix entries. Permutation-completion mappings must load zero even when the corresponding matrix entry is nonzero.

For example, consider a 4×4 matrix with H[0,0] = H[1,2] = H[2,1] = 0.5. A matching slot swaps indices 1 and 2 while fixing 0; a separate slot handles the diagonal. Following the current formula loads H[0,0] in both slots.

  • Explicitly document that every nonzero entry must be assigned to one slot, with consistent reverse-slot pairing.
  • Specify that filler mappings load zero rather than looking up the corresponding matrix entry.
  • Add a handwritten-oracle regression test using this example and verify the complete encoded block.
  • Include a banded example where different modular location permutations overlap but their valid-value supports differ.

Sources: [oracle contract](

``o_val`` XOR-loads, for slot ``s`` and register content ``x`` (the *row*
), [QROM filler handling](
delta = [[0] * size for _ in range(num_loc)]
).

4. Tighten input validation

  • Reject fractional band offsets before integer conversion. Currently, banded_oracles([0.9], [1], 2, 4) silently selects the diagonal.
  • Check finiteness of both real and imaginary components before applying the real-valued tolerance. complex(1, float("nan")) is currently accepted as 1.0.
  • Reject nonfinite normalization after computing d_padded * h. Finite input such as 1e308 * np.eye(2) currently produces alpha == inf.
  • Add regression tests for each case.

Sources: [offset conversion](

offsets = [int(o) for o in offsets]
), [entry validation](
def _real_entry(value, i: int, j: int) -> float:
), [normalization]( ).

Additional circuit tests

These should execute through CUDA-Q, rather than checking construction metadata alone.

  • QROM dimensions and padding: reconstruct complete blocks for 1×1, 2×2, positive diagonal-only, and non-power-of-two matrices. Verify that padded rows and columns are zero.
  • Direct oracle behavior: test location mappings, inverses, value/sign/upper-bit XOR loading, padded slots, clipped boundaries, and restoration of clean work qubits.
  • QROM consumers: test controlled application, repeated walk powers, and QSVT against independent dense references.
  • Reverse-slot branches: cover multiple nontrivial reverse-slot pairs and the controlled flip when the slot register has one qubit.
  • Dilation: test padded slots, signed nonsymmetric input, controlled application, and walk/QSVT integration, including the factory’s forced-oracle dilation path.
  • Control coherence: compare the complete controlled output with its reference, including a control prepared in superposition.

The state-taking convenience kernels also lack system-width validation. Add coverage for oversized and undersized input states and define their rejection behavior. This is a shared issue with existing Walk/QSVT interfaces, rather than a defect unique to this PR.

Verification limitation

The review used independent source-derived gate simulations and analytical checks. CUDA-Q was unavailable locally, so the repository’s CUDA-Q tests were not rerun. The added regression tests should confirm both the mathematical behavior and CUDA-Q compilation/runtime behavior.

…om data

The sparse package offers one construction, SparseOracleEncoding (the
sparse-access oracle encoding of Camps et al.), fed by banded_oracles,
qrom_oracles or a hand-written OracleKernels bundle. The linear
combination of transposition and reflection unitaries that
SparseLCUEncoding built has no algorithm in the literature behind it and
no demonstrated regime of efficiency, so it goes, together with its
tests (review of NVIDIA#53).

`encode_sparse(matrix, value_bits=, dim=, dilate=, max_entries=)` is now
the data-level constructor for the oracle encoding: it validates the
entries, dilates on request, prices alpha = d_padded * h and the register
budget classically, refuses negative diagonals and non-symmetric input
with the way out (shift the diagonal, or dilate=True, under which every
entry sits off the diagonal), bounds the QROM table by `max_entries`, and
attaches `report` (alpha, qubits, nnz, max_row_nnz, slots, dim, dilated,
value_bits). The matrix-input helpers it shares with qrom_oracles live in
sparse/_entries.py.

Tests: the factory file pins the report and the encoded block for a
uniform band, skewed weights, a negative diagonal through the dilation,
non-symmetric input through the dilation, the tiny-scale asymmetry
detection, the table budget and argument validation. Guide, conventions,
README and the API page describe the one construction.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
…r input checks, one slot per entry

The banded adder and comparator rotations took the whole offset (or the
comparator constant) as a float: 2 pi K / 2^(t+1) for K near 2^53 loses
the low bits and the location oracle lands on the wrong row. Each
constant is now reduced per bit as an integer, K mod 2^(t+1), the form
primitives.phase_add_constant already uses, so any offset is exact;
`_residues` holds the table and a test checks 2^30 + 1 and 2^53 + 1
against exact rational phases, builds the 54-qubit bundle, and pins the
clipped band at offsets near the register limit through the circuit.

Band symmetry is judged relative to the band scale (1e-12 * max |value|),
as the matrix-data check already was: lopsided bands at 1e-13 encoded a
zero block before. Offsets must be integers (0.9 silently selected the
diagonal), a matrix entry must be finite in both parts before the real
tolerance is applied (complex(1, nan) passed as 1.0), and alpha =
d_padded * h must be finite (1e308 * I produced an infinite alpha),
checked in the encoding and before any kernel is minted in encode_sparse.

The oracle contract states that every nonzero entry belongs to exactly
one slot and that a mapping which merely completes a slot's permutation
loads zero, even where the matrix entry is nonzero and owned by another
slot; the previous wording loaded H[x, cinv(s, x)] unconditionally and
counted such entries twice. A hand-written four-by-four oracle pins the
rule, with the double-counting reading as the counterexample, and a
banded case with coinciding modular shifts but different supports pins
the comparator gating.

Review of NVIDIA#53 (smcardleQ), items 1-4.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
… and the dilation

A new test file exercises the sparse-access encoding through CUDA-Q
rather than through construction metadata: QROM-backed oracles at every
shape (1x1, 2x2, diagonal-only, 3x3, 5x5) with padded rows and columns
zero; the location and value oracles on basis states (locations and
inverses, angle / sign / upper loads for assigned entries and zero for
fillers and padded slots, clipped band boundaries, work qubits clean);
controlled application, walk powers and QSVT of a QROM-backed encoding
against dense references; two nontrivial reverse-slot pairs and the
one-qubit slot register pair; the Hermitian dilation with padded slots
and signed non-symmetric input under controlled application, the walk,
QSVT and the factory's dilation path; and a control in superposition
against the full reference state.

`state_from(ket, num_qubits=None)` gains the width check the kernel
boundary cannot make: a kernel that allocates `cudaq.qvector(state)`
takes whatever width the data implies, so an oversized ket silently
widens the system register and an undersized one fails inside the
simulator with an opaque error; with `num_qubits` given, any other length
raises ValueError. The encodings' state-taking kernels and the guide say
so.

Review of NVIDIA#53 (smcardleQ), additional circuit tests.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough pass, and for the reproducers: all four ran here exactly as you described, on CUDA-Q 0.15.1 and 0.16.

SparseLCUEncoding is gone (commit f3cef96): the module, its tests and the two-path pricing. encode_sparse(matrix, value_bits=, dim=, dilate=, max_entries=) is now the data-level constructor for SparseOracleEncoding over qrom_oracles; negative diagonals and non-symmetric input raise with the way out (shift the diagonal, or dilate=True, under which every entry sits off the diagonal), and report carries alpha, qubits, nnz, max_row_nnz, slots, dim, dilated, value_bits.

1. Symmetry tolerance. Band symmetry is now judged relative to the band scale (1e-12 * max |value|), the same rule the matrix-data path already used. Tests at 1e-13, 1 and 1e6 reject a value against zero, two magnitudes and opposite signs, and check that equal tiny bands encode the expected quantized block.

2. Integer precision. The adder and comparator rotations take each constant reduced per bit as an integer, K mod 2^(t+1), the form primitives.phase_add_constant uses, in o_loc, o_loc_adj and the boundary comparator of o_val. Tests: the clipped band at offsets near the register limit (±7 on 3 qubits, −5, 3 on 2) through the circuit; bands whose modular shifts coincide (+1 ≡ −3 mod 4) but whose supports differ; and the analytic check you asked for, 2^30 + 1 and 2^53 + 1 (both signs) against exact rational phases, with the unreduced float shown to be off by π on bit 0 for 2^53 + 1, plus the 54-qubit bundle building without a statevector. (Your 4.4 % figure modelled the error differently; with the kernel's expression the low bit lands a full π off, so the location is simply wrong.)

3. Contract. The module docstring now states that every nonzero entry belongs to exactly one slot, that a mapping which merely completes a slot's permutation loads zero even where the matrix entry is nonzero and owned by another slot, and that the assignment must be consistent across reverse slots. qrom_oracles already did this (it writes a value only for rows in a slot's matching); the hand-written-oracle test on your 4×4 example checks the full block, and the double-counting reading is kept as the counterexample (the diagonal entry comes out doubled, nothing else moves).

4. Validation. Offsets must be integers (0.9 is rejected), an entry must be finite in both parts before the real tolerance applies (complex(1, nan) is rejected), and alpha = d_padded * h must be finite (1e308 * I is rejected in the encoding and before any kernel is minted in encode_sparse). Each has a regression test.

Circuit tests. A new file, test_sparse_circuits.py, runs the rest of your list through CUDA-Q: QROM-backed oracles at every shape (1×1, 2×2, diagonal-only, non-power-of-two 3×3) with padded rows and columns zero; the location and value oracles on basis states (locations and inverses, angle / sign / upper loads for assigned entries and zero for fillers and padded slots, clipped band boundaries, work qubits clean); controlled application, walk powers and QSVT of a QROM-backed encoding against dense references; two nontrivial reverse-slot pairs and the one-qubit slot register pair; the Hermitian dilation with padded slots and signed non-symmetric input under controlled application, the walk and QSVT (the factory's data-level dilation is pinned in the factory file); and a control in superposition against the full reference state. Register sizes stay at two system qubits for QROM-backed cases, since their work register adds 2n + d qubits and a 3-qubit case costs minutes per statevector; the file runs in about six minutes per CUDA-Q version.

Input width. The kernel boundary cannot check it (a kernel allocates whatever cudaq.qvector(state) implies, so an oversized ket silently widened the system and an undersized one failed inside the simulator), so state_from(ket, num_qubits=None) now takes the register width and raises on any other length; the state-taking kernels and the guide say to pass num_qubits=encoding.num_system. That is the shared Walk / QSVT fix as well, since every state-taking path builds its state through the same helper.

All of it runs through CUDA-Q on both supported versions; the branch is also merged with current main.

@wsttiger wsttiger changed the title Add sparse access block encoding Add the sparse-access block encoding Oct 30, 2026
Brings in NVIDIA#57 (PhaseEstimation and the sim_utils package). Conflict: the
package import list (union).
@wsttiger
wsttiger marked this pull request as ready for review October 30, 2026 17:57
@wsttiger
wsttiger merged commit 87426c1 into NVIDIA:main Oct 31, 2026
24 checks passed
@wsttiger
wsttiger deleted the features/sparse_access_block_encoding branch October 31, 2026 00:42
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