Skip to content

Quantum embeddings: amplitude_encode accepts nested Python sequences, plus smaller follow-ups #4791

Description

@spital

Hi CUDA-Q team, thank you for adding the quantum embedding helpers on main and resolving #2982. I had been working locally on the same feature area during unitaryHACK 2026, but I was not able to submit that work because I had reached the unitaryHACK 2026 PR limit. After testing the current main branch locally, I found one silent correctness issue and a few smaller robustness / docs follow-ups that seem worth tracking.

I checked the items below against main at commit 3344a8a99 (3344a8a997b93bba04e89a524386cc45c0cb3176, 2026-06-27). I did not find a duplicate issue for these in NVIDIA/cuda-quantum.

To reproduce from the same revision:

git clone --filter=blob:none https://github.com/NVIDIA/cuda-quantum.git
cd cuda-quantum
git checkout 3344a8a99   # or use current main
git rev-parse --short HEAD

Summary

# Area Item Severity
1 Python correctness amplitude_encode rejects 2-D NumPy arrays but silently flattens equivalent nested Python list / tuple inputs High
2 C++ portability runtime/cudaq/builder/kernels.h uses std::optional without directly including <optional> Low
3 Docs Unicode math glyphs are used inside Doxygen / Sphinx math expressions Low
4 C++ robustness amplitude_encode(const state&) reads get_tensor(0) without validating tensor count / rank in that helper Low-Med
5 C++ robustness nextPowerOfTwo can overflow into a non-terminating loop for pathological sizes Low
6 Python API consistency Builder-mode angular_encode rejects NumPy angle arrays, while amplitude_encode accepts NumPy arrays Low / maybe intentional

1. amplitude_encode silently flattens nested Python sequences

amplitude_encode documents that input must be one-dimensional, and it correctly rejects a 2-D NumPy array. The equivalent nested Python list or tuple, however, takes the list/tuple branch and is flattened with .ravel() before any ndim check.

Source check:

grep -n -A1 'isinstance(data, (list, tuple))' python/cudaq/contrib/encoding.py
grep -n -A2 'isinstance(data, np.ndarray)' python/cudaq/contrib/encoding.py
rg -n 'test_amplitude_encode_2d|np\.eye|expected a 1D vector' python/tests/contrib/test_amplitude_encode.py

Observed on 3344a8a99:

54:    if isinstance(data, (list, tuple)):
55-        return np, np.asarray(data, dtype=dtype).ravel()
57:    if isinstance(data, np.ndarray):
58-        if data.ndim != 1:
59-            raise ValueError("amplitude_encode: expected a 1D vector.")
99:def test_amplitude_encode_2d_rejected():
101:        cudaq.contrib.amplitude_encode(np.eye(2), pad=0)

Expected: all non-1D inputs raise ValueError("amplitude_encode: expected a 1D vector."), independent of whether the caller used a NumPy array, nested list, or nested tuple.

Observed: the existing test covers only the NumPy path. The list/tuple path accepts the nested input and normalizes the flattened vector.

Source-build runtime repro:

import numpy as np
import cudaq

for label, data in [
    ("NumPy 2-D array", np.eye(2)),
    ("nested Python list", [[1, 0], [0, 1]]),
    ("tuple of tuples", ((1, 0), (0, 1))),
]:
    try:
        state = cudaq.contrib.amplitude_encode(data, pad=0)
        print(label, "accepted:", state.num_qubits(), np.asarray(state).round(4))
    except Exception as e:
        print(label, "raised:", type(e).__name__, e)

Expected output:

NumPy 2-D array raised: ValueError amplitude_encode: expected a 1D vector.
nested Python list raised: ValueError amplitude_encode: expected a 1D vector.
tuple of tuples raised: ValueError amplitude_encode: expected a 1D vector.

Observed from the current code path:

NumPy 2-D array raised: ValueError amplitude_encode: expected a 1D vector.
nested Python list accepted: 2 [0.7071+0.j 0.    +0.j 0.    +0.j 0.7071+0.j]
tuple of tuples accepted: 2 [0.7071+0.j 0.    +0.j 0.    +0.j 0.7071+0.j]

This looks like a silent correctness issue: a caller who accidentally passes a matrix-shaped nested sequence gets a valid-looking two-qubit state instead of the documented shape error.

2. kernels.h uses std::optional without including <optional>

runtime/cudaq/builder/kernels.h declares validateAngularEncodeSizes(std::optional<std::size_t> qSize, ...), but does not directly include <optional>.

Verify:

grep -n 'std::optional' runtime/cudaq/builder/kernels.h
grep -n '#include <optional>' runtime/cudaq/builder/kernels.h || true
grep -n '^#include' runtime/cudaq/builder/kernels.h

Observed:

194:inline void validateAngularEncodeSizes(std::optional<std::size_t> qSize,
11:#include "kernel_builder.h"
12:#include <complex>
13:#include <functional>
14:#include <span>
15:#include <stdexcept>

Expected: a header that names std::optional includes <optional> directly. It may compile today via a transitive include, but the direct include would make the header more portable.

3. Unicode glyphs inside math expressions

Some Doxygen / Sphinx math expressions use literal Unicode glyphs such as α, θ, , and ψ inside math. HTML rendering may tolerate this, but LaTeX/PDF builds are usually more robust when the math uses commands such as \alpha, \theta, \rangle, and \psi consistently.

Verify:

grep -nP '[^\x00-\x7F]' runtime/cudaq/algorithms/encoding.h python/cudaq/contrib/encoding.py

Selected observed lines:

runtime/cudaq/algorithms/encoding.h:34:///    Coefficients are \f$α_i = x'_i / \|\mathbf{x}'\|_2\f$.
runtime/cudaq/algorithms/encoding.h:37:///    \f$|\psi\rangle = \sum_{i=0}^{N-1} α_i |i\rangle\f$, where
python/cudaq/contrib/encoding.py:89:       :math:`|\psi⟩ = \sum_{i=0}^{N-1} α_i |i⟩`, where
python/cudaq/contrib/encoding.py:204:       |ψ⟩
python/cudaq/contrib/encoding.py:213:       R_P(θ) = e^{-i θ P / 2}, \quad P \in \{X, Y, Z\},

Expected: math expressions use LaTeX commands consistently.

Observed: literal glyphs are mixed with LaTeX commands in the same formulas.

4. amplitude_encode(const state&) does not validate state tensor shape in the helper

The C++ state overload converts input through stateToAmplitudeVector, which reads the first tensor and then flat-indexes the state. I did not find a guard in that helper that the input has exactly one rank-1 state-vector tensor.

Inspect:

grep -nE 'get_tensor\(0\)|get_num_tensors|get_rank' runtime/cudaq/algorithms/encoding.cpp
sed -n '60,72p' runtime/cudaq/algorithms/encoding.cpp

Observed:

61:  const auto tensor = data.get_tensor(0);
std::vector<std::complex<double>> stateToAmplitudeVector(const state &data) {
  const auto tensor = data.get_tensor(0);
  const std::size_t numElements = tensor.get_num_elements();
  if (numElements == 0)
    throw std::invalid_argument("amplitude_encode: input must be non-empty.");

  std::vector<std::complex<double>> vec;
  vec.reserve(numElements);
  for (std::size_t i = 0; i < numElements; ++i)
    vec.push_back(data[i]);
  return vec;
}

Expected: either document that cudaq::state inputs are guaranteed to be a single rank-1 state vector before this point, or reject unsupported shapes with a clear exception.

Observed: this helper only checks numElements == 0; a non-vector-shaped state appears to be flat-read and re-normalized.

5. nextPowerOfTwo can overflow for pathological sizes

This is defensive only, because such an input cannot realistically fit in memory. Still, the helper can wrap p to zero for a size larger than the largest representable power of two, after which p < n remains true forever.

Where:

sed -n '17,26p' runtime/cudaq/algorithms/encoding.cpp

Standalone reproduction of the same loop:

g++ -std=c++20 -O2 -x c++ -o /tmp/cudaq_npot_check - <<'CPP'
#include <cstdio>
#include <cstddef>
int main() {
  std::size_t n = (std::size_t(1) << 63) + 1;
  std::size_t p = 1;
  for (int iters = 1; p < n; ++iters) {
    p <<= 1;
    if (iters > 70) {
      std::printf("p=%zu after wrap; loop would not terminate without cap\n", p);
      return 0;
    }
  }
  std::printf("terminated p=%zu\n", p);
}
CPP
/tmp/cudaq_npot_check

Observed:

p=0 after wrap; loop would not terminate without cap

Expected: reject impossible sizes before the shift can overflow, or otherwise avoid the wraparound loop.

6. Builder-mode angular_encode rejects NumPy angle arrays

This may be intentional, but it is a small API inconsistency with amplitude_encode, which accepts NumPy arrays. In builder mode, angular_encode accepts list / tuple or a kernel list argument, so a 1-D NumPy array of angles raises TypeError.

Inspect:

sed -n '162,190p' python/cudaq/contrib/encoding.py

Source-build runtime repro:

import numpy as np
import cudaq

kernel = cudaq.make_kernel()
q = kernel.qalloc(2)

cudaq.contrib.angular_encode(kernel, q, [0.1, 0.2], rotation="Y")
cudaq.contrib.angular_encode(kernel, q, np.array([0.1, 0.2]), rotation="Y")

Expected, if NumPy arrays are intended to be accepted consistently: both calls append the corresponding rotations.

Observed:

TypeError: cudaq.contrib.angular_encode: angles must be a list[float] or a kernel list argument

If NumPy arrays are intentionally unsupported in builder mode, documenting that explicitly would be enough.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions