Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/api/qumat_qdp.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ loaders:
- qumat_qdp.backend
- qumat_qdp.tensor
- qumat_qdp.loader
- qumat_qdp.preprocessing
- qumat_qdp.triton_amd

# Generated QDP docs intentionally load implementation modules so pydoc-markdown
Expand Down
1 change: 1 addition & 0 deletions docs/api/qumat_qdp_header.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ return values, and exceptions.
- [QuantumDataLoader](#quantumdataloader) — build iterable encoding pipelines
- [QdpBenchmark](#qdpbenchmark) — measure throughput and latency
- [QdpTensor](#qdptensor) — exchange encoded results through DLPack
- [Preprocessor](#preprocessor) — validate inputs and compute L2 norms on the CPU
75 changes: 75 additions & 0 deletions docs/qdp/python-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ sidebar_label: Python API
- A unified encoding facade via `QdpEngine`
- Benchmark and loader builders
- Backend-selection helpers
- CPU-side input validation and L2 norms via `Preprocessor`
- Optional access to native `_qdp` types when the Rust extension is installed

## Overview
Expand All @@ -22,6 +23,7 @@ from qumat_qdp import (
Backend,
LatencyResult,
NativeQuantumTensor,
Preprocessor,
QdpBenchmark,
QdpEngine,
QdpTensor,
Expand All @@ -42,6 +44,7 @@ Key exports:
- `QdpBenchmark`: benchmark builder for the Rust pipeline or explicit PyTorch reference backend.
- `QuantumDataLoader`: batch iterator builder for synthetic or file-backed inputs.
- `QdpTensor` / `QuantumTensor`: thin DLPack facade type.
- `Preprocessor`: CPU-side input checks and L2 norms (the same checks the encoders run before using the GPU).
- `Backend`, `BACKEND`, `force_backend`: backend detection and override helpers for the `_qdp` / PyTorch-reference selection layer.
- `TritonAmdEngine`, `is_triton_amd_available`: direct AMD-route entry points.
- `RustQdpEngine`, `NativeQuantumTensor`: native `_qdp` exports when the extension is available.
Expand Down Expand Up @@ -151,6 +154,78 @@ qt = QdpEngine(device_id=0, backend="cuda").encode(
tensor = torch.from_dlpack(qt)
```

## Preprocessing API

`Preprocessor` exposes the CPU-side checks that the native encoders run
before any data reaches the GPU. Use it to fail fast on bad input, or to
see the norm that amplitude encoding divides by. All four methods are
static, run on the CPU, and need the `_qdp` extension but not a GPU (a
stub build without the CUDA toolkit works).

| Method | Returns | Checks |
|--------|---------|--------|
| `validate_input(data, num_qubits)` | `None` | `1 <= num_qubits <= 30`, `data` non-empty, `len(data) <= 2**num_qubits` |
| `calculate_l2_norm(data)` | `float` | norm is non-zero and finite (no NaN / Inf) |
| `validate_batch(batch_data, num_samples, sample_size, num_qubits)` | `None` | `num_samples > 0`, length is `num_samples * sample_size`, `sample_size <= 2**num_qubits` |
| `calculate_batch_l2_norms(batch_data, num_samples, sample_size)` | `numpy.ndarray` (float64, one norm per sample) | every sample norm is non-zero and finite |

Input notes:

- `data` is a 1D NumPy array or a sequence of numbers. Other dtypes are
converted to `float64`; a C-contiguous `float64` array is used without
a copy.
- `batch_data` is either 2D with shape `(num_samples, sample_size)` or the
same values flattened to 1D in row-major order. Non-contiguous and
Fortran-ordered arrays are read in logical row order.
- A failed check raises `RuntimeError` whose message starts with
`"Invalid input:"` (the same message `QdpEngine.encode` reports). A 1D
argument given a 2D array raises `TypeError`; values NumPy cannot
convert to `float64` (strings, ragged lists) raise `ValueError`.

### Why the norm matters for amplitude encoding

Amplitude encoding stores a feature vector `x` as the amplitudes of an
`n`-qubit state. A quantum state must have total probability 1, so QDP
loads `x / ||x||` rather than `x`, padding with zeros up to `2**n`
entries. `calculate_l2_norm` returns that `||x||`. A vector of all zeros
has norm 0 and cannot be normalized, so it is rejected instead of being
encoded as an invalid state; NaN or infinite values are rejected for the
same reason.

```python
import numpy as np
from qumat_qdp import Preprocessor

x = np.array([3.0, 4.0]) # 2 features -> fits in 1 qubit (2**1 = 2)

Preprocessor.validate_input(x, num_qubits=1) # raises on bad input
norm = Preprocessor.calculate_l2_norm(x) # 5.0
amplitudes = x / norm # [0.6, 0.8]
print(norm, amplitudes, (amplitudes**2).sum()) # probabilities sum to 1

try:
Preprocessor.calculate_l2_norm([0.0, 0.0])
except RuntimeError as err:
print(err) # Invalid input: Input data has zero norm
```

`QdpEngine(...).encode(x, num_qubits=1)` on a GPU produces the state with
the same amplitudes `[0.6, 0.8]` (as complex numbers).

Batch form, for a 2D array of samples:

```python
batch = np.array([[3.0, 4.0], [1.0, 1.0]])
Preprocessor.validate_batch(batch, num_samples=2, sample_size=2, num_qubits=1)
norms = Preprocessor.calculate_batch_l2_norms(batch, 2, 2) # [5.0, 1.414...]
states = batch / norms[:, None]
```

Naming note: these method names mirror the Rust
`qdp_core::preprocessing::Preprocessor` API and are the stable public
names. If they are ever renamed, the old names should keep working with a
`DeprecationWarning` for at least one release rather than being removed.

## Benchmark API

`QdpBenchmark` is a builder for throughput and latency runs.
Expand Down
6 changes: 5 additions & 1 deletion qdp/qdp-python/qumat_qdp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@

Public API: QdpEngine (unified router), QdpTensor/QuantumTensor (DLPack facade),
QdpBenchmark, ThroughputResult, LatencyResult (benchmark API),
QuantumDataLoader (data loader iterator).
QuantumDataLoader (data loader iterator), Preprocessor (CPU input
validation and L2 norms).

Usage:
from qumat_qdp import QdpEngine, QuantumTensor
from qumat_qdp import QdpBenchmark, ThroughputResult, LatencyResult
from qumat_qdp import QuantumDataLoader
from qumat_qdp import Preprocessor
"""

from __future__ import annotations
Expand Down Expand Up @@ -72,6 +74,7 @@ def is_cuda_available() -> bool:
)
from qumat_qdp.backend import QdpEngine
from qumat_qdp.loader import QuantumDataLoader
from qumat_qdp.preprocessing import Preprocessor
from qumat_qdp.tensor import QdpTensor, QuantumTensor
from qumat_qdp.triton_amd import TritonAmdEngine, is_triton_amd_available

Expand All @@ -80,6 +83,7 @@ def is_cuda_available() -> bool:
"Backend",
"LatencyResult",
"NativeQuantumTensor",
"Preprocessor",
"QdpBenchmark",
"QdpEngine",
"QdpTensor",
Expand Down
162 changes: 162 additions & 0 deletions qdp/qdp-python/qumat_qdp/preprocessing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""CPU-side input validation and L2 norms used before QDP encoding.

``Preprocessor`` exposes the same checks the native encoders run before any
data is copied to the GPU, so callers can validate inputs (and inspect the
norm amplitude encoding will divide by) without a GPU. The work is done by
the Rust ``_qdp`` extension; this module only adds documentation and a clear
error when the extension is not installed.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from qumat_qdp._backend import get_qdp

if TYPE_CHECKING:
import numpy as np
from numpy.typing import ArrayLike

_MISSING_EXTENSION_MSG = (
"qumat_qdp.Preprocessor requires the _qdp Rust extension. "
"Build it with: maturin develop --manifest-path qdp/qdp-python/Cargo.toml"
)


def _native() -> Any:
qdp = get_qdp()
native = getattr(qdp, "Preprocessor", None) if qdp is not None else None
if native is None:
raise RuntimeError(_MISSING_EXTENSION_MSG)
return native


class Preprocessor:
"""Validate classical input and compute L2 norms before encoding.

All methods are static and run on the CPU; no GPU or CUDA toolkit is
needed. Inputs may be NumPy arrays or plain Python sequences. Values are
converted to ``float64`` (a C-contiguous ``float64`` array is used without
a copy). Every method raises ``RuntimeError`` with a message starting
with ``"Invalid input:"`` when a check fails, matching the errors that
``QdpEngine.encode`` reports for the same input.

Example -- check a sample, then compute the amplitudes that amplitude
encoding will load::

import numpy as np
from qumat_qdp import Preprocessor

x = np.array([3.0, 4.0]) # 2 features
Preprocessor.validate_input(x, num_qubits=1)
norm = Preprocessor.calculate_l2_norm(x) # 5.0
amplitudes = x / norm # [0.6, 0.8]

Amplitude encoding stores ``x / ||x||`` so that the squared amplitudes
(measurement probabilities) sum to 1. A zero vector has no such
normalization, so it is rejected rather than silently encoded.

:raises RuntimeError: From any method, if the ``_qdp`` extension is not
installed.
"""

@staticmethod
def validate_input(data: ArrayLike, num_qubits: int) -> None:
"""Check that one sample can be encoded with ``num_qubits`` qubits.

:param data: One sample as a 1D array or sequence of floats.
:param num_qubits: Number of qubits in the target state, between 1
and 30 inclusive.
:returns: ``None`` when the input is valid.
:raises RuntimeError: If ``num_qubits`` is 0 or greater than 30, if
``data`` is empty, or if ``len(data) > 2**num_qubits``.
:raises TypeError: If ``data`` is not one-dimensional.
:raises ValueError: If ``data`` holds values NumPy cannot convert to
``float64`` (for example strings or ragged nested lists).
"""
_native().validate_input(data, num_qubits)

@staticmethod
def calculate_l2_norm(data: ArrayLike) -> float:
"""Return the Euclidean (L2) norm ``sqrt(sum(x_i ** 2))`` of a sample.

This is the value amplitude encoding divides by. The sum is computed
in parallel, so the last bits may differ from ``numpy.linalg.norm``.

:param data: One sample as a 1D array or sequence of floats.
:returns: The norm as a positive, finite Python ``float``.
:raises RuntimeError: If the norm is zero (including empty input), or
if ``data`` contains NaN or infinite values.
:raises TypeError: If ``data`` is not one-dimensional.
:raises ValueError: If ``data`` holds values NumPy cannot convert to
``float64`` (for example strings or ragged nested lists).
"""
return _native().calculate_l2_norm(data)

@staticmethod
def validate_batch(
batch_data: ArrayLike,
num_samples: int,
sample_size: int,
num_qubits: int,
) -> None:
"""Check that a batch of samples can be encoded with ``num_qubits``.

:param batch_data: Either a 2D array of shape
``(num_samples, sample_size)`` or the same values flattened to 1D
in row-major order.
:param num_samples: Number of samples in the batch; must be > 0.
:param sample_size: Number of values in each sample; must be at most
``2**num_qubits``.
:param num_qubits: Number of qubits in each target state, between 1
and 30 inclusive.
:returns: ``None`` when the batch is valid.
:raises RuntimeError: If ``num_samples`` is 0, if the data length is
not ``num_samples * sample_size`` (or a 2D shape disagrees), if
``num_qubits`` is out of range, or if
``sample_size > 2**num_qubits``.
:raises ValueError: If ``batch_data`` holds values NumPy cannot
convert to ``float64``.
"""
_native().validate_batch(batch_data, num_samples, sample_size, num_qubits)

@staticmethod
def calculate_batch_l2_norms(
batch_data: ArrayLike,
num_samples: int,
sample_size: int,
) -> np.ndarray:
"""Return the L2 norm of every sample in a batch.

:param batch_data: Either a 2D array of shape
``(num_samples, sample_size)`` or the same values flattened to 1D
in row-major order.
:param num_samples: Number of samples in the batch; must be > 0.
:param sample_size: Number of values in each sample; must be > 0.
:returns: A 1D ``float64`` NumPy array of length ``num_samples``.
:raises RuntimeError: If the data length does not equal
``num_samples * sample_size``, or if any sample has a zero, NaN or
infinite norm (the message names the first bad sample index).
:raises ValueError: If ``batch_data`` holds values NumPy cannot
convert to ``float64``.
"""
return _native().calculate_batch_l2_norms(batch_data, num_samples, sample_size)


__all__ = ["Preprocessor"]
3 changes: 3 additions & 0 deletions qdp/qdp-python/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ mod constants;
mod dlpack;
mod engine;
mod loader;
mod preprocessing;
mod pytorch;
mod tensor;

use engine::QdpEngine;
use preprocessing::PyPreprocessor;
use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use tensor::QuantumTensor;
Expand Down Expand Up @@ -90,6 +92,7 @@ fn _qdp(m: &Bound<'_, PyModule>) -> PyResult<()> {

m.add_class::<QdpEngine>()?;
m.add_class::<QuantumTensor>()?;
m.add_class::<PyPreprocessor>()?;
m.add_function(wrap_pyfunction!(cuda_available, m)?)?;
#[cfg(target_os = "linux")]
m.add_class::<PyQuantumLoader>()?;
Expand Down
Loading
Loading