This is an automated email from the ASF dual-hosted git repository.
ryankert01 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git
The following commit(s) were added to refs/heads/main by this push:
new 33bee580e feat: add IQP / IQP-Z latency and throughput benchmark
(#1257)
33bee580e is described below
commit 33bee580ef9b3a0a994d795bfa3eb5a3ced50319
Author: Vic Wen <[email protected]>
AuthorDate: Thu May 7 18:17:51 2026 +0800
feat: add IQP / IQP-Z latency and throughput benchmark (#1257)
* feat: add iqp/iqp-z benchmark functionality
* test: add unit tests to cover the changes
* docs: update README.md and API.md
* docs: clarify benchmark input sizes
---
docs/qdp/python-api.md | 20 +++---
qdp/qdp-core/src/pipeline_runner.rs | 67 ++++++++++++++++-
qdp/qdp-python/benchmark/README.md | 10 ++-
qdp/qdp-python/benchmark/benchmark_latency.py | 27 ++++++-
qdp/qdp-python/benchmark/benchmark_pytorch_ref.py | 4 +-
qdp/qdp-python/benchmark/benchmark_throughput.py | 27 ++++++-
qdp/qdp-python/benchmark/utils.py | 62 ++++++++--------
qdp/qdp-python/qumat_qdp/api.py | 6 +-
qdp/qdp-python/qumat_qdp/loader.py | 28 ++++----
qdp/qdp-python/qumat_qdp/torch_ref.py | 18 +++--
testing/conftest.py | 6 +-
testing/qdp_python/test_benchmark_utils.py | 87 +++++++++++++++++++++++
testing/qdp_python/test_fallback.py | 41 +++++++++++
testing/qdp_python/test_torch_ref.py | 4 ++
14 files changed, 336 insertions(+), 71 deletions(-)
diff --git a/docs/qdp/python-api.md b/docs/qdp/python-api.md
index 60d88e3a4..a9d8b2ec3 100644
--- a/docs/qdp/python-api.md
+++ b/docs/qdp/python-api.md
@@ -163,11 +163,11 @@ tensor = torch.from_dlpack(qt)
| Method | Description |
|--------|-------------|
-| `qubits(n)` | Set number of qubits. |
-| `encoding(method)` | Set encoding method. |
-| `batches(total, size=64)` | Set total batches and batch size. |
-| `prefetch(n)` | No-op for API compatibility. |
-| `warmup(n)` | Set warmup batches. |
+| `qubits(n)` | Number of qubits. |
+| `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. |
| `backend(name)` | Select `"rust"` or `"pytorch"`. |
Backend notes:
@@ -229,12 +229,12 @@ Result types:
| Method | Description |
|--------|-------------|
-| `qubits(n)` | Set number of qubits. |
-| `encoding(method)` | Set encoding method. |
-| `batches(total, size=64)` | Set total batches and batch size. |
-| `source_synthetic(total_batches=None)` | Use synthetic data. |
+| `qubits(n)` | Number of qubits. |
+| `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. |
| `source_file(path, streaming=False)` | Use a file-backed source. |
-| `seed(s)` | Set reproducible synthetic-data seed. |
+| `seed(s)` | RNG seed for reproducibility. |
| `null_handling(policy)` | Set `"fill_zero"` or `"reject"`. |
| `backend(name)` | Select `"rust"` or `"pytorch"`. |
diff --git a/qdp/qdp-core/src/pipeline_runner.rs
b/qdp/qdp-core/src/pipeline_runner.rs
index 42bb5cc65..fc19dd6a3 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 4e01d9835..348441fdc 100644
--- a/qdp/qdp-python/benchmark/README.md
+++ b/qdp/qdp-python/benchmark/README.md
@@ -87,10 +87,12 @@ 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`).
+ - `--qubits`: controls the input length together with `--encoding-method`.
+ Amplitude uses `2^qubits`, angle and `iqp-z` use `qubits`, basis uses one
basis index,
+ and `iqp` uses `qubits + qubits*(qubits-1)/2`.
- `--batches`: number of host-side batches to stream.
- `--batch-size`: vectors per batch; raises total samples (`batches *
batch-size`).
- `--prefetch`: CPU queue depth; higher values help keep the pipeline fed.
@@ -115,7 +117,9 @@ 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`.
+- For synthetic inputs, amplitude uses `2^qubits`, angle and `iqp-z` use
`qubits`,
+ basis uses one basis index, and `iqp` uses `qubits + qubits*(qubits-1)/2`.
- 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 5b53c56c8..4194d99c3 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 ad9507908..de8d44a5e 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 7617d4995..0b24485ce 100644
--- a/qdp/qdp-python/benchmark/benchmark_throughput.py
+++ b/qdp/qdp-python/benchmark/benchmark_throughput.py
@@ -170,6 +170,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.")
@@ -465,8 +475,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).",
)
parser.add_argument(
"--rocm-lib-dir",
@@ -494,8 +504,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 ef55ac1b6..bc9b577db 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 8c90bae38..2ae4e45e6 100644
--- a/qdp/qdp-python/qumat_qdp/api.py
+++ b/qdp/qdp-python/qumat_qdp/api.py
@@ -209,7 +209,7 @@ class QdpBenchmark:
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 @@ class QdpBenchmark:
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 9a180baf0..7873f2bf6 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 @@ class QuantumDataLoader:
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 d023e1103..b2fcaca56 100644
--- a/qdp/qdp-python/qumat_qdp/torch_ref.py
+++ b/qdp/qdp-python/qumat_qdp/torch_ref.py
@@ -330,6 +330,8 @@ _ENCODERS = {
"iqp": iqp_encode,
}
+_SUPPORTED_ENCODINGS = tuple(sorted((*_ENCODERS.keys(), "iqp-z")))
+
def encode(
data: torch.Tensor,
@@ -337,24 +339,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))}"
+ f"Supported: {', '.join(_SUPPORTED_ENCODINGS)}"
)
- 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 6a723cf7a..88e49deef 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 000000000..0d282b707
--- /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)
+
+
[email protected]("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 e6aac2d61..d07bf42f5 100644
--- a/testing/qdp_python/test_fallback.py
+++ b/testing/qdp_python/test_fallback.py
@@ -92,6 +92,17 @@ class TestBackendDetection:
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 @@ class TestLoaderPytorchBackend:
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 @@ class TestBenchmarkFallback:
)
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 1f28d9cfc..c6c49883b 100644
--- a/testing/qdp_python/test_torch_ref.py
+++ b/testing/qdp_python/test_torch_ref.py
@@ -284,6 +284,10 @@ class TestDispatcher:
with pytest.raises(ValueError, match="Unknown encoding method"):
encode(torch.randn(1, 4), num_qubits=2, encoding_method="invalid")
+ def test_unknown_lists_iqp_z_as_supported(self):
+ with pytest.raises(ValueError, match=r"Supported: .*iqp-z"):
+ encode(torch.randn(1, 4), num_qubits=2, encoding_method="invalid")
+
# ---------------------------------------------------------------------------
# Device placement (CPU always; GPU if available)