diff --git a/docs/qdp/python-api.md b/docs/qdp/python-api.md index e0efd72c80..cfb67ac978 100644 --- a/docs/qdp/python-api.md +++ b/docs/qdp/python-api.md @@ -82,7 +82,7 @@ Builder; chain methods then call `run_throughput()` or `run_latency()`. | Method | Description | |--------|-------------| | `qubits(n)` | Number of qubits. | -| `encoding(method)` | `"amplitude"` \| `"angle"` \| `"basis"`. | +| `encoding(method)` | `"amplitude"` \| `"angle"` \| `"basis"` \| `"iqp"` \| `"iqp-z"`. | | `batches(total, size=64)` | Total batches and batch size. | | `prefetch(n)` | No-op (API compatibility). | | `warmup(n)` | Warmup batch count. | @@ -147,7 +147,7 @@ Builder for a synthetic-data loader. Calling `iter(loader)` (or `for qt in loade | Method | Description | |--------|-------------| | `qubits(n)` | Number of qubits. | -| `encoding(method)` | `"amplitude"` \| `"angle"` \| `"basis"`. | +| `encoding(method)` | `"amplitude"` \| `"angle"` \| `"basis"` \| `"iqp"` \| `"iqp-z"`. | | `batches(total, size=64)` | Total batches and batch size. | | `source_synthetic(total_batches=None)` | Synthetic data (default); optional override for total batches. | | `seed(s)` | RNG seed for reproducibility. | diff --git a/qdp/qdp-core/src/pipeline_runner.rs b/qdp/qdp-core/src/pipeline_runner.rs index 42bb5cc655..fc19dd6a34 100644 --- a/qdp/qdp-core/src/pipeline_runner.rs +++ b/qdp/qdp-core/src/pipeline_runner.rs @@ -537,11 +537,13 @@ pub fn vector_len(num_qubits: u32, encoding_method: &str) -> usize { match encoding_method.to_lowercase().as_str() { "angle" => n, "basis" => 1, + "iqp-z" => n, + "iqp" => n + n.saturating_mul(n.saturating_sub(1)) / 2, _ => 1 << n, // amplitude } } -/// Deterministic sample generation matching Python utils.build_sample (amplitude/angle/basis). +/// Deterministic sample generation matching Python benchmark helpers. fn fill_sample(seed: u64, out: &mut [f64], encoding_method: &str, num_qubits: usize) -> Result<()> { let len = out.len(); if len == 0 { @@ -562,6 +564,13 @@ fn fill_sample(seed: u64, out: &mut [f64], encoding_method: &str, num_qubits: us *v = mixed as f64 * scale; } } + "iqp-z" | "iqp" => { + let scale = (2.0 * PI) / len as f64; + for (i, v) in out.iter_mut().enumerate() { + let mixed = (i as u64 + seed) % (len as u64); + *v = mixed as f64 * scale; + } + } _ => { // amplitude let mask = (len - 1) as u64; @@ -631,6 +640,13 @@ fn fill_sample_f32( *v = mixed as f32 * scale; } } + "iqp-z" | "iqp" => { + let scale = (2.0 * std::f32::consts::PI) / len as f32; + for (i, v) in out.iter_mut().enumerate() { + let mixed = (i as u64 + seed) % (len as u64); + *v = mixed as f32 * scale; + } + } _ => { // amplitude let mask = (len - 1) as u64; @@ -829,6 +845,16 @@ mod tests { assert_generate_and_inplace_match("basis"); } + #[test] + fn generate_batch_matches_fill_batch_inplace_iqp_z() { + assert_generate_and_inplace_match("iqp-z"); + } + + #[test] + fn generate_batch_matches_fill_batch_inplace_iqp() { + assert_generate_and_inplace_match("iqp"); + } + #[test] fn adjacent_batches_differ_amplitude() { assert_adjacent_batches_differ("amplitude"); @@ -844,6 +870,16 @@ mod tests { assert_adjacent_batches_differ("basis"); } + #[test] + fn adjacent_batches_differ_iqp_z() { + assert_adjacent_batches_differ("iqp-z"); + } + + #[test] + fn adjacent_batches_differ_iqp() { + assert_adjacent_batches_differ("iqp"); + } + #[test] fn test_seed_none() { let config = PipelineConfig { @@ -1165,6 +1201,35 @@ mod tests { assert!(super::encoding_supports_f32("AMPLITUDE")); assert!(!super::encoding_supports_f32("angle")); assert!(!super::encoding_supports_f32("basis")); + assert!(!super::encoding_supports_f32("iqp-z")); assert!(!super::encoding_supports_f32("iqp")); } + + #[test] + fn test_vector_len_for_iqp_variants() { + assert_eq!(super::vector_len(4, "iqp-z"), 4); + assert_eq!(super::vector_len(4, "iqp"), 10); + } + + #[test] + fn test_iqp_samples_in_angle_range() { + let config = PipelineConfig { + num_qubits: 4, + batch_size: 3, + encoding_method: "iqp".to_string(), + seed: Some(7), + ..Default::default() + }; + + let vector_len = super::vector_len(config.num_qubits, &config.encoding_method); + let batch = generate_batch(&config, 0, vector_len); + let upper = 2.0 * PI; + for &value in &batch { + assert!( + (0.0..upper).contains(&value), + "iqp value should be in [0, 2pi), got {}", + value + ); + } + } } diff --git a/qdp/qdp-python/benchmark/README.md b/qdp/qdp-python/benchmark/README.md index 4e01d9835b..17ac17c2c2 100644 --- a/qdp/qdp-python/benchmark/README.md +++ b/qdp/qdp-python/benchmark/README.md @@ -87,7 +87,7 @@ Notes: - `--frameworks` is a comma-separated list or `all`. Options: `mahout`, `pennylane`, `qiskit-init`, `qiskit-statevector`. -- `--encoding-method` selects the encoding method: `amplitude` (default) or `basis`. +- `--encoding-method` selects the encoding method: `amplitude` (default), `angle`, `basis`, `iqp`, or `iqp-z`. - The latency test reports average milliseconds per vector. - Flags: - `--qubits`: controls vector length (`2^qubits`). @@ -115,7 +115,7 @@ Notes: - `--frameworks` is a comma-separated list or `all`. Options: `mahout`, `pennylane`, `qiskit`. -- `--encoding-method` selects the encoding method: `amplitude` (default) or `basis`. +- `--encoding-method` selects the encoding method: `amplitude` (default), `angle`, `basis`, `iqp`, or `iqp-z`. - Throughput is reported in vectors/sec (higher is better). ## Dependency Notes diff --git a/qdp/qdp-python/benchmark/benchmark_latency.py b/qdp/qdp-python/benchmark/benchmark_latency.py index 5b53c56c86..4194d99c37 100644 --- a/qdp/qdp-python/benchmark/benchmark_latency.py +++ b/qdp/qdp-python/benchmark/benchmark_latency.py @@ -113,6 +113,16 @@ def run_mahout( return result.duration_sec, result.latency_ms_per_vector +def _sample_dim(num_qubits: int, encoding_method: str) -> int: + if encoding_method == "basis": + return 1 + if encoding_method in {"angle", "iqp-z"}: + return num_qubits + if encoding_method == "iqp": + return num_qubits + num_qubits * (num_qubits - 1) // 2 + return 1 << num_qubits + + def run_pennylane(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): if not HAS_PENNYLANE: print("[PennyLane] Not installed, skipping.") @@ -236,8 +246,8 @@ def main() -> None: "--encoding-method", type=str, default="amplitude", - choices=["amplitude", "angle", "basis"], - help="Encoding method to use for Mahout (amplitude, angle, or basis).", + choices=["amplitude", "angle", "basis", "iqp", "iqp-z"], + help="Encoding method to use for Mahout (amplitude, angle, basis, iqp, or iqp-z).", ) args = parser.parse_args() @@ -249,8 +259,19 @@ def main() -> None: except ValueError as exc: parser.error(str(exc)) + # TODO: fix this with #1252 in the future. + if args.encoding_method in {"iqp", "iqp-z"}: + unsupported = [name for name in frameworks if name != "mahout"] + if unsupported: + print( + "Warning: IQP benchmarks in this script currently support only " + "framework 'mahout'; skipping unsupported frameworks: " + f"{', '.join(unsupported)}." + ) + frameworks = ["mahout"] + total_vectors = args.batches * args.batch_size - vector_len = 1 << args.qubits + vector_len = _sample_dim(args.qubits, args.encoding_method) print(f"Generating {total_vectors} samples of {args.qubits} qubits...") print(f" Batch size : {args.batch_size}") diff --git a/qdp/qdp-python/benchmark/benchmark_pytorch_ref.py b/qdp/qdp-python/benchmark/benchmark_pytorch_ref.py index ad95079084..de8d44a5e6 100644 --- a/qdp/qdp-python/benchmark/benchmark_pytorch_ref.py +++ b/qdp/qdp-python/benchmark/benchmark_pytorch_ref.py @@ -68,6 +68,8 @@ def _sample_dim(encoding_method: str, num_qubits: int) -> int: return 1 if encoding_method == "angle": return num_qubits + if encoding_method == "iqp-z": + return num_qubits if encoding_method == "iqp": return num_qubits + num_qubits * (num_qubits - 1) // 2 return 1 << num_qubits @@ -270,7 +272,7 @@ def main() -> None: parser.add_argument( "--encoding-method", default="amplitude", - choices=["amplitude", "angle", "basis", "iqp"], + choices=["amplitude", "angle", "basis", "iqp", "iqp-z"], ) parser.add_argument("--warmup", type=int, default=5) parser.add_argument("--trials", type=int, default=3) diff --git a/qdp/qdp-python/benchmark/benchmark_throughput.py b/qdp/qdp-python/benchmark/benchmark_throughput.py index 8abbf7c79b..d060525e28 100644 --- a/qdp/qdp-python/benchmark/benchmark_throughput.py +++ b/qdp/qdp-python/benchmark/benchmark_throughput.py @@ -105,6 +105,16 @@ def run_mahout( return result.duration_sec, result.vectors_per_sec +def _sample_dim(num_qubits: int, encoding_method: str) -> int: + if encoding_method == "basis": + return 1 + if encoding_method in {"angle", "iqp-z"}: + return num_qubits + if encoding_method == "iqp": + return num_qubits + num_qubits * (num_qubits - 1) // 2 + return 1 << num_qubits + + def run_pennylane(num_qubits: int, total_batches: int, batch_size: int, prefetch: int): if not HAS_PENNYLANE: print("[PennyLane] Not installed, skipping.") @@ -209,8 +219,8 @@ def main() -> None: "--encoding-method", type=str, default="amplitude", - choices=["amplitude", "angle", "basis"], - help="Encoding method to use for Mahout (amplitude, angle, or basis).", + choices=["amplitude", "angle", "basis", "iqp", "iqp-z"], + help="Encoding method to use for Mahout (amplitude, angle, basis, iqp, or iqp-z).", ) args = parser.parse_args() @@ -219,8 +229,19 @@ def main() -> None: except ValueError as exc: parser.error(str(exc)) + # TODO: fix this with #1252 in the future. + if args.encoding_method in {"iqp", "iqp-z"}: + unsupported = [name for name in frameworks if name != "mahout"] + if unsupported: + print( + "Warning: IQP benchmarks in this script currently support only " + "framework 'mahout'; skipping unsupported frameworks: " + f"{', '.join(unsupported)}." + ) + frameworks = ["mahout"] + total_vectors = args.batches * args.batch_size - vector_len = 1 << args.qubits + vector_len = _sample_dim(args.qubits, args.encoding_method) print(f"Generating {total_vectors} samples of {args.qubits} qubits...") print(f" Batch size : {args.batch_size}") diff --git a/qdp/qdp-python/benchmark/utils.py b/qdp/qdp-python/benchmark/utils.py index ef55ac1b6d..bc9b577dbc 100644 --- a/qdp/qdp-python/benchmark/utils.py +++ b/qdp/qdp-python/benchmark/utils.py @@ -39,8 +39,8 @@ def build_sample( Args: seed: Seed value used to generate deterministic data. - vector_len: Length of the vector (2^num_qubits for amplitude, num_qubits for angle). - encoding_method: "amplitude", "angle", or "basis". + vector_len: Input length for the selected encoding. + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". Returns: NumPy array containing the sample data. @@ -50,21 +50,22 @@ def build_sample( mask = np.uint64(vector_len - 1) idx = np.uint64(seed) & mask return np.array([idx], dtype=np.float64) - if encoding_method == "angle": - # Angle encoding: one angle per qubit, scaled to [0, 2*pi) + + if encoding_method in ("angle", "iqp", "iqp-z"): + # Angle/IQP-family encodings: deterministic phase parameters in [0, 2*pi) if vector_len == 0: return np.array([], dtype=np.float64) scale = (2.0 * np.pi) / vector_len idx = np.arange(vector_len, dtype=np.uint64) mixed = (idx + np.uint64(seed)) % np.uint64(vector_len) return mixed.astype(np.float64) * scale - else: - # Amplitude encoding: full vector - mask = np.uint64(vector_len - 1) - scale = 1.0 / vector_len - idx = np.arange(vector_len, dtype=np.uint64) - mixed = (idx + np.uint64(seed)) & mask - return mixed.astype(np.float64) * scale + + # Amplitude encoding: full vector + mask = np.uint64(vector_len - 1) + scale = 1.0 / vector_len + idx = np.arange(vector_len, dtype=np.uint64) + mixed = (idx + np.uint64(seed)) & mask + return mixed.astype(np.float64) * scale def generate_batch_data( @@ -78,8 +79,8 @@ def generate_batch_data( Args: n_samples: Number of samples to generate. - dim: Dimension of each sample (2^num_qubits for amplitude encoding). - encoding_method: "amplitude", "angle", or "basis". + dim: Input dimension for the selected encoding. + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". seed: Random seed for reproducibility. Returns: @@ -90,12 +91,13 @@ def generate_batch_data( if encoding_method == "basis": # Basis encoding: single index per sample return np.random.randint(0, dim, size=(n_samples, 1)).astype(np.float64) - if encoding_method == "angle": - # Angle encoding: per-qubit angles in [0, 2*pi) + + if encoding_method in ("angle", "iqp", "iqp-z"): + # Angle/IQP-family encodings: phase parameters in [0, 2*pi) return (np.random.rand(n_samples, dim) * (2.0 * np.pi)).astype(np.float64) - else: - # Amplitude encoding: full vectors - return np.random.rand(n_samples, dim).astype(np.float64) + + # Amplitude encoding: full vectors + return np.random.rand(n_samples, dim).astype(np.float64) def normalize_batch( @@ -106,13 +108,13 @@ def normalize_batch( Args: batch: NumPy array of shape (batch_size, vector_len). - encoding_method: "amplitude", "angle", or "basis". + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". Returns: - Normalized batch. For basis/angle encoding, returns the input unchanged. + Normalized batch. For basis/angle/IQP-family encodings, returns the input unchanged. """ - if encoding_method in ("basis", "angle"): - # Basis/angle encodings don't need normalization + if encoding_method in ("basis", "angle", "iqp", "iqp-z"): + # Basis/angle/IQP-family encodings don't need normalization return batch # Amplitude encoding: normalize vectors norms = np.linalg.norm(batch, axis=1, keepdims=True) @@ -128,13 +130,13 @@ def normalize_batch_torch( Args: batch: PyTorch tensor of shape (batch_size, vector_len). - encoding_method: "amplitude", "angle", or "basis". + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". Returns: - Normalized batch. For basis/angle encoding, returns the input unchanged. + Normalized batch. For basis/angle/IQP-family encodings, returns the input unchanged. """ - if encoding_method in ("basis", "angle"): - # Basis/angle encodings don't need normalization + if encoding_method in ("basis", "angle", "iqp", "iqp-z"): + # Basis/angle/IQP-family encodings don't need normalization return batch # Amplitude encoding: normalize vectors norms = torch.norm(batch, dim=1, keepdim=True) @@ -157,9 +159,9 @@ def prefetched_batches( Args: total_batches: Total number of batches to generate. batch_size: Number of samples per batch. - vector_len: Length of each vector (2^num_qubits for amplitude, num_qubits for angle). + vector_len: Input length for the selected encoding. prefetch: Number of batches to prefetch. - encoding_method: "amplitude", "angle", or "basis". + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". Yields: NumPy arrays of shape (batch_size, vector_len) or (batch_size, 1). @@ -200,9 +202,9 @@ def prefetched_batches_torch( Args: total_batches: Total number of batches to generate. batch_size: Number of samples per batch. - vector_len: Length of each vector (2^num_qubits for amplitude, num_qubits for angle). + vector_len: Input length for the selected encoding. prefetch: Number of batches to prefetch. - encoding_method: "amplitude", "angle", or "basis". + encoding_method: "amplitude", "angle", "basis", "iqp", or "iqp-z". Yields: PyTorch tensors of shape (batch_size, vector_len) or (batch_size, 1). diff --git a/qdp/qdp-python/qumat_qdp/api.py b/qdp/qdp-python/qumat_qdp/api.py index 8c90bae386..2ae4e45e6e 100644 --- a/qdp/qdp-python/qumat_qdp/api.py +++ b/qdp/qdp-python/qumat_qdp/api.py @@ -209,7 +209,7 @@ def _run_throughput_pytorch(self) -> ThroughputResult: if encoding_method == "basis": sample_dim = 1 - elif encoding_method == "angle": + elif encoding_method in {"angle", "iqp-z"}: sample_dim = num_qubits elif encoding_method == "iqp": sample_dim = num_qubits + num_qubits * (num_qubits - 1) // 2 @@ -225,6 +225,10 @@ def _run_throughput_pytorch(self) -> ThroughputResult: data = torch.randint( 0, 1 << num_qubits, (batch_size,), device=device ).to(torch.float64) + elif encoding_method in {"angle", "iqp", "iqp-z"}: + data = ( + torch.rand(batch_size, sample_dim, device=device) * (2.0 * torch.pi) + ).to(torch.float64) else: data = torch.randn( batch_size, sample_dim, dtype=torch.float64, device=device diff --git a/qdp/qdp-python/qumat_qdp/loader.py b/qdp/qdp-python/qumat_qdp/loader.py index 9a180baf00..7873f2bf67 100644 --- a/qdp/qdp-python/qumat_qdp/loader.py +++ b/qdp/qdp-python/qumat_qdp/loader.py @@ -85,8 +85,7 @@ def _validate_loader_args( def _build_sample(seed: int, vector_len: int, encoding_method: str) -> list[float]: """Build a single deterministic sample vector for the given encoding method. - Supports amplitude, angle, basis, and iqp (iqp uses the same mask-and-scale - logic as amplitude). + Supports amplitude, angle, basis, iqp, and iqp-z. """ import numpy as np @@ -94,14 +93,14 @@ def _build_sample(seed: int, vector_len: int, encoding_method: str) -> list[floa mask = np.uint64(vector_len - 1) idx = np.uint64(seed) & mask return [float(idx)] - if encoding_method == "angle": + if encoding_method in ("angle", "iqp", "iqp-z"): if vector_len == 0: return [] scale = (2.0 * math.pi) / vector_len idx = np.arange(vector_len, dtype=np.uint64) mixed = (idx + np.uint64(seed)) % np.uint64(vector_len) return (mixed.astype(np.float64) * scale).tolist() - # amplitude / iqp + # amplitude mask = np.uint64(vector_len - 1) scale = 1.0 / vector_len idx = np.arange(vector_len, dtype=np.uint64) @@ -109,6 +108,17 @@ def _build_sample(seed: int, vector_len: int, encoding_method: str) -> list[floa return (mixed.astype(np.float64) * scale).tolist() +def _sample_dim(num_qubits: int, encoding_method: str) -> int: + """Return the synthetic sample dimension for the selected encoding.""" + if encoding_method == "basis": + return 1 + if encoding_method in ("angle", "iqp-z"): + return num_qubits + if encoding_method == "iqp": + return num_qubits + num_qubits * (num_qubits - 1) // 2 + return 1 << num_qubits + + class QuantumDataLoader: """ Builder for a synthetic-data quantum encoding iterator. @@ -365,15 +375,7 @@ def _pytorch_synthetic_iter( encoding_method = self._encoding_method batch_size = self._batch_size seed = self._seed if self._seed is not None else 0 - - if encoding_method == "basis": - sample_size = 1 - elif encoding_method == "angle": - sample_size = num_qubits - elif encoding_method == "iqp": - sample_size = num_qubits + num_qubits * (num_qubits - 1) // 2 - else: - sample_size = 1 << num_qubits + sample_size = _sample_dim(num_qubits, encoding_method) for batch_idx in range(self._total_batches): base = batch_idx * batch_size diff --git a/qdp/qdp-python/qumat_qdp/torch_ref.py b/qdp/qdp-python/qumat_qdp/torch_ref.py index d023e11037..c00b7ce809 100644 --- a/qdp/qdp-python/qumat_qdp/torch_ref.py +++ b/qdp/qdp-python/qumat_qdp/torch_ref.py @@ -337,24 +337,30 @@ def encode( encoding_method: str = "amplitude", *, device: torch.device | str | None = None, - **kwargs: object, + enable_zz: bool = True, ) -> torch.Tensor: """Dispatch to the appropriate encoding function by method name. Args: data: Input tensor. num_qubits: Number of qubits. - encoding_method: One of ``"amplitude"``, ``"angle"``, ``"basis"``, ``"iqp"``. + encoding_method: One of ``"amplitude"``, ``"angle"``, ``"basis"``, ``"iqp"``, ``"iqp-z"``. device: Target device. - **kwargs: Extra arguments forwarded to the encoder (e.g. *enable_zz* for IQP). + enable_zz: Whether IQP encoding includes ZZ interaction terms. Ignored for + non-IQP encodings. ``"iqp-z"`` always forces this to ``False``. Returns: Complex tensor of shape ``(batch, 2**num_qubits)``. """ + if encoding_method == "iqp-z": + return iqp_encode(data, num_qubits, device=device, enable_zz=False) + if encoding_method == "iqp": + return iqp_encode(data, num_qubits, device=device, enable_zz=enable_zz) + fn = _ENCODERS.get(encoding_method) if fn is None: raise ValueError( f"Unknown encoding method {encoding_method!r}. " f"Supported: {', '.join(sorted(_ENCODERS))}" ) - return fn(data, num_qubits, device=device, **kwargs) + return fn(data, num_qubits, device=device) diff --git a/testing/conftest.py b/testing/conftest.py index 6a723cf7ad..88e49deeff 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -59,7 +59,11 @@ def pytest_collection_modifyitems(config, items): ) # Tests that work without _qdp (PyTorch reference backend tests). - _NO_QDP_OK = {"test_torch_ref.py", "test_fallback.py"} + _NO_QDP_OK = { + "test_torch_ref.py", + "test_fallback.py", + "test_benchmark_utils.py", + } for item in items: # Skip tests explicitly marked with @pytest.mark.gpu diff --git a/testing/qdp_python/test_benchmark_utils.py b/testing/qdp_python/test_benchmark_utils.py new file mode 100644 index 0000000000..0d282b7071 --- /dev/null +++ b/testing/qdp_python/test_benchmark_utils.py @@ -0,0 +1,87 @@ +# +# 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. + +from __future__ import annotations + +import importlib.util +from pathlib import Path + +import numpy as np +import pytest +import torch + +_QDP_PYTHON = Path(__file__).resolve().parents[2] / "qdp" / "qdp-python" +_UTILS_PATH = _QDP_PYTHON / "benchmark" / "utils.py" + +_UTILS_SPEC = importlib.util.spec_from_file_location("qdp_benchmark_utils", _UTILS_PATH) +assert _UTILS_SPEC is not None +assert _UTILS_SPEC.loader is not None +_benchmark_utils = importlib.util.module_from_spec(_UTILS_SPEC) +_UTILS_SPEC.loader.exec_module(_benchmark_utils) + +build_sample = _benchmark_utils.build_sample +generate_batch_data = _benchmark_utils.generate_batch_data +normalize_batch = _benchmark_utils.normalize_batch +normalize_batch_torch = _benchmark_utils.normalize_batch_torch + + +def test_build_sample_iqp_z_has_qubit_sized_parameter_vector(): + sample = build_sample(seed=5, vector_len=4, encoding_method="iqp-z") + + assert sample.shape == (4,) + assert np.all(sample >= 0.0) + assert np.all(sample < 2.0 * np.pi) + + +def test_build_sample_iqp_has_full_parameter_vector(): + sample = build_sample(seed=7, vector_len=10, encoding_method="iqp") + + assert sample.shape == (10,) + assert np.all(sample >= 0.0) + assert np.all(sample < 2.0 * np.pi) + + +@pytest.mark.parametrize("encoding_method", ["angle", "iqp", "iqp-z"]) +def test_build_sample_phase_encodings_handle_zero_length(encoding_method): + sample = build_sample(seed=3, vector_len=0, encoding_method=encoding_method) + + assert sample.shape == (0,) + + +def test_generate_batch_data_iqp_family_uses_phase_ranges(): + iqp = generate_batch_data(3, 6, encoding_method="iqp", seed=11) + iqp_z = generate_batch_data(3, 4, encoding_method="iqp-z", seed=11) + + assert iqp.shape == (3, 6) + assert iqp_z.shape == (3, 4) + assert np.all(iqp >= 0.0) + assert np.all(iqp < 2.0 * np.pi) + assert np.all(iqp_z >= 0.0) + assert np.all(iqp_z < 2.0 * np.pi) + + +def test_normalize_batch_leaves_iqp_family_unchanged(): + batch = np.array([[0.1, 0.2, 0.3], [1.1, 1.2, 1.3]], dtype=np.float64) + + assert np.array_equal(normalize_batch(batch, "iqp"), batch) + assert np.array_equal(normalize_batch(batch, "iqp-z"), batch) + + +def test_normalize_batch_torch_leaves_iqp_family_unchanged(): + batch = torch.tensor([[0.1, 0.2, 0.3], [1.1, 1.2, 1.3]], dtype=torch.float64) + + assert torch.equal(normalize_batch_torch(batch, "iqp"), batch) + assert torch.equal(normalize_batch_torch(batch, "iqp-z"), batch) diff --git a/testing/qdp_python/test_fallback.py b/testing/qdp_python/test_fallback.py index e6aac2d614..d07bf42f51 100644 --- a/testing/qdp_python/test_fallback.py +++ b/testing/qdp_python/test_fallback.py @@ -92,6 +92,17 @@ def test_get_torch(self): class TestLoaderPytorchBackend: + def test_loader_helpers_cover_iqp_family_edges(self): + from qumat_qdp.loader import _build_sample, _sample_dim + + assert _sample_dim(3, "basis") == 1 + assert _sample_dim(3, "angle") == 3 + assert _sample_dim(3, "iqp-z") == 3 + assert _sample_dim(3, "iqp") == 6 + assert _sample_dim(3, "amplitude") == 8 + assert _build_sample(4, 0, "iqp") == [] + assert _build_sample(4, 0, "iqp-z") == [] + def test_no_qdp_without_explicit_backend_raises(self, monkeypatch): """Without _qdp and without .backend('pytorch'), iteration raises.""" from qumat_qdp import loader as loader_mod @@ -209,6 +220,21 @@ def test_synthetic_pytorch_iqp(self): assert len(batches) == 2 assert batches[0].shape == (4, 8) + def test_synthetic_pytorch_iqp_z(self): + from qumat_qdp.loader import QuantumDataLoader + + loader = ( + QuantumDataLoader(device_id=0) + .backend("pytorch") + .qubits(3) + .encoding("iqp-z") + .batches(2, size=4) + .source_synthetic() + ) + batches = list(loader) + assert len(batches) == 2 + assert batches[0].shape == (4, 8) + def test_file_pt_pytorch(self, tmp_path): from qumat_qdp.loader import QuantumDataLoader @@ -322,3 +348,18 @@ def test_pytorch_latency(self): ) assert result.duration_sec > 0 assert result.latency_ms_per_vector > 0 + + @pytest.mark.parametrize("encoding_method", ["iqp", "iqp-z"]) + def test_pytorch_iqp_family(self, encoding_method): + from qumat_qdp.api import QdpBenchmark + + result = ( + QdpBenchmark() + .backend("pytorch") + .qubits(3) + .encoding(encoding_method) + .batches(3, size=2) + .run_throughput() + ) + assert result.duration_sec > 0 + assert result.vectors_per_sec > 0 diff --git a/testing/qdp_python/test_torch_ref.py b/testing/qdp_python/test_torch_ref.py index a7ccd25e6e..f9a29667c2 100644 --- a/testing/qdp_python/test_torch_ref.py +++ b/testing/qdp_python/test_torch_ref.py @@ -371,7 +371,7 @@ def test_encoding_matches_rust(self, encoding): import _qdp import numpy as np - engine = _qdp.QdpEngine(0) # type: ignore[unresolved-attribute] + engine = _qdp.QdpEngine(0) num_qubits = 3 state_dim = 1 << num_qubits