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)

Reply via email to