This is an automated email from the ASF dual-hosted git repository.
hcr 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 026e6c762 MAHOUT-799 [QDP] Add IQP encoding (#868)
026e6c762 is described below
commit 026e6c7628624b5c65972ba1d6d95e1475cdcb6e
Author: Ryan Huang <[email protected]>
AuthorDate: Fri Jan 23 16:59:11 2026 +0800
MAHOUT-799 [QDP] Add IQP encoding (#868)
* Add IQP encoding support with CUDA kernels and Python bindings
* [QDP] test: add unit tests for IQP encoding functionality
* solve build issue
* refactor: Use configurable block size and grid size limits in IQP
encoding kernels
---
qdp/qdp-core/src/gpu/encodings/iqp.rs | 290 +++++++++++++++
qdp/qdp-core/src/gpu/encodings/mod.rs | 8 +-
qdp/qdp-core/tests/iqp_encoding.rs | 660 ++++++++++++++++++++++++++++++++++
qdp/qdp-kernels/build.rs | 2 +
qdp/qdp-kernels/src/iqp.cu | 232 ++++++++++++
qdp/qdp-kernels/src/lib.rs | 58 +++
testing/qdp/test_bindings.py | 260 ++++++++++++++
7 files changed, 1508 insertions(+), 2 deletions(-)
diff --git a/qdp/qdp-core/src/gpu/encodings/iqp.rs
b/qdp/qdp-core/src/gpu/encodings/iqp.rs
new file mode 100644
index 000000000..89d4a8f56
--- /dev/null
+++ b/qdp/qdp-core/src/gpu/encodings/iqp.rs
@@ -0,0 +1,290 @@
+//
+// 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.
+
+// IQP (Instantaneous Quantum Polynomial) encoding: entangled quantum states
via diagonal phases.
+
+use super::QuantumEncoder;
+#[cfg(target_os = "linux")]
+use crate::error::cuda_error_to_string;
+use crate::error::{MahoutError, Result};
+use crate::gpu::memory::GpuStateVector;
+use cudarc::driver::CudaDevice;
+use std::sync::Arc;
+
+#[cfg(target_os = "linux")]
+use crate::gpu::memory::map_allocation_error;
+#[cfg(target_os = "linux")]
+use cudarc::driver::DevicePtr;
+#[cfg(target_os = "linux")]
+use std::ffi::c_void;
+
+/// IQP encoding: creates entangled quantum states using diagonal phase gates.
+///
+/// Two variants are supported:
+/// - `enable_zz = false`: Single-qubit Z rotations only (n parameters)
+/// - `enable_zz = true`: Full ZZ interactions (n + n*(n-1)/2 parameters)
+pub struct IqpEncoder {
+ enable_zz: bool,
+}
+
+impl IqpEncoder {
+ /// Create an IQP encoder with full ZZ interactions.
+ #[must_use]
+ pub fn full() -> Self {
+ Self { enable_zz: true }
+ }
+
+ /// Create an IQP encoder with single-qubit Z rotations only.
+ #[must_use]
+ pub fn z_only() -> Self {
+ Self { enable_zz: false }
+ }
+
+ /// Calculate the expected data length for this encoding variant.
+ fn expected_data_len(&self, num_qubits: usize) -> usize {
+ if self.enable_zz {
+ // n single-qubit + n*(n-1)/2 two-qubit terms
+ num_qubits + num_qubits * (num_qubits - 1) / 2
+ } else {
+ num_qubits
+ }
+ }
+}
+
+impl QuantumEncoder for IqpEncoder {
+ fn encode(
+ &self,
+ #[cfg(target_os = "linux")] device: &Arc<CudaDevice>,
+ #[cfg(not(target_os = "linux"))] _device: &Arc<CudaDevice>,
+ data: &[f64],
+ num_qubits: usize,
+ ) -> Result<GpuStateVector> {
+ self.validate_input(data, num_qubits)?;
+ let state_len = 1 << num_qubits;
+
+ #[cfg(target_os = "linux")]
+ {
+ let input_bytes = std::mem::size_of_val(data);
+ let data_gpu = {
+ crate::profile_scope!("GPU::H2D_IqpData");
+ device.htod_sync_copy(data).map_err(|e| {
+ map_allocation_error(input_bytes, "IQP input upload",
Some(num_qubits), e)
+ })?
+ };
+
+ let state_vector = {
+ crate::profile_scope!("GPU::Alloc");
+ GpuStateVector::new(device, num_qubits)?
+ };
+
+ let state_ptr = state_vector.ptr_f64().ok_or_else(|| {
+ MahoutError::InvalidInput(
+ "State vector precision mismatch (expected float64
buffer)".to_string(),
+ )
+ })?;
+
+ let ret = {
+ crate::profile_scope!("GPU::KernelLaunch");
+ unsafe {
+ qdp_kernels::launch_iqp_encode(
+ *data_gpu.device_ptr() as *const f64,
+ state_ptr as *mut c_void,
+ state_len,
+ num_qubits as u32,
+ if self.enable_zz { 1 } else { 0 },
+ std::ptr::null_mut(),
+ )
+ }
+ };
+
+ if ret != 0 {
+ return Err(MahoutError::KernelLaunch(format!(
+ "IQP encoding kernel failed with CUDA error code: {} ({})",
+ ret,
+ cuda_error_to_string(ret)
+ )));
+ }
+
+ {
+ crate::profile_scope!("GPU::Synchronize");
+ device.synchronize().map_err(|e| {
+ MahoutError::Cuda(format!("CUDA device synchronize failed:
{:?}", e))
+ })?;
+ }
+
+ Ok(state_vector)
+ }
+
+ #[cfg(not(target_os = "linux"))]
+ {
+ Err(MahoutError::Cuda(
+ "CUDA unavailable (non-Linux stub)".to_string(),
+ ))
+ }
+ }
+
+ /// Encode multiple IQP samples in a single GPU allocation and kernel
launch
+ #[cfg(target_os = "linux")]
+ fn encode_batch(
+ &self,
+ device: &Arc<CudaDevice>,
+ batch_data: &[f64],
+ num_samples: usize,
+ sample_size: usize,
+ num_qubits: usize,
+ ) -> Result<GpuStateVector> {
+ crate::profile_scope!("IqpEncoder::encode_batch");
+
+ let expected_len = self.expected_data_len(num_qubits);
+ if sample_size != expected_len {
+ return Err(MahoutError::InvalidInput(format!(
+ "IQP{} encoding expects sample_size={} for {} qubits, got {}",
+ if self.enable_zz { "" } else { "-Z" },
+ expected_len,
+ num_qubits,
+ sample_size
+ )));
+ }
+
+ if batch_data.len() != num_samples * sample_size {
+ return Err(MahoutError::InvalidInput(format!(
+ "Batch data length {} doesn't match num_samples {} *
sample_size {}",
+ batch_data.len(),
+ num_samples,
+ sample_size
+ )));
+ }
+
+ if num_qubits == 0 || num_qubits > 30 {
+ return Err(MahoutError::InvalidInput(format!(
+ "Number of qubits {} must be between 1 and 30",
+ num_qubits
+ )));
+ }
+
+ for (i, &val) in batch_data.iter().enumerate() {
+ if !val.is_finite() {
+ let sample_idx = i / sample_size;
+ let param_idx = i % sample_size;
+ return Err(MahoutError::InvalidInput(format!(
+ "Sample {} parameter {} must be finite, got {}",
+ sample_idx, param_idx, val
+ )));
+ }
+ }
+
+ let state_len = 1 << num_qubits;
+
+ let batch_state_vector = {
+ crate::profile_scope!("GPU::AllocBatch");
+ GpuStateVector::new_batch(device, num_samples, num_qubits)?
+ };
+
+ let input_bytes = std::mem::size_of_val(batch_data);
+ let data_gpu = {
+ crate::profile_scope!("GPU::H2D_BatchIqpData");
+ device.htod_sync_copy(batch_data).map_err(|e| {
+ map_allocation_error(input_bytes, "IQP batch upload",
Some(num_qubits), e)
+ })?
+ };
+
+ let state_ptr = batch_state_vector.ptr_f64().ok_or_else(|| {
+ MahoutError::InvalidInput(
+ "Batch state vector precision mismatch (expected float64
buffer)".to_string(),
+ )
+ })?;
+
+ {
+ crate::profile_scope!("GPU::BatchKernelLaunch");
+ let ret = unsafe {
+ qdp_kernels::launch_iqp_encode_batch(
+ *data_gpu.device_ptr() as *const f64,
+ state_ptr as *mut c_void,
+ num_samples,
+ state_len,
+ num_qubits as u32,
+ sample_size as u32,
+ if self.enable_zz { 1 } else { 0 },
+ std::ptr::null_mut(),
+ )
+ };
+
+ if ret != 0 {
+ return Err(MahoutError::KernelLaunch(format!(
+ "Batch IQP encoding kernel failed: {} ({})",
+ ret,
+ cuda_error_to_string(ret)
+ )));
+ }
+ }
+
+ {
+ crate::profile_scope!("GPU::Synchronize");
+ device
+ .synchronize()
+ .map_err(|e| MahoutError::Cuda(format!("Sync failed: {:?}",
e)))?;
+ }
+
+ Ok(batch_state_vector)
+ }
+
+ fn validate_input(&self, data: &[f64], num_qubits: usize) -> Result<()> {
+ if num_qubits == 0 {
+ return Err(MahoutError::InvalidInput(
+ "Number of qubits must be at least 1".to_string(),
+ ));
+ }
+ if num_qubits > 30 {
+ return Err(MahoutError::InvalidInput(format!(
+ "Number of qubits {} exceeds practical limit of 30",
+ num_qubits
+ )));
+ }
+
+ let expected_len = self.expected_data_len(num_qubits);
+ if data.len() != expected_len {
+ return Err(MahoutError::InvalidInput(format!(
+ "IQP{} encoding expects {} values for {} qubits, got {}",
+ if self.enable_zz { "" } else { "-Z" },
+ expected_len,
+ num_qubits,
+ data.len()
+ )));
+ }
+
+ for (i, &val) in data.iter().enumerate() {
+ if !val.is_finite() {
+ return Err(MahoutError::InvalidInput(format!(
+ "Parameter at index {} must be finite, got {}",
+ i, val
+ )));
+ }
+ }
+ Ok(())
+ }
+
+ fn name(&self) -> &'static str {
+ if self.enable_zz { "iqp" } else { "iqp-z" }
+ }
+
+ fn description(&self) -> &'static str {
+ if self.enable_zz {
+ "IQP encoding: entangled states with Z and ZZ interactions"
+ } else {
+ "IQP-Z encoding: product states with single-qubit Z rotations"
+ }
+ }
+}
diff --git a/qdp/qdp-core/src/gpu/encodings/mod.rs
b/qdp/qdp-core/src/gpu/encodings/mod.rs
index 63c6addca..ad1a9577a 100644
--- a/qdp/qdp-core/src/gpu/encodings/mod.rs
+++ b/qdp/qdp-core/src/gpu/encodings/mod.rs
@@ -96,19 +96,23 @@ pub trait QuantumEncoder: Send + Sync {
pub mod amplitude;
pub mod angle;
pub mod basis;
+pub mod iqp;
pub use amplitude::AmplitudeEncoder;
pub use angle::AngleEncoder;
pub use basis::BasisEncoder;
+pub use iqp::IqpEncoder;
-/// Create encoder by name: "amplitude", "angle", or "basis"
+/// Create encoder by name: "amplitude", "angle", "basis", "iqp", or "iqp-z"
pub fn get_encoder(name: &str) -> Result<Box<dyn QuantumEncoder>> {
match name.to_lowercase().as_str() {
"amplitude" => Ok(Box::new(AmplitudeEncoder)),
"angle" => Ok(Box::new(AngleEncoder)),
"basis" => Ok(Box::new(BasisEncoder)),
+ "iqp" => Ok(Box::new(IqpEncoder::full())),
+ "iqp-z" => Ok(Box::new(IqpEncoder::z_only())),
_ => Err(crate::error::MahoutError::InvalidInput(format!(
- "Unknown encoder: {}. Available: amplitude, angle, basis",
+ "Unknown encoder: {}. Available: amplitude, angle, basis, iqp,
iqp-z",
name
))),
}
diff --git a/qdp/qdp-core/tests/iqp_encoding.rs
b/qdp/qdp-core/tests/iqp_encoding.rs
new file mode 100644
index 000000000..4fc48bcc5
--- /dev/null
+++ b/qdp/qdp-core/tests/iqp_encoding.rs
@@ -0,0 +1,660 @@
+//
+// 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.
+
+// Unit tests for IQP (Instantaneous Quantum Polynomial) encoding
+
+use qdp_core::{MahoutError, QdpEngine};
+
+mod common;
+
+/// Helper to calculate expected data length for IQP full encoding (n +
n*(n-1)/2)
+fn iqp_full_data_len(num_qubits: usize) -> usize {
+ num_qubits + num_qubits * (num_qubits - 1) / 2
+}
+
+/// Helper to calculate expected data length for IQP-Z encoding (n only)
+fn iqp_z_data_len(num_qubits: usize) -> usize {
+ num_qubits
+}
+
+//
=============================================================================
+// Input Validation Tests
+//
=============================================================================
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_zero_qubits_rejected() {
+ println!("Testing IQP zero qubits rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let data = vec![0.5; 1];
+ let result = engine.encode(&data, 0, "iqp");
+ assert!(result.is_err(), "Should reject zero qubits");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("at least 1"),
+ "Error should mention minimum qubit requirement"
+ );
+ println!("PASS: Correctly rejected zero qubits: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for zero qubits"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_max_qubits_exceeded() {
+ println!("Testing IQP max qubits (>30) rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let data = vec![0.5; iqp_full_data_len(31)];
+ let result = engine.encode(&data, 31, "iqp");
+ assert!(result.is_err(), "Should reject qubits > 30");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(msg.contains("30"), "Error should mention 30 qubit limit");
+ println!("PASS: Correctly rejected excessive qubits: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for max qubits"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_wrong_data_length() {
+ println!("Testing IQP wrong data length rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 4;
+ let expected_len = iqp_full_data_len(num_qubits); // 4 + 6 = 10
+
+ // Provide wrong length (too few)
+ let data = vec![0.5; expected_len - 1];
+ let result = engine.encode(&data, num_qubits, "iqp");
+ assert!(result.is_err(), "Should reject wrong data length");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("expects") &&
msg.contains(&expected_len.to_string()),
+ "Error should mention expected length"
+ );
+ println!("PASS: Correctly rejected wrong data length: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for wrong data length"),
+ }
+
+ // Provide wrong length (too many)
+ let data = vec![0.5; expected_len + 1];
+ let result = engine.encode(&data, num_qubits, "iqp");
+ assert!(
+ result.is_err(),
+ "Should reject wrong data length (too many)"
+ );
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_z_wrong_data_length() {
+ println!("Testing IQP-Z wrong data length rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 4;
+ let expected_len = iqp_z_data_len(num_qubits); // 4
+
+ // Provide wrong length
+ let data = vec![0.5; expected_len + 2];
+ let result = engine.encode(&data, num_qubits, "iqp-z");
+ assert!(result.is_err(), "Should reject wrong data length for IQP-Z");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("IQP-Z") &&
msg.contains(&expected_len.to_string()),
+ "Error should mention IQP-Z and expected length"
+ );
+ println!("PASS: Correctly rejected wrong IQP-Z data length: {}",
msg);
+ }
+ _ => panic!("Expected InvalidInput error for wrong IQP-Z data length"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_nan_value_rejected() {
+ println!("Testing IQP NaN value rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 3;
+ let mut data = vec![0.5; iqp_full_data_len(num_qubits)];
+ data[2] = f64::NAN;
+
+ let result = engine.encode(&data, num_qubits, "iqp");
+ assert!(result.is_err(), "Should reject NaN values");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("finite"),
+ "Error should mention finite requirement"
+ );
+ println!("PASS: Correctly rejected NaN value: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for NaN value"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_infinity_value_rejected() {
+ println!("Testing IQP infinity value rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 3;
+ let mut data = vec![0.5; iqp_full_data_len(num_qubits)];
+ data[1] = f64::INFINITY;
+
+ let result = engine.encode(&data, num_qubits, "iqp");
+ assert!(result.is_err(), "Should reject infinity values");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("finite"),
+ "Error should mention finite requirement"
+ );
+ println!("PASS: Correctly rejected infinity value: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for infinity value"),
+ }
+}
+
+//
=============================================================================
+// Single Encode Workflow Tests
+//
=============================================================================
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_full_encoding_workflow() {
+ println!("Testing IQP full encoding workflow...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ let num_qubits = 4;
+ let data: Vec<f64> = (0..iqp_full_data_len(num_qubits))
+ .map(|i| (i as f64) * 0.1)
+ .collect();
+
+ let result = engine.encode(&data, num_qubits, "iqp");
+ let dlpack_ptr = result.expect("IQP encoding should succeed");
+ assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null");
+ println!("PASS: IQP full encoding succeeded");
+
+ unsafe {
+ let managed = &*dlpack_ptr;
+ let tensor = &managed.dl_tensor;
+
+ // Verify 2D shape: [1, 2^num_qubits]
+ assert_eq!(tensor.ndim, 2, "IQP tensor should be 2D");
+
+ let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim
as usize);
+ assert_eq!(shape_slice[0], 1, "First dimension should be 1");
+ assert_eq!(
+ shape_slice[1],
+ (1 << num_qubits) as i64,
+ "Second dimension should be 2^num_qubits"
+ );
+
+ println!(
+ "PASS: IQP tensor shape correct: [{}, {}]",
+ shape_slice[0], shape_slice[1]
+ );
+
+ if let Some(deleter) = managed.deleter {
+ deleter(dlpack_ptr);
+ }
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_z_encoding_workflow() {
+ println!("Testing IQP-Z encoding workflow...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ let num_qubits = 5;
+ let data: Vec<f64> = (0..iqp_z_data_len(num_qubits))
+ .map(|i| (i as f64) * 0.2)
+ .collect();
+
+ let result = engine.encode(&data, num_qubits, "iqp-z");
+ let dlpack_ptr = result.expect("IQP-Z encoding should succeed");
+ assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null");
+ println!("PASS: IQP-Z encoding succeeded");
+
+ unsafe {
+ let managed = &*dlpack_ptr;
+ let tensor = &managed.dl_tensor;
+
+ assert_eq!(tensor.ndim, 2, "IQP-Z tensor should be 2D");
+
+ let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim
as usize);
+ assert_eq!(shape_slice[0], 1, "First dimension should be 1");
+ assert_eq!(
+ shape_slice[1],
+ (1 << num_qubits) as i64,
+ "Second dimension should be 2^num_qubits"
+ );
+
+ println!(
+ "PASS: IQP-Z tensor shape correct: [{}, {}]",
+ shape_slice[0], shape_slice[1]
+ );
+
+ if let Some(deleter) = managed.deleter {
+ deleter(dlpack_ptr);
+ }
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_single_qubit() {
+ println!("Testing IQP single qubit encoding...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ // Single qubit IQP full: 1 parameter (no ZZ terms with only 1 qubit)
+ let num_qubits = 1;
+ let data = vec![std::f64::consts::PI / 4.0]; // 1 param for n=1
+
+ let result = engine.encode(&data, num_qubits, "iqp");
+ let dlpack_ptr = result.expect("Single qubit IQP encoding should succeed");
+ assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null");
+
+ unsafe {
+ let managed = &*dlpack_ptr;
+ let tensor = &managed.dl_tensor;
+
+ let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim
as usize);
+ assert_eq!(
+ shape_slice[1], 2,
+ "Single qubit should have 2 state amplitudes"
+ );
+
+ println!("PASS: Single qubit IQP encoding succeeded with shape [1,
2]");
+
+ if let Some(deleter) = managed.deleter {
+ deleter(dlpack_ptr);
+ }
+ }
+}
+
+//
=============================================================================
+// Batch Encoding Tests
+//
=============================================================================
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_batch_encoding() {
+ println!("Testing IQP batch encoding...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ let num_qubits = 3;
+ let num_samples = 4;
+ let sample_size = iqp_full_data_len(num_qubits); // 3 + 3 = 6
+
+ let batch_data: Vec<f64> = (0..num_samples * sample_size)
+ .map(|i| (i as f64) * 0.05)
+ .collect();
+
+ let result = engine.encode_batch(&batch_data, num_samples, sample_size,
num_qubits, "iqp");
+ let dlpack_ptr = result.expect("IQP batch encoding should succeed");
+ assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null");
+
+ unsafe {
+ let managed = &*dlpack_ptr;
+ let tensor = &managed.dl_tensor;
+
+ assert_eq!(tensor.ndim, 2, "Batch tensor should be 2D");
+
+ let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim
as usize);
+ assert_eq!(
+ shape_slice[0], num_samples as i64,
+ "First dimension should be num_samples"
+ );
+ assert_eq!(
+ shape_slice[1],
+ (1 << num_qubits) as i64,
+ "Second dimension should be 2^num_qubits"
+ );
+
+ println!(
+ "PASS: IQP batch encoding shape correct: [{}, {}]",
+ shape_slice[0], shape_slice[1]
+ );
+
+ if let Some(deleter) = managed.deleter {
+ deleter(dlpack_ptr);
+ }
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_z_batch_encoding() {
+ println!("Testing IQP-Z batch encoding...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ let num_qubits = 4;
+ let num_samples = 5;
+ let sample_size = iqp_z_data_len(num_qubits); // 4
+
+ let batch_data: Vec<f64> = (0..num_samples * sample_size)
+ .map(|i| (i as f64) * 0.1)
+ .collect();
+
+ let result = engine.encode_batch(&batch_data, num_samples, sample_size,
num_qubits, "iqp-z");
+ let dlpack_ptr = result.expect("IQP-Z batch encoding should succeed");
+ assert!(!dlpack_ptr.is_null(), "DLPack pointer should not be null");
+
+ unsafe {
+ let managed = &*dlpack_ptr;
+ let tensor = &managed.dl_tensor;
+
+ assert_eq!(tensor.ndim, 2, "Batch tensor should be 2D");
+
+ let shape_slice = std::slice::from_raw_parts(tensor.shape, tensor.ndim
as usize);
+ assert_eq!(
+ shape_slice[0], num_samples as i64,
+ "First dimension should be num_samples"
+ );
+ assert_eq!(
+ shape_slice[1],
+ (1 << num_qubits) as i64,
+ "Second dimension should be 2^num_qubits"
+ );
+
+ println!(
+ "PASS: IQP-Z batch encoding shape correct: [{}, {}]",
+ shape_slice[0], shape_slice[1]
+ );
+
+ if let Some(deleter) = managed.deleter {
+ deleter(dlpack_ptr);
+ }
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_batch_wrong_sample_size() {
+ println!("Testing IQP batch wrong sample_size rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 3;
+ let num_samples = 2;
+ let wrong_sample_size = iqp_full_data_len(num_qubits) + 1; // Wrong!
+
+ let batch_data: Vec<f64> = vec![0.5; num_samples * wrong_sample_size];
+
+ let result = engine.encode_batch(
+ &batch_data,
+ num_samples,
+ wrong_sample_size,
+ num_qubits,
+ "iqp",
+ );
+ assert!(result.is_err(), "Should reject wrong sample_size");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("sample_size"),
+ "Error should mention sample_size"
+ );
+ println!("PASS: Correctly rejected wrong sample_size: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for wrong sample_size"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_batch_data_length_mismatch() {
+ println!("Testing IQP batch data length mismatch rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 3;
+ let num_samples = 3;
+ let sample_size = iqp_full_data_len(num_qubits);
+
+ // Provide fewer elements than expected
+ let batch_data: Vec<f64> = vec![0.5; num_samples * sample_size - 1];
+
+ let result = engine.encode_batch(&batch_data, num_samples, sample_size,
num_qubits, "iqp");
+ assert!(result.is_err(), "Should reject data length mismatch");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("length") || msg.contains("match"),
+ "Error should mention length mismatch"
+ );
+ println!("PASS: Correctly rejected data length mismatch: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for data length mismatch"),
+ }
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_batch_nan_in_sample() {
+ println!("Testing IQP batch NaN value rejection...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => return,
+ };
+
+ let num_qubits = 3;
+ let num_samples = 2;
+ let sample_size = iqp_full_data_len(num_qubits);
+
+ let mut batch_data: Vec<f64> = vec![0.5; num_samples * sample_size];
+ batch_data[sample_size + 2] = f64::NAN; // NaN in second sample
+
+ let result = engine.encode_batch(&batch_data, num_samples, sample_size,
num_qubits, "iqp");
+ assert!(result.is_err(), "Should reject NaN in batch data");
+
+ match result {
+ Err(MahoutError::InvalidInput(msg)) => {
+ assert!(
+ msg.contains("finite") || msg.contains("Sample"),
+ "Error should mention finite requirement or sample index"
+ );
+ println!("PASS: Correctly rejected NaN in batch: {}", msg);
+ }
+ _ => panic!("Expected InvalidInput error for NaN in batch"),
+ }
+}
+
+//
=============================================================================
+// Expected Data Length Calculation Tests
+//
=============================================================================
+
+#[test]
+fn test_iqp_data_length_calculations() {
+ println!("Testing IQP data length calculations...");
+
+ // IQP full: n + n*(n-1)/2
+ assert_eq!(iqp_full_data_len(1), 1); // 1 + 0 = 1
+ assert_eq!(iqp_full_data_len(2), 3); // 2 + 1 = 3
+ assert_eq!(iqp_full_data_len(3), 6); // 3 + 3 = 6
+ assert_eq!(iqp_full_data_len(4), 10); // 4 + 6 = 10
+ assert_eq!(iqp_full_data_len(5), 15); // 5 + 10 = 15
+
+ // IQP-Z: n only
+ assert_eq!(iqp_z_data_len(1), 1);
+ assert_eq!(iqp_z_data_len(2), 2);
+ assert_eq!(iqp_z_data_len(3), 3);
+ assert_eq!(iqp_z_data_len(4), 4);
+ assert_eq!(iqp_z_data_len(5), 5);
+
+ println!("PASS: Data length calculations are correct");
+}
+
+//
=============================================================================
+// Encoder Factory Tests
+//
=============================================================================
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_encoder_via_factory() {
+ println!("Testing IQP encoder creation via get_encoder...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ // Test that "iqp" and "IQP" work (case insensitive)
+ let num_qubits = 2;
+ let data: Vec<f64> = vec![0.1, 0.2, 0.3]; // 2 + 1 = 3 params
+
+ let result1 = engine.encode(&data, num_qubits, "iqp");
+ assert!(result1.is_ok(), "lowercase 'iqp' should work");
+
+ let result2 = engine.encode(&data, num_qubits, "IQP");
+ assert!(result2.is_ok(), "uppercase 'IQP' should work");
+
+ // Clean up
+ unsafe {
+ if let Ok(ptr) = result1
+ && let Some(d) = (*ptr).deleter
+ {
+ d(ptr);
+ }
+ if let Ok(ptr) = result2
+ && let Some(d) = (*ptr).deleter
+ {
+ d(ptr);
+ }
+ }
+
+ println!("PASS: IQP encoder factory works with case insensitivity");
+}
+
+#[test]
+#[cfg(target_os = "linux")]
+fn test_iqp_z_encoder_via_factory() {
+ println!("Testing IQP-Z encoder creation via get_encoder...");
+
+ let engine = match QdpEngine::new(0) {
+ Ok(e) => e,
+ Err(_) => {
+ println!("SKIP: No GPU available");
+ return;
+ }
+ };
+
+ let num_qubits = 3;
+ let data: Vec<f64> = vec![0.1, 0.2, 0.3]; // 3 params for IQP-Z
+
+ let result = engine.encode(&data, num_qubits, "iqp-z");
+ assert!(result.is_ok(), "'iqp-z' should work");
+
+ unsafe {
+ if let Ok(ptr) = result
+ && let Some(d) = (*ptr).deleter
+ {
+ d(ptr);
+ }
+ }
+
+ println!("PASS: IQP-Z encoder factory works");
+}
diff --git a/qdp/qdp-kernels/build.rs b/qdp/qdp-kernels/build.rs
index 093fbf71e..765ed4b68 100644
--- a/qdp/qdp-kernels/build.rs
+++ b/qdp/qdp-kernels/build.rs
@@ -34,6 +34,7 @@ fn main() {
println!("cargo:rerun-if-changed=src/amplitude.cu");
println!("cargo:rerun-if-changed=src/basis.cu");
println!("cargo:rerun-if-changed=src/angle.cu");
+ println!("cargo:rerun-if-changed=src/iqp.cu");
println!("cargo:rerun-if-env-changed=QDP_NO_CUDA");
println!("cargo:rerun-if-changed=src/kernel_config.h");
@@ -98,5 +99,6 @@ fn main() {
.file("src/amplitude.cu")
.file("src/basis.cu")
.file("src/angle.cu")
+ .file("src/iqp.cu")
.compile("kernels");
}
diff --git a/qdp/qdp-kernels/src/iqp.cu b/qdp/qdp-kernels/src/iqp.cu
new file mode 100644
index 000000000..f0e63db50
--- /dev/null
+++ b/qdp/qdp-kernels/src/iqp.cu
@@ -0,0 +1,232 @@
+//
+// 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.
+
+// IQP (Instantaneous Quantum Polynomial) Encoding CUDA Kernels
+//
+// Creates entangled quantum states via diagonal phase gates:
+// |psi> = H^n * U_phase(data) * H^n |0>^n
+//
+// The amplitude for basis state |z> is:
+// amplitude[z] = (1/2^n) * sum_x exp(i*theta(x)) * (-1)^popcount(x AND z)
+//
+// Two variants:
+// - enable_zz=0: theta(x) = sum_i x_i * data_i (n parameters)
+// - enable_zz=1: theta(x) = sum_i x_i * data_i + sum_{i<j} x_i * x_j * data_ij
+// (n + n*(n-1)/2 parameters)
+
+#include <cuda_runtime.h>
+#include <cuComplex.h>
+#include <math.h>
+#include "kernel_config.h"
+
+// Compute phase theta(x) for a given basis state x
+__device__ double compute_phase(
+ const double* __restrict__ data,
+ size_t x,
+ unsigned int num_qubits,
+ int enable_zz
+) {
+ double phase = 0.0;
+
+ // Single-qubit Z terms: sum_i x_i * data[i]
+ for (unsigned int i = 0; i < num_qubits; ++i) {
+ if ((x >> i) & 1U) {
+ phase += data[i];
+ }
+ }
+
+ // Two-qubit ZZ terms (if enabled): sum_{i<j} x_i * x_j * data[n +
pair_index]
+ if (enable_zz) {
+ unsigned int pair_idx = num_qubits;
+ for (unsigned int i = 0; i < num_qubits; ++i) {
+ for (unsigned int j = i + 1; j < num_qubits; ++j) {
+ if (((x >> i) & 1U) && ((x >> j) & 1U)) {
+ phase += data[pair_idx];
+ }
+ pair_idx++;
+ }
+ }
+ }
+
+ return phase;
+}
+
+__global__ void iqp_encode_kernel(
+ const double* __restrict__ data,
+ cuDoubleComplex* __restrict__ state,
+ size_t state_len,
+ unsigned int num_qubits,
+ int enable_zz
+) {
+ size_t z = blockIdx.x * blockDim.x + threadIdx.x;
+ if (z >= state_len) return;
+
+ double real_sum = 0.0;
+ double imag_sum = 0.0;
+
+ // Sum over all input basis states x
+ for (size_t x = 0; x < state_len; ++x) {
+ double phase = compute_phase(data, x, num_qubits, enable_zz);
+
+ // Compute (-1)^{popcount(x AND z)} using __popcll intrinsic
+ int parity = __popcll(x & z) & 1;
+ double sign = (parity == 0) ? 1.0 : -1.0;
+
+ // Accumulate: sign * exp(i*phase) = sign * (cos(phase) + i*sin(phase))
+ double cos_phase, sin_phase;
+ sincos(phase, &sin_phase, &cos_phase);
+ real_sum += sign * cos_phase;
+ imag_sum += sign * sin_phase;
+ }
+
+ // Normalize by 1/2^n (state_len = 2^n)
+ double norm = 1.0 / (double)state_len;
+ state[z] = make_cuDoubleComplex(real_sum * norm, imag_sum * norm);
+}
+
+__global__ void iqp_encode_batch_kernel(
+ const double* __restrict__ data_batch,
+ cuDoubleComplex* __restrict__ state_batch,
+ size_t num_samples,
+ size_t state_len,
+ unsigned int num_qubits,
+ unsigned int data_len,
+ int enable_zz
+) {
+ const size_t total_elements = num_samples * state_len;
+ const size_t stride = gridDim.x * blockDim.x;
+ const size_t state_mask = state_len - 1;
+
+ for (size_t global_idx = blockIdx.x * blockDim.x + threadIdx.x;
+ global_idx < total_elements;
+ global_idx += stride) {
+ const size_t sample_idx = global_idx >> num_qubits;
+ const size_t z = global_idx & state_mask;
+ const double* data = data_batch + sample_idx * data_len;
+
+ double real_sum = 0.0;
+ double imag_sum = 0.0;
+
+ // Sum over all input basis states x
+ for (size_t x = 0; x < state_len; ++x) {
+ double phase = compute_phase(data, x, num_qubits, enable_zz);
+
+ // Compute (-1)^{popcount(x AND z)}
+ int parity = __popcll(x & z) & 1;
+ double sign = (parity == 0) ? 1.0 : -1.0;
+
+ double cos_phase, sin_phase;
+ sincos(phase, &sin_phase, &cos_phase);
+ real_sum += sign * cos_phase;
+ imag_sum += sign * sin_phase;
+ }
+
+ double norm = 1.0 / (double)state_len;
+ state_batch[global_idx] = make_cuDoubleComplex(real_sum * norm,
imag_sum * norm);
+ }
+}
+
+extern "C" {
+
+/// Launch IQP encoding kernel
+///
+/// # Arguments
+/// * data_d - Device pointer to encoding parameters
+/// * state_d - Device pointer to output state vector
+/// * state_len - Target state vector size (2^num_qubits)
+/// * num_qubits - Number of qubits
+/// * enable_zz - 0 for Z-only, 1 for full ZZ interactions
+/// * stream - CUDA stream for async execution (nullptr = default stream)
+///
+/// # Returns
+/// CUDA error code (0 = cudaSuccess)
+int launch_iqp_encode(
+ const double* data_d,
+ void* state_d,
+ size_t state_len,
+ unsigned int num_qubits,
+ int enable_zz,
+ cudaStream_t stream
+) {
+ if (state_len == 0 || num_qubits == 0) {
+ return cudaErrorInvalidValue;
+ }
+
+ cuDoubleComplex* state_complex_d = static_cast<cuDoubleComplex*>(state_d);
+
+ const int blockSize = DEFAULT_BLOCK_SIZE;
+ const int gridSize = (state_len + blockSize - 1) / blockSize;
+
+ iqp_encode_kernel<<<gridSize, blockSize, 0, stream>>>(
+ data_d,
+ state_complex_d,
+ state_len,
+ num_qubits,
+ enable_zz
+ );
+
+ return (int)cudaGetLastError();
+}
+
+/// Launch batch IQP encoding kernel
+///
+/// # Arguments
+/// * data_batch_d - Device pointer to batch parameters (num_samples *
data_len)
+/// * state_batch_d - Device pointer to output batch state vectors
+/// * num_samples - Number of samples in batch
+/// * state_len - State vector size per sample (2^num_qubits)
+/// * num_qubits - Number of qubits
+/// * data_len - Length of each sample's data
+/// * enable_zz - 0 for Z-only, 1 for full ZZ interactions
+/// * stream - CUDA stream for async execution
+///
+/// # Returns
+/// CUDA error code (0 = cudaSuccess)
+int launch_iqp_encode_batch(
+ const double* data_batch_d,
+ void* state_batch_d,
+ size_t num_samples,
+ size_t state_len,
+ unsigned int num_qubits,
+ unsigned int data_len,
+ int enable_zz,
+ cudaStream_t stream
+) {
+ if (num_samples == 0 || state_len == 0 || num_qubits == 0) {
+ return cudaErrorInvalidValue;
+ }
+
+ cuDoubleComplex* state_complex_d =
static_cast<cuDoubleComplex*>(state_batch_d);
+
+ const int blockSize = DEFAULT_BLOCK_SIZE;
+ const size_t total_elements = num_samples * state_len;
+ const size_t blocks_needed = (total_elements + blockSize - 1) / blockSize;
+ const size_t gridSize = (blocks_needed < MAX_GRID_BLOCKS) ? blocks_needed
: MAX_GRID_BLOCKS;
+
+ iqp_encode_batch_kernel<<<gridSize, blockSize, 0, stream>>>(
+ data_batch_d,
+ state_complex_d,
+ num_samples,
+ state_len,
+ num_qubits,
+ data_len,
+ enable_zz
+ );
+
+ return (int)cudaGetLastError();
+}
+
+} // extern "C"
diff --git a/qdp/qdp-kernels/src/lib.rs b/qdp/qdp-kernels/src/lib.rs
index 7bab4bf80..e31cb4e1d 100644
--- a/qdp/qdp-kernels/src/lib.rs
+++ b/qdp/qdp-kernels/src/lib.rs
@@ -188,6 +188,36 @@ unsafe extern "C" {
num_qubits: u32,
stream: *mut c_void,
) -> i32;
+
+ /// Launch IQP encoding kernel
+ /// Returns CUDA error code (0 = success)
+ ///
+ /// # Safety
+ /// Requires valid GPU pointers, must sync before freeing
+ pub fn launch_iqp_encode(
+ data_d: *const f64,
+ state_d: *mut c_void,
+ state_len: usize,
+ num_qubits: u32,
+ enable_zz: i32,
+ stream: *mut c_void,
+ ) -> i32;
+
+ /// Launch batch IQP encoding kernel
+ /// Returns CUDA error code (0 = success)
+ ///
+ /// # Safety
+ /// Requires valid GPU pointers, must sync before freeing
+ pub fn launch_iqp_encode_batch(
+ data_batch_d: *const f64,
+ state_batch_d: *mut c_void,
+ num_samples: usize,
+ state_len: usize,
+ num_qubits: u32,
+ data_len: u32,
+ enable_zz: i32,
+ stream: *mut c_void,
+ ) -> i32;
}
// Dummy implementation for non-Linux and Linux builds without CUDA (allows
linking)
@@ -313,3 +343,31 @@ pub extern "C" fn launch_angle_encode_batch(
) -> i32 {
999
}
+
+#[cfg(any(not(target_os = "linux"), qdp_no_cuda))]
+#[unsafe(no_mangle)]
+pub extern "C" fn launch_iqp_encode(
+ _data_d: *const f64,
+ _state_d: *mut c_void,
+ _state_len: usize,
+ _num_qubits: u32,
+ _enable_zz: i32,
+ _stream: *mut c_void,
+) -> i32 {
+ 999
+}
+
+#[cfg(any(not(target_os = "linux"), qdp_no_cuda))]
+#[unsafe(no_mangle)]
+pub extern "C" fn launch_iqp_encode_batch(
+ _data_batch_d: *const f64,
+ _state_batch_d: *mut c_void,
+ _num_samples: usize,
+ _state_len: usize,
+ _num_qubits: u32,
+ _data_len: u32,
+ _enable_zz: i32,
+ _stream: *mut c_void,
+) -> i32 {
+ 999
+}
diff --git a/testing/qdp/test_bindings.py b/testing/qdp/test_bindings.py
index 590e3ec63..d928d93c5 100644
--- a/testing/qdp/test_bindings.py
+++ b/testing/qdp/test_bindings.py
@@ -764,3 +764,263 @@ def test_angle_encode_errors():
# Non-finite angle
with pytest.raises(RuntimeError, match="must be finite"):
engine.encode([float("nan"), 0.0], 2, "angle")
+
+
+# ==================== IQP Encoding Tests ====================
+
+
[email protected]
+def test_iqp_z_encode_basic():
+ """Test basic IQP-Z encoding with zero angles (requires GPU).
+
+ With zero parameters, IQP produces |00...0⟩ because:
+ - H^n|0⟩^n gives uniform superposition
+ - Zero phases leave state unchanged
+ - H^n transforms back to |0⟩^n
+ """
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # With zero angles, H^n * I * H^n |0⟩ = |0⟩, so amplitude 1 at index 0
+ qtensor = engine.encode([0.0, 0.0], 2, "iqp-z")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.is_cuda
+ assert torch_tensor.shape == (1, 4)
+
+ # Should get |00⟩ state: amplitude 1 at index 0, 0 elsewhere
+ expected = torch.tensor([[1.0 + 0j, 0.0 + 0j, 0.0 + 0j, 0.0 + 0j]],
device="cuda:0")
+ assert torch.allclose(torch_tensor, expected, atol=1e-6)
+
+
[email protected]
+def test_iqp_z_encode_nonzero():
+ """Test IQP-Z encoding with non-zero angles (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # With non-zero angles, we get interference patterns
+ # Using pi on qubit 0: phase flip when qubit 0 is |1⟩
+ qtensor = engine.encode([torch.pi, 0.0], 2, "iqp-z")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.shape == (1, 4)
+
+ # The state should be different from |00⟩
+ # Verify normalization (sum of |amplitude|^2 = 1)
+ norm = torch.sum(torch.abs(torch_tensor) ** 2)
+ assert torch.allclose(norm, torch.tensor(1.0, device="cuda:0"), atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_basic():
+ """Test basic IQP encoding with ZZ interactions (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # 2 qubits needs 3 parameters: [theta_0, theta_1, J_01]
+ # With all zeros, should get |00⟩ state
+ qtensor = engine.encode([0.0, 0.0, 0.0], 2, "iqp")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.is_cuda
+ assert torch_tensor.shape == (1, 4)
+
+ # Should get |00⟩ state: amplitude 1 at index 0, 0 elsewhere
+ expected = torch.tensor([[1.0 + 0j, 0.0 + 0j, 0.0 + 0j, 0.0 + 0j]],
device="cuda:0")
+ assert torch.allclose(torch_tensor, expected, atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_zz_effect():
+ """Test that ZZ interaction produces different result than Z-only
(requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # Same single-qubit angles, but with ZZ interaction
+ angles_z_only = [torch.pi / 4, torch.pi / 4]
+ angles_with_zz = [torch.pi / 4, torch.pi / 4, torch.pi / 2] # Add J_01
+
+ qtensor_z = engine.encode(angles_z_only, 2, "iqp-z")
+ qtensor_zz = engine.encode(angles_with_zz, 2, "iqp")
+
+ tensor_z = torch.from_dlpack(qtensor_z)
+ tensor_zz = torch.from_dlpack(qtensor_zz)
+
+ # The two should be different due to ZZ interaction
+ assert not torch.allclose(tensor_z, tensor_zz, atol=1e-6)
+
+ # Both should be normalized
+ norm_z = torch.sum(torch.abs(tensor_z) ** 2)
+ norm_zz = torch.sum(torch.abs(tensor_zz) ** 2)
+ assert torch.allclose(norm_z, torch.tensor(1.0, device="cuda:0"),
atol=1e-6)
+ assert torch.allclose(norm_zz, torch.tensor(1.0, device="cuda:0"),
atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_3_qubits():
+ """Test IQP encoding with 3 qubits (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # 3 qubits needs 6 parameters: [theta_0, theta_1, theta_2, J_01, J_02,
J_12]
+ # With all zeros, should get |000⟩ state
+ qtensor = engine.encode([0.0, 0.0, 0.0, 0.0, 0.0, 0.0], 3, "iqp")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.shape == (1, 8)
+
+ # Should get |000⟩ state: amplitude 1 at index 0, 0 elsewhere
+ expected = torch.zeros((1, 8), dtype=torch.complex128, device="cuda:0")
+ expected[0, 0] = 1.0 + 0j
+ assert torch.allclose(torch_tensor, expected, atol=1e-6)
+
+
[email protected]
+def test_iqp_z_encode_batch():
+ """Test batch IQP-Z encoding (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # Batch of 2 samples with different angles
+ data = torch.tensor([[0.0, 0.0], [torch.pi, 0.0]], dtype=torch.float64)
+ qtensor = engine.encode(data, 2, "iqp-z")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.shape == (2, 4)
+
+ # First sample (zero angles) should give |00⟩
+ expected_0 = torch.tensor([1.0 + 0j, 0.0 + 0j, 0.0 + 0j, 0.0 + 0j],
device="cuda:0")
+ assert torch.allclose(torch_tensor[0], expected_0, atol=1e-6)
+
+ # Second sample should be different and normalized
+ norm_1 = torch.sum(torch.abs(torch_tensor[1]) ** 2)
+ assert torch.allclose(norm_1, torch.tensor(1.0, device="cuda:0"),
atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_batch():
+ """Test batch IQP encoding with ZZ interactions (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # Batch of 2 samples, each with 3 parameters (2 qubits)
+ data = torch.tensor(
+ [[0.0, 0.0, 0.0], [torch.pi / 4, torch.pi / 4, torch.pi / 2]],
+ dtype=torch.float64,
+ )
+ qtensor = engine.encode(data, 2, "iqp")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.shape == (2, 4)
+
+ # First sample (zero params) should give |00⟩
+ expected_0 = torch.tensor([1.0 + 0j, 0.0 + 0j, 0.0 + 0j, 0.0 + 0j],
device="cuda:0")
+ assert torch.allclose(torch_tensor[0], expected_0, atol=1e-6)
+
+ # Second sample should be different and normalized
+ norm_1 = torch.sum(torch.abs(torch_tensor[1]) ** 2)
+ assert torch.allclose(norm_1, torch.tensor(1.0, device="cuda:0"),
atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_single_qubit():
+ """Test IQP encoding with single qubit edge case (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # 1 qubit, iqp-z needs 1 parameter
+ qtensor = engine.encode([0.0], 1, "iqp-z")
+ torch_tensor = torch.from_dlpack(qtensor)
+
+ assert torch_tensor.shape == (1, 2)
+
+ # Zero angle gives |0⟩
+ expected = torch.tensor([[1.0 + 0j, 0.0 + 0j]], device="cuda:0")
+ assert torch.allclose(torch_tensor, expected, atol=1e-6)
+
+ # 1 qubit, iqp needs 1 parameter (no pairs)
+ qtensor2 = engine.encode([0.0], 1, "iqp")
+ torch_tensor2 = torch.from_dlpack(qtensor2)
+ assert torch.allclose(torch_tensor2, expected, atol=1e-6)
+
+
[email protected]
+def test_iqp_encode_errors():
+ """Test error handling for IQP encoding (requires GPU)."""
+ pytest.importorskip("torch")
+ import torch
+ from _qdp import QdpEngine
+
+ if not torch.cuda.is_available():
+ pytest.skip("GPU required for QdpEngine")
+
+ engine = QdpEngine(0)
+
+ # Wrong length for iqp-z (expects 2 for 2 qubits, got 3)
+ with pytest.raises(RuntimeError, match="expects 2 values"):
+ engine.encode([0.0, 0.0, 0.0], 2, "iqp-z")
+
+ # Wrong length for iqp (expects 3 for 2 qubits, got 2)
+ with pytest.raises(RuntimeError, match="expects 3 values"):
+ engine.encode([0.0, 0.0], 2, "iqp")
+
+ # Non-finite parameter (NaN)
+ with pytest.raises(RuntimeError, match="must be finite"):
+ engine.encode([float("nan"), 0.0], 2, "iqp-z")
+
+ # Non-finite parameter (positive infinity)
+ with pytest.raises(RuntimeError, match="must be finite"):
+ engine.encode([0.0, float("inf"), 0.0], 2, "iqp")
+
+ # Non-finite parameter (negative infinity)
+ with pytest.raises(RuntimeError, match="must be finite"):
+ engine.encode([float("-inf"), 0.0], 2, "iqp-z")