diff --git a/python/cudaq_algorithms/primitives/__init__.py b/python/cudaq_algorithms/primitives/__init__.py index 11820a5..7ed8e36 100644 --- a/python/cudaq_algorithms/primitives/__init__.py +++ b/python/cudaq_algorithms/primitives/__init__.py @@ -19,6 +19,17 @@ minted kernels actually have, and the resource tests pin them against the compiler. +Composition (CUDA-Q >= 0.16): minted kernels are building blocks — call +them from your own kernels, and wrap the whole composition in +``cudaq.control`` to obtain the controlled operation. This is the +intended consumption model for SELECT-style constructions (the +select_swap QROM is itself built this way), and it is pinned by tests +(``test_primitives_composition.py``). Through CUDA-Q 0.15, +control-variant generation rejected kernels that call kernels, so on +0.15 use the factory's ``controlled=True`` variants instead — they also +remain the cheaper option everywhere (the control is folded into the +walk rather than added to every gate). + Import the subpackage directly (``from cudaq_algorithms.primitives import QROM``); nothing here is re-exported from the package root. """ diff --git a/python/cudaq_algorithms/primitives/_qrom.py b/python/cudaq_algorithms/primitives/_qrom.py index 16680c2..e3b0d91 100644 --- a/python/cudaq_algorithms/primitives/_qrom.py +++ b/python/cudaq_algorithms/primitives/_qrom.py @@ -80,28 +80,25 @@ The address register is likewise split by convention, low bits first: ``address[0 .. low)`` are the routing bits (low = log2(B)), -``address[low ..)`` the block index the walk iterates. The block-index -walk is emitted by ``_emit_walk`` as if its address register started at -wire 0, then *rebased*: every address operand is shifted up by ``low`` -(the ``_ADDRESS_OPERANDS`` table names which operand slots to shift). - -**Why "ladder-to-ladder CNOTs" appear** (``_OP_CX_LADDER_LADDER``): -the interpreter's opcodes name *registers*, not roles, so this one -opcode serves three distinct jobs: - -1. In the plain walk: parent-line -> child-line CNOTs (sibling - crossings) — genuinely ladder-to-ladder. -2. In the ``select_swap`` write stage: ``_emit_walk`` emits the block - writes as leaf-controlled body X's onto its *target* register, but - here the write destination is the block registers, which live in - the ``ladder`` view — so each ``_OP_BODY_X`` is rewritten to a - ladder-to-ladder CNOT from the word line onto a block-register - qubit (the rebase loop in ``_build_select_swap``). -3. In the routing network: each controlled register swap is ``b`` - Fredkins, decomposed ``cswap(c; u, v) = cx(v,u) ccx(u,c,v) - cx(v,u)`` — the outer CNOTs connect two block-register qubits, - again both inside ``ladder``. The middle Toffoli reuses the walk's - ``_OP_CCX`` shape with a low address bit as its second control. +``address[low ..)`` the block index the walk iterates. + +``select_swap`` is built by **composition** (CUDA-Q >= 0.16: kernels +calling minted kernels compose and ``cudaq.control`` propagates through +the calls): a parent kernel carves the honest sub-views out of +``address`` and ``ladder`` with qview slices and calls three +sub-kernels, ``W S C S^-1 W``: + +- ``W`` — the block-index walk, a plain + :func:`~cudaq_algorithms.primitives.unary_iteration_kernels` mint over + the high address bits whose *target view is the block registers*: the + per-block body X's write the visited block's ``B`` entries. +- ``S`` — a dedicated routing kernel over ``(low address bits, + blocks)``: ``low`` rounds of Fredkins, each controlled register swap + being ``b`` Fredkins decomposed ``cswap(c; u, v) = cx(v,u) ccx(u,c,v) + cx(v,u)``. ``S^-1`` is the same Fredkin list reversed (each Fredkin + is self-inverse). +- ``C`` — ``b`` CNOTs copying block slot 0 onto ``output``, inline in + the parent. End-to-end, ``select_swap`` is the five-step program (with ``h + low = address_bits``, ``B = 2^low`` entries per block): @@ -123,27 +120,38 @@ from collections.abc import Sequence -from ._unary_iteration import (_OP_BODY_X, _OP_CCX, _OP_CCX_ADDR_ADDR, - _OP_CCX_CTRL, _OP_CX_ADDR_ADDR, - _OP_CX_ADDR_LADDER, _OP_CX_LADDER_LADDER, - _OP_CX_LADDER_TARGET, _OP_X_ADDR, - _TOFFOLI_OPCODES, _emit_walk, _mint_interpreter, - _walk_toffoli_count, unary_iteration_kernels) +import cudaq + +from ._unary_iteration import (_retain, _walk_toffoli_count, + unary_iteration_kernels) __all__ = ["QROM"] _VARIANTS = ("auto", "select", "select_swap") -# Walk opcodes whose (a, b) operands index the address register, used to -# shift the block-index walk onto the high address bits. -_ADDRESS_OPERANDS = { - _OP_X_ADDR: (0, ), - _OP_CX_ADDR_LADDER: (0, ), - _OP_CCX: (1, ), - _OP_CCX_CTRL: (1, ), - _OP_CX_ADDR_ADDR: (0, 1), - _OP_CCX_ADDR_ADDR: (0, 1), -} + +def _mint_route(swap_c: list, swap_u: list, swap_v: list): + """Mint the routing kernel: Fredkin ``i`` swaps ``blocks[swap_u[i]]`` + and ``blocks[swap_v[i]]`` controlled on ``low[swap_c[i]]``. + + The inverse network is this same mint over the reversed lists (each + Fredkin is self-inverse). + """ + num_swaps = len(swap_c) + + @cudaq.kernel + def primitives_qrom_route(low: cudaq.qview, blocks: cudaq.qview): + for i in range(num_swaps): + c = swap_c[i] + u = swap_u[i] + v = swap_v[i] + # cswap(low[c]; blocks[u], blocks[v]) via one Toffoli. + cx(blocks[v], blocks[u]) + x.ctrl(blocks[u], low[c], blocks[v]) + cx(blocks[v], blocks[u]) + + _retain(primitives_qrom_route) + return primitives_qrom_route def _select_swap_cost(address_bits: int, num_entries: int, output_bits: int, @@ -304,59 +312,59 @@ def block_write(j: int) -> list[tuple[str, int]]: ("x", i * b + t) for t in range(b) if (word >> t) & 1) return gates - walk_ops = _emit_walk(high_bits, num_blocks, False, block_write) # This IS the textbook SELECT-SWAP: a unary-iteration walk over - # the high (block-index) bits whose leaf action writes each - # block, then a low-bit-controlled swap network routes the - # selected entry. We reuse the walk EMITTER rather than a minted - # walk kernel because CUDA-Q kernels cannot call kernels: the - # whole lookup must be one flat tape, so the walk's instructions - # are rebased into the combined kernel's wire layout here. - # In that layout (see "select_swap register layout" in - # _unary_iteration's docstring) the block registers ride in the - # ladder VIEW after the walk lines — "ladder" names the wire - # bundle, not a role — so the walk's leaf-controlled block-write - # X lands as a CNOT between two ladder-view wires. - ops = [] - for op in walk_ops: - opcode, a, bb, c = op - if opcode == _OP_BODY_X: - ops.append((_OP_CX_LADDER_LADDER, a, high_bits + bb, 0)) - continue - operands = [a, bb, c] - for position in _ADDRESS_OPERANDS.get(opcode, ()): - operands[position] += low_bits - ops.append((opcode, *operands)) - walk_ops = ops + # the high (block-index) bits whose target view is the block + # registers (the leaf action writes each visited block), then a + # low-bit-controlled swap network routes the selected entry. + # X-only body: the walk is an involution, skip the adjoint mint. + walk = unary_iteration_kernels(high_bits, + num_blocks, + block_write, + include_adjoint=False) + walk_kernel = walk.kernel # Binary routing network: after stage s (controlled on # address[s]), block slot i (i = 0 mod 2^(s+1)) holds the entry # at low-address offset (i + a_s 2^s + ... + a_0); slot 0 ends # holding the selected entry. Each controlled register swap is b - # Fredkins: cswap(c; u, v) = cx(v, u) ccx(c, u, v) cx(v, u). - # u and v are block-register wires (which live in the ladder - # view — see the rebase note above), so the outer CNOTs of each - # Fredkin appear as _OP_CX_LADDER_LADDER: CNOTs purely between - # the data blocks, exactly the routing network's own gates. - swap_ops = [] + # Fredkins (one Toffoli each). + swap_c: list[int] = [] + swap_u: list[int] = [] + swap_v: list[int] = [] for s in range(low_bits): for i in range(0, size, 1 << (s + 1)): for t in range(b): - u = high_bits + i * b + t - v = high_bits + (i + (1 << s)) * b + t - swap_ops.append((_OP_CX_LADDER_LADDER, v, u, 0)) - swap_ops.append((_OP_CCX, u, s, v)) - swap_ops.append((_OP_CX_LADDER_LADDER, v, u, 0)) - copy_ops = [(_OP_CX_LADDER_TARGET, high_bits + t, t, 0) - for t in range(b)] + swap_c.append(s) + swap_u.append(i * b + t) + swap_v.append((i + (1 << s)) * b + t) + route_kernel = _mint_route(swap_c, swap_u, swap_v) + unroute_kernel = _mint_route(list(reversed(swap_c)), + list(reversed(swap_u)), + list(reversed(swap_v))) # W S C S^-1 W: write, route, copy, unroute, unwrite — clean - # ancillas and an exactly self-inverse lookup. - ops = (walk_ops + swap_ops + copy_ops + list(reversed(swap_ops)) + - walk_ops) - self._kernel = _mint_interpreter(ops, controlled=False, has_work=False) + # ancillas and an exactly self-inverse lookup. The parent carves + # the documented sub-views out of the public (address, ladder, + # output) signature and composes the sub-kernels. + @cudaq.kernel + def primitives_qrom_select_swap(address: cudaq.qview, + ladder: cudaq.qview, + output: cudaq.qview): + low = address[0:low_bits] + high = address[low_bits:] + lines = ladder[0:high_bits] + blocks = ladder[high_bits:] + walk_kernel(high, lines, blocks) + route_kernel(low, blocks) + for t in range(b): + cx(blocks[t], output[t]) + unroute_kernel(low, blocks) + walk_kernel(high, lines, blocks) + + _retain(primitives_qrom_select_swap) + self._kernel = primitives_qrom_select_swap self._num_ladder = high_bits + size * b - self._toffoli_count = sum(1 for op in ops if op[0] in _TOFFOLI_OPCODES) + self._toffoli_count = 2 * walk.toffoli_count + 2 * len(swap_c) @property def data(self) -> tuple[int, ...]: diff --git a/python/cudaq_algorithms/primitives/_unary_iteration.py b/python/cudaq_algorithms/primitives/_unary_iteration.py index 0671c1f..743346f 100644 --- a/python/cudaq_algorithms/primitives/_unary_iteration.py +++ b/python/cudaq_algorithms/primitives/_unary_iteration.py @@ -43,11 +43,13 @@ tree walk (ladder gadgets + body gates) into parallel opcode/operand integer lists, and the minted kernel is a single flat interpreter loop over those captured lists, pruned to the dispatch arms whose opcodes the -tape actually uses (see ``_mint_interpreter``). Flatness is load-bearing: -CUDA-Q's -control-variant generation rejects kernels that call other kernels, so -anything built this way stays ``cudaq.control``-compatible and can sit -inside a controlled SELECT. +tape actually uses (see ``_mint_interpreter``). Flatness was required +through CUDA-Q 0.15, whose control-variant generation rejected kernels +that call other kernels; from 0.16 minted kernels compose (they may be +called from other kernels and the whole composition wrapped in +``cudaq.control``), and the flat tape remains the design for its own +reasons: factory-time bodies, inspectability (``describe()``), and +exhaustive per-opcode verification. Body instruction set. Each item is a tuple whose head names the gate; ``target``/``work`` operands index the target/work registers. The three @@ -174,7 +176,6 @@ def _retain(*kernels) -> None: _OP_Z_LADDER = 17 # z(ladder[a]) _OP_CX_ADDR_ADDR = 18 # cx(address[a], address[b]) _OP_CCX_ADDR_ADDR = 19 # x.ctrl(address[a], address[b], ladder[c]) -_OP_CX_LADDER_TARGET = 20 # cx(ladder[a], target[b]) _BODY_OPCODES = {"x": _OP_BODY_X, "y": _OP_BODY_Y, "z": _OP_BODY_Z} @@ -206,7 +207,7 @@ def _retain(*kernels) -> None: _BASE_OPS = frozenset({ _OP_X_ADDR, _OP_X_LADDER, _OP_CX_ADDR_LADDER, _OP_CX_LADDER_LADDER, _OP_CCX, _OP_BODY_X, _OP_BODY_Y, _OP_BODY_Z, _OP_FREE_X, _OP_FREE_CX, - _OP_Z_LADDER, _OP_CX_ADDR_ADDR, _OP_CCX_ADDR_ADDR, _OP_CX_LADDER_TARGET + _OP_Z_LADDER, _OP_CX_ADDR_ADDR, _OP_CCX_ADDR_ADDR }) _CONTROL_OPS = frozenset({_OP_CX_CTRL_LADDER, _OP_CCX_CTRL}) _WORK_OPS = frozenset( @@ -245,7 +246,6 @@ def _retain(*kernels) -> None: _OP_CX_ADDR_ADDR: "cx(address[{a}] -> address[{b}])", _OP_CCX_ADDR_ADDR: "ccx(address[{a}], address[{b}] -> ladder[{c}]) # Toffoli", - _OP_CX_LADDER_TARGET: "cx(ladder[{a}] -> target[{b}])", } @@ -636,7 +636,6 @@ def _mint_interpreter(ops: list, controlled: bool, has_work: bool): op_z_ladder = _OP_Z_LADDER op_cx_addr_addr = _OP_CX_ADDR_ADDR op_ccx_addr_addr = _OP_CCX_ADDR_ADDR - op_cx_ladder_target = _OP_CX_LADDER_TARGET # Per-arm pruning flags (see the docstring): captured bools fold at # kernel-compile time, so a False flag removes its arm entirely. @@ -660,7 +659,6 @@ def _mint_interpreter(ops: list, controlled: bool, has_work: bool): use_z_ladder = _OP_Z_LADDER in present use_cx_addr_addr = _OP_CX_ADDR_ADDR in present use_ccx_addr_addr = _OP_CCX_ADDR_ADDR in present - use_cx_ladder_target = _OP_CX_LADDER_TARGET in present if controlled and has_work: @@ -735,9 +733,6 @@ def primitives_unary_walk_work_ctrl(control: cudaq.qview, if use_ccx_addr_addr: if op == op_ccx_addr_addr: x.ctrl(address[a], address[b], ladder[c]) - if use_cx_ladder_target: - if op == op_cx_ladder_target: - cx(ladder[a], target[b]) _retain(primitives_unary_walk_work_ctrl) return primitives_unary_walk_work_ctrl @@ -807,9 +802,6 @@ def primitives_unary_walk_work(address: cudaq.qview, if use_ccx_addr_addr: if op == op_ccx_addr_addr: x.ctrl(address[a], address[b], ladder[c]) - if use_cx_ladder_target: - if op == op_cx_ladder_target: - cx(ladder[a], target[b]) _retain(primitives_unary_walk_work) return primitives_unary_walk_work @@ -871,9 +863,6 @@ def primitives_unary_walk_ctrl(control: cudaq.qview, if use_ccx_addr_addr: if op == op_ccx_addr_addr: x.ctrl(address[a], address[b], ladder[c]) - if use_cx_ladder_target: - if op == op_cx_ladder_target: - cx(ladder[a], target[b]) _retain(primitives_unary_walk_ctrl) return primitives_unary_walk_ctrl @@ -925,9 +914,6 @@ def primitives_unary_walk(address: cudaq.qview, ladder: cudaq.qview, if use_ccx_addr_addr: if op == op_ccx_addr_addr: x.ctrl(address[a], address[b], ladder[c]) - if use_cx_ladder_target: - if op == op_cx_ladder_target: - cx(ladder[a], target[b]) _retain(primitives_unary_walk) return primitives_unary_walk diff --git a/tests/python/test_primitives_composition.py b/tests/python/test_primitives_composition.py new file mode 100644 index 0000000..2bad13d --- /dev/null +++ b/tests/python/test_primitives_composition.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Composition as a supported surface: minted kernels as building blocks. + +The package docstring promises that minted kernels may be called from +user kernels, and (on CUDA-Q >= 0.16) that ``cudaq.control`` applied to +the whole composition yields the controlled operation. These tests pin +both halves of that promise: + +- plain composition (a user kernel calling minted kernels) works on + every supported CUDA-Q — kernels calling kernels was always legal; +- controlled composition equals the factory's ``controlled=True`` + variant AND the analytic reference, so the two routes cannot agree by + being wrong the same way. This half is capability-probed and skips + with an honest message where control-variant generation predates the + 0.16 fix. +""" + +import numpy as np +import pytest + +import cudaq + +from cudaq_algorithms.primitives import unary_iteration_kernels +from test_primitives_qrom import _control_propagates_through_composition + +cudaq.set_target("qpp-cpu") + +_MARKED = (0, 3) + + +def _z_body(k): + return [("z", 0)] if k in _MARKED else [] + + +def test_user_kernel_composes_minted_walk_and_its_adjoint(): + # Plain composition, no control: a user kernel calls the minted walk + # and its hand-written inverse back to back — identity on every + # branch of a superposed address. Valid on CUDA-Q 0.15 and 0.16 + # alike; this is the un-gated half of the composition promise. + def body(k): + if k % 2 == 1: + return [("x", 0), ("z", 0), ("y", 1)] + return [("x", 1)] + + walk = unary_iteration_kernels(2, 4, body) + kernel = walk.kernel + kernel_adj = walk.kernel_adj + + @cudaq.kernel + def user_program(address: cudaq.qview, ladder: cudaq.qview, + target: cudaq.qview): + kernel(address, ladder, target) + kernel_adj(address, ladder, target) + + @cudaq.kernel + def run(): + address = cudaq.qvector(2) + ladder = cudaq.qvector(2) + target = cudaq.qvector(2) + x(target[0]) + for j in range(2): + h(address[j]) + user_program(address, ladder, target) + + state = np.array(cudaq.get_state(run)) + expected = np.zeros(1 << 6, dtype=np.complex128) + for address in range(4): + # Layout: address [0, 2), ladder [2, 4), target [4, 6); target + # holds |01> (bit 0 set) throughout. + expected[address + (1 << 4)] = 0.5 + np.testing.assert_allclose(state, expected, atol=1e-12) + + +def test_control_of_composition_matches_builtin_controlled_walk(): + # The 0.16 half: cudaq.control over a PARENT kernel that calls the + # minted (uncontrolled) walk equals the factory's controlled=True + # walk, and both equal the analytic state — on a superposed control + # and superposed addresses, where a branch-relative phase error + # would be invisible to classical control values. + if not _control_propagates_through_composition(): + pytest.skip("cudaq.control does not propagate through kernels that " + "call kernels on this CUDA-Q (< 0.16)") + + plain = unary_iteration_kernels(2, 4, _z_body) + builtin = unary_iteration_kernels(2, 4, _z_body, controlled=True) + plain_kernel = plain.kernel + builtin_kernel = builtin.kernel + theta = 0.7 + + @cudaq.kernel + def parent(address: cudaq.qview, ladder: cudaq.qview, target: cudaq.qview): + plain_kernel(address, ladder, target) + + @cudaq.kernel + def run_wrapped(): + control = cudaq.qvector(1) + address = cudaq.qvector(2) + ladder = cudaq.qvector(2) + target = cudaq.qvector(1) + ry(theta, control[0]) + for j in range(2): + h(address[j]) + x(target[0]) + cudaq.control(parent, control[0], address, ladder, target) + + @cudaq.kernel + def run_builtin(): + control = cudaq.qvector(1) + address = cudaq.qvector(2) + ladder = cudaq.qvector(2) + target = cudaq.qvector(1) + ry(theta, control[0]) + for j in range(2): + h(address[j]) + x(target[0]) + builtin_kernel(control, address, ladder, target) + + wrapped = np.array(cudaq.get_state(run_wrapped)) + built = np.array(cudaq.get_state(run_builtin)) + + amp = (np.cos(theta / 2), np.sin(theta / 2)) + expected = np.zeros(1 << 6, dtype=np.complex128) + for control_bit in range(2): + for address in range(4): + sign = -1.0 if (control_bit == 1 and address in _MARKED) else 1.0 + # Layout: control 0, address [1, 3), ladder [3, 5), target 5. + expected[control_bit + (address << 1) + (1 << 5)] = \ + sign * amp[control_bit] / 2.0 + + np.testing.assert_allclose(wrapped, expected, atol=1e-12) + np.testing.assert_allclose(built, expected, atol=1e-12) + np.testing.assert_allclose(wrapped, built, atol=1e-12) diff --git a/tests/python/test_primitives_interpreter.py b/tests/python/test_primitives_interpreter.py index 943a5f5..4ae1a8a 100644 --- a/tests/python/test_primitives_interpreter.py +++ b/tests/python/test_primitives_interpreter.py @@ -23,9 +23,8 @@ _BASE_OPS, _CONTROL_OPS, _OP_AND_TT, _OP_AND_WT, _OP_BODY_X, _OP_BODY_X_W, _OP_BODY_Y, _OP_BODY_Z, _OP_BODY_Z_W, _OP_CCX, _OP_CCX_ADDR_ADDR, _OP_CCX_CTRL, _OP_COPY_TW, _OP_CX_ADDR_ADDR, _OP_CX_ADDR_LADDER, - _OP_CX_CTRL_LADDER, _OP_CX_LADDER_LADDER, _OP_CX_LADDER_TARGET, - _OP_FREE_CX, _OP_FREE_X, _OP_X_ADDR, _OP_X_LADDER, _OP_Z_LADDER, _WORK_OPS, - _mint_interpreter) + _OP_CX_CTRL_LADDER, _OP_CX_LADDER_LADDER, _OP_FREE_CX, _OP_FREE_X, + _OP_X_ADDR, _OP_X_LADDER, _OP_Z_LADDER, _WORK_OPS, _mint_interpreter) # Register widths in the harness: control 1, address 2, ladder 2, # target 2, work 2 — enough for two distinct operand indices per @@ -63,8 +62,6 @@ "cx_addr_addr": (_OP_CX_ADDR_ADDR, (0, 1, 0), "cx", [("a", 0), ("a", 1)]), "ccx_addr_addr": (_OP_CCX_ADDR_ADDR, (0, 1, 1), "ccx", [("a", 0), ("a", 1), ("l", 1)]), - "cx_ladder_target": (_OP_CX_LADDER_TARGET, (0, 1, 0), "cx", [("l", 0), - ("t", 1)]), } _ALL_VARIANTS = [(False, False), (True, False), (False, True), (True, True)] diff --git a/tests/python/test_primitives_qrom.py b/tests/python/test_primitives_qrom.py index 3f3ed81..3c87c39 100644 --- a/tests/python/test_primitives_qrom.py +++ b/tests/python/test_primitives_qrom.py @@ -13,6 +13,8 @@ as |0>). """ +import functools + import numpy as np import pytest @@ -21,6 +23,36 @@ from cudaq_algorithms.primitives import QROM +@functools.lru_cache(maxsize=1) +def _control_propagates_through_composition() -> bool: + """Whether ``cudaq.control`` works on kernels that call kernels. + + True from CUDA-Q 0.16; through 0.15 control-variant generation + rejects the composition ("Unhandled controlled quantum kernel call") + and the composed select_swap lookup cannot be externally controlled. + """ + + @cudaq.kernel + def probe_sub(q: cudaq.qview): + x(q[0]) + + @cudaq.kernel + def probe_parent(q: cudaq.qview): + probe_sub(q) + + @cudaq.kernel + def probe(): + control = cudaq.qvector(1) + q = cudaq.qvector(1) + cudaq.control(probe_parent, control[0], q) + + try: + cudaq.get_state(probe) + return True + except RuntimeError: + return False + + def _basis(index: int, num_qubits: int) -> np.ndarray: ket = np.zeros(1 << num_qubits, dtype=np.complex128) ket[index] = 1.0 @@ -174,6 +206,51 @@ def run(): atol=1e-12) +def test_qrom_select_swap_external_control_on_the_composition(): + # The promise behind building select_swap as composed kernels + # (CUDA-Q >= 0.16): cudaq.control applied to the WHOLE composition — + # the parent kernel that calls the walk and routing sub-kernels — + # implements the controlled lookup. Pinned on a superposed control + # and superposed addresses against the dense reference + # |c>|k>|0...0>|y> -> |c>|k>|0...0>|y XOR c * data[k]>. + if not _control_propagates_through_composition(): + pytest.skip("cudaq.control does not propagate through kernels that " + "call kernels on this CUDA-Q (< 0.16)") + data = [3, 0, 5, 6, 1] + address_bits, output_bits = 3, 3 + qrom = QROM(data, + address_bits, + output_bits, + variant="select_swap", + block_size=2) + lookup = qrom.kernel + num_ladder = qrom.num_ladder + theta = 2 * np.arccos(0.6) # control amplitudes (0.6, 0.8) + + @cudaq.kernel + def run(): + control = cudaq.qvector(1) + address_reg = cudaq.qvector(address_bits) + ladder = cudaq.qvector(num_ladder) + output = cudaq.qvector(output_bits) + ry(theta, control[0]) + for b in range(address_bits): + h(address_reg[b]) + cudaq.control(lookup, control[0], address_reg, ladder, output) + + state = np.array(cudaq.get_state(run)) + total = 1 + address_bits + num_ladder + output_bits + shift = 1 + address_bits + num_ladder + amp = {0: 0.6, 1: 0.8} + expected = np.zeros(1 << total, dtype=np.complex128) + for c in range(2): + for k in range(1 << address_bits): + word = data[k] if (c == 1 and k < len(data)) else 0 + index = c + (k << 1) + (word << shift) + expected[index] = amp[c] / np.sqrt(1 << address_bits) + np.testing.assert_allclose(state, expected, atol=1e-12) + + def test_qrom_auto_dispatch_picks_the_cheaper_variant(): # Big table, narrow output: routing is cheap, the block walk is a # fraction of the full walk -> select_swap wins.