Skip to content

Add LCUBlockEncoding: alias-sampled PREPARE, unary-iteration SELECT - #55

Merged
wsttiger merged 3 commits into
NVIDIA:mainfrom
wsttiger:features/lcu_alias_qrom_select
Oct 27, 2026
Merged

wsttiger merged 3 commits into
NVIDIA:mainfrom
wsttiger:features/lcu_alias_qrom_select

Conversation

@wsttiger

@wsttiger wsttiger commented Oct 8, 2026 •

Copy link
Copy Markdown
Collaborator

Adds cudaq_algorithms.LCUBlockEncoding, a generic linear-combination-of-unitaries
block encoding of H / alpha for a Hamiltonian written as a weighted sum of Hermitian
involutory unitaries H = sum_k c_k U_k. It assembles the merged primitives — coherent
alias sampling (AliasSamplingPrepare, #48) as PREPARE and unary iteration
(unary_iteration_kernels, #40) as SELECT — into the standard
U_A = PREPARE† · SELECT · PREPARE structure. alpha is the term one-norm sum_k |c_k|.

What the caller supplies. The term weights |c_k| and one SELECT body per term
describing U_k in the unary-iteration gate vocabulary (("x"|"y"|"z", target),
("sign",) for a negative c_k, and the free / AND-ladder work ops for multi-controlled
term unitaries). This keeps the primitive agnostic to how a problem decomposes into terms.
Every body op is validated at construction (known op, operand count, non-negative integer
operands, system targets below num_system, and a Hermitian product for Pauli-word
bodies) with a ValueError naming the term and op; integer parameters are not silently
truncated.

What it provides.

  • The full BlockEncoding protocol, so it drives the existing Walk and QSVT
    consumers unchanged; encode_kernel / walk_kernel convenience factories with
    optional state_prep injection.
  • The walk runs in the PREPARE frame, exactly as PauliLCU's does
    (W = PREPARE · R · PREPARE† · SELECT, real prepare_kernel / unprepare_kernel), so
    select_observable — the SELECT operator sum_k sign_k |k><k|_index ⊗ P_k, identity on
    the alias garbage — is derived from the bodies whenever every term is a Pauli word, and
    Walk.moment serves odd orders. Bodies in the extended vocabulary raise
    NotImplementedError there (even orders remain available).
  • alpha, num_system, num_ancilla (= index + alias garbage + SELECT work), plus
    num_index / num_garbage / num_select_work, the term weights,
    discretized_weights, discretization_bound, and the underlying preparation.

Contracts / boundaries.

  • Each U_k must be a Hermitian involution; SELECT is then a Hermitian involution and
    U_A is exactly self-adjoint on the all-|0…0> ancilla subspace (the domain every
    consumer supplies).
  • Walk reflections cover the full ancilla register (garbage included): SELECT leaves
    the alias-PREPARE garbage entangled with the system, so an index-only reflection would
    misidentify the block.
  • The alias PREPARE is pinned to the "select" QROM variant so SELECT can reuse its clean
    QROM ladder; PREPARE is never placed under cudaq.control (only SELECT and the
    reflection carry the external control).
  • The ancilla register is index(m) + garbage(2m + 2mu + 2) + work qubits, so mu
    drives the width: the five-term, two-qubit test fixture is 22 qubits at mu = 4 and
    30 at the default mu = 8, sized for resource estimation rather than for the CI
    simulators.

Validation. tests/python/test_lcu_block_encoding.py (21 tests), green on CI
(cudaq 0.16) — exercised through a linear combination of Pauli words against independent
dense references, two ways: the circuit against the exactly-encoded matrix (alias-table
weights, dense unitaries) at simulator precision (1e-10), and the exactly-encoded matrix
against the ideal Hamiltonian within the derived discretization bound. Plus the one-norm,
the self-adjoint involution, Walk Chebyshev powers (T1, T2 of −H/alpha), the full
Walk.moments sequence (orders 0–5) and multi-qubit-word odd moments on small instances,
the controlled-apply and controlled walk-step control conventions (directly and through
Walk.controlled_roundtrip_kernel), a body on the SELECT work register (CZ), a QSVT phase
sequence against the signal model, protocol conformance, and the factory-boundary guards.

Docs. LCUBlockEncoding in the Python API reference and the block-encodings guide
(what the caller supplies, the ancilla formula, the moment frame); the protocol and
qubitization guide state the PREPARE-frame contract the odd-moment path relies on.

Follow-up (separate PR). PauliLCU opt-in params to route its PREPARE/SELECT through
this primitive (alias PREPARE + QROM/unary SELECT alongside today's exact
multiplexed-Ry + controlled-Pauli path); its term unitaries are Pauli words, so
select_observable / Walk.moment carry over by delegation.

Add a generic linear-combination-of-unitaries block encoding
(cudaq_algorithms.LCUBlockEncoding) that block-encodes H / alpha for a
Hamiltonian written as a weighted sum of Hermitian involutory unitaries
H = sum_k c_k U_k. The caller supplies the term weights |c_k| and a SELECT
body per term describing U_k in the unary-iteration gate vocabulary; the
encoding assembles U_A = PREPARE-dagger . SELECT . PREPARE from the merged
coherent-alias-sampling PREPARE (AliasSamplingPrepare) and unary-iteration
SELECT (unary_iteration_kernels) primitives, and alpha is the term
one-norm sum_k |c_k|.

The encoding satisfies the BlockEncoding protocol, so it drives the
existing Walk and QSVT consumers directly. Walk reflections cover the full
ancilla register (index + alias garbage + SELECT work) because SELECT
leaves the PREPARE garbage entangled with the system; select_observable
is available only when the caller supplies the odd-moment observable
(valid when the U_k are computational-frame Pauli words).

Tests validate against independent dense references over a linear
combination of Pauli words, two ways: the circuit against the
exactly-encoded matrix (alias-table weights, dense unitaries) at
simulator precision, and the exactly-encoded matrix against the ideal
Hamiltonian within the derived discretization bound; plus the one-norm,
the self-adjoint involution, Walk Chebyshev powers, controlled-apply
conventions, a QSVT phase sequence, protocol conformance, and
factory-boundary validation.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger
wsttiger force-pushed the features/lcu_alias_qrom_select branch from 1260727 to a4e924a Compare October 12, 2026 23:08
…ment

The walk step was W = R U_A with no-op prepare/unprepare hooks (the zero
frame), and `select_observable` could be supplied at construction. In
that frame `Walk.moment` measures the observable on (R U_A)^p |0, psi>,
where the documented sum_k sign_k |k><k| (x) P_k returns the first term
alone (checked at mu=2: order 1 gave -0.855 against a true -0.174), so
no odd-moment observable existed. The step is now defined as `PauliLCU`
defines it, W = PREPARE R PREPARE-dagger SELECT (SELECT, then reflect
about the PREPARE state), with the alias PREPARE and its adjoint as the
prepare/unprepare hooks, at the same per-step gate count. The odd-moment
observable is then the SELECT operator itself on the index and system
registers (identity on the alias garbage): it agrees with the SELECT
circuit on every state the walk produces, because those keep the ladder
and work scratch clean. `select_observable` derives it from the bodies
whenever every term is a Pauli word and raises `NotImplementedError` for
bodies in the extended vocabulary; the constructor no longer accepts an
observable. `walk_kernel` wraps PREPARE, steps, PREPARE-dagger, so its
block is unchanged, as are `apply_kernel` and QSVT.

Body ops are validated at the factory boundary (known op, operand count,
non-negative integer operands, system targets below `num_system`) with a
`ValueError` naming the term and op, instead of an `IndexError` from the
work-width scan or a runtime index error inside the kernel; a Pauli-word
body whose ops compose to a +-i phase is rejected as non-Hermitian. The
work width is derived from the unary-iteration operand tables. Integer
parameters (`num_system`, `mu`, `num_select_work`, renamed from
`num_work` to match the property) reject fractional values instead of
truncating them.

Tests: the full `Walk.moments` sequence (orders 0-5) on a one-qubit
instance and odd moments on the two-qubit fixture at mu=2; the controlled
walk-step kernels, previously untested, pinned directly (roundtrip
identity at control |0> and |1>; a single step is the identity at |0> and
has block -H/alpha at |1>) and through `Walk.controlled_roundtrip_kernel`;
a CZ term built on the SELECT work register, uncontrolled and controlled;
the extended-vocabulary hook raise; the Hermiticity, target, arity,
operand-type and integer-parameter guards; pinned register geometry.

Docs: `LCUBlockEncoding` in the Python API reference and the
block-encodings guide (what the caller supplies, the ancilla formula and
how `mu` drives it, the moment frame); the protocol docstring and the
qubitization guide state the PREPARE-frame contract the odd-moment path
relies on; the Walk/QSVT docstrings name both provided encodings.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
Comment thread docs/sphinx/guide/block_encodings.rst Resolved
Comment thread python/cudaq_algorithms/lcu.py Resolved

@smcardleQ smcardleQ left a comment

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.

Approve overall; I think we should revisit the naming convention of 'PauliLCU' vs 'LCUBlockEncoding' (both of which assume Pauli terms). The real distinction is in how PREP and SEL are implemented. I think ideally there would be one 'PauliLCU' class, with arguments that choose how PREP and SEL are implemented.

The kernels addressed the ancilla register through one-letter offsets
(`m`, `g`, `w`, `l0`) and inline slice arithmetic, repeated in seven
kernels and twice over for the controlled variants. Each kernel now
carves named views once — `index`, `garbage`, `ladder`, `work` (and
`control`) — and calls PREPARE and SELECT on them. Same gates, same
layout: [index | garbage | work], with SELECT's ladder reusing the
PREPARE QROM ladder inside the garbage view.

The guide's opening paragraph presented `PauliLCU` and `LCUBlockEncoding`
as two parallel Pauli encodings. It now states their roles: `PauliLCU` is
the front door for a Pauli sum; `LCUBlockEncoding` is the body-programmed
primitive whose terms are SELECT bodies in the unary-iteration
vocabulary, not necessarily Pauli words, which the double-factorized and
sparse-access encodings program directly.

Signed-off-by: Scott Thornton <wsttiger@gmail.com>
@wsttiger
wsttiger merged commit 0ab88da into NVIDIA:main Oct 27, 2026
22 checks passed
@wsttiger
wsttiger deleted the features/lcu_alias_qrom_select branch October 27, 2026 00:41
wsttiger added a commit that referenced this pull request Oct 27, 2026
> **Stacked on #55** (`LCUBlockEncoding`). Until #55 merges this PR
shows its commits
> too; it will be rebased onto `main` and squashed to a single commit
once #55 lands.
> Opened as a draft for parallel review.

Adds an opt-in `backend` parameter to `PauliLCU`. The default
`backend="exact"` is
unchanged — the exact multiplexed-Ry amplitude PREPARE and the
address-indexed
controlled-Pauli SELECT. `backend="alias"` instead builds the encoding
from the
alias-sampled PREPARE and unary-iteration (QROM) SELECT primitives
through #55's
`LCUBlockEncoding`: the Pauli-sum term weights become the alias weights
and each term's
Pauli word becomes a unary-iteration SELECT body (with a `("sign",)` op
for a negative
coefficient). `mu` sets the alias keep precision and is accepted with
this backend only.

This trades the exact PREPARE's rotations for Toffoli-style gates, at
the cost of extra
ancillas (index + alias garbage + SELECT work: `3m + 2mu + 3` qubits for
`m` index bits,
so 20 rather than 3 for the five-term test fixture at `mu = 4`) and a
rounding of each term
weight to `mu` bits (within `alpha · discretization_bound`; `alpha`
stays the exact
one-norm).

**Thin-adapter delegation.** On the alias backend `PauliLCU` captures
the weights and
bodies, builds an `LCUBlockEncoding`, and delegates `num_ancilla`, every
kernel factory
(`encode_kernel` / `walk_kernel` / `apply_kernel` and the
`BlockEncoding` protocol
factories) and `select_observable` to it. The walk therefore runs in the
PREPARE frame on
both backends, and `Walk.moment` serves odd orders through the derived
SELECT observable;
the module-level `select_observable(encoding)` dispatches the same way.
The exact backend's
code paths are untouched.

**Boundaries.**
- `kernel_args` (the exact backend's flattened kernel arrays) is
exact-backend only and
raises `NotImplementedError` on the alias backend, which composes
primitives rather than
  the module-level kernels.
- Even `Walk.moment` orders use the geometry-only reflection observable,
whose
`2^num_ancilla` terms are practical only at small `mu` on the alias
backend.
- Input guards raise `ValueError`: an unknown backend name, `mu` given
with the exact
  backend, or a fractional / bool / sub-1 `mu`.

**Validation.** `tests/python/test_pauli_lcu_alias.py` (19 tests), green
on CI
(cudaq 0.16), exercise the alias backend through the `PauliLCU` API
against independent
dense references: the circuit vs the exactly-encoded matrix at simulator
precision (1e-10)
and vs the ideal Hamiltonian within the discretization bound; the
one-norm and pinned
ancilla geometry; single-term, single-negative-term, identity-only,
negative-identity and
identity-excluded Hamiltonians; `SpinOperator` input; `state_prep`
injection; walk
Chebyshev powers; the full `Walk.moments` sequence (orders 0–5) and
multi-qubit-word odd
moments; plain and controlled consumer roundtrips; QSVT sequences with
forward and adjoint
walk directions; protocol conformance with every factory delegating by
identity; and the
backend / `mu` guards. The exact-backend suite and the consumer suites
are unchanged.

**Docs.** The block-encodings guide gains a construction line and a
short section on the
alias backend; the qubitization guide notes that `select_observable`
serves both backends.

## Files
- `python/cudaq_algorithms/pauli_lcu.py` (opt-in backend)
- `tests/python/test_pauli_lcu_alias.py` (new)
- `docs/sphinx/guide/block_encodings.rst`,
`docs/sphinx/guide/qubitization_qsvt.rst`

---------

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