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")

Reply via email to