This is an automated email from the ASF dual-hosted git repository.

guanmingchiu 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 11db508df [QDP] Refactor `encode()` method into helper functions with 
tests (#814)
11db508df is described below

commit 11db508dfdfc5deac48f4f0250f421dcbc8ae92c
Author: Vic Wen <[email protected]>
AuthorDate: Fri Jan 23 23:39:55 2026 +0800

    [QDP] Refactor `encode()` method into helper functions with tests (#814)
    
    * refactor: Refactor encoding methods for NumPy and PyTorch tensors
    
    - Introduced dedicated methods `encode_from_numpy` and 
`encode_from_pytorch` to streamline the encoding process for NumPy arrays and 
PyTorch tensors, respectively.
    - Improved error handling for unsupported shapes in both encoding methods.
    - Simplified the main encoding logic by delegating to these new methods.
    
    * chore: remove unused import
    
    * fix(python): replace PyReadonlyArrayDyn with PyReadonlyArray1/2
    
    Replace deprecated PyReadonlyArrayDyn with dimension-specific types
    PyReadonlyArray1 and PyReadonlyArray2 for better type safety and
    compatibility with newer pyo3-numpy versions.
    
    * refactor(tests): refactor `test_bindings.py`
    
    * feat(validation): add shape validation for arrays and tensors
    
    * test: add 3D tensor shape validation testing
    
    * Remove commented-out section for IQP Encoding Tests in `test_bindings.py`
---
 qdp/qdp-python/src/lib.rs    | 544 ++++++++++++++++++++++++++-----------------
 testing/qdp/test_bindings.py | 348 ++++++++++++++-------------
 2 files changed, 507 insertions(+), 385 deletions(-)

diff --git a/qdp/qdp-python/src/lib.rs b/qdp/qdp-python/src/lib.rs
index 016ee1259..bcf03c129 100644
--- a/qdp/qdp-python/src/lib.rs
+++ b/qdp/qdp-python/src/lib.rs
@@ -14,7 +14,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use numpy::{PyReadonlyArray1, PyReadonlyArray2, PyReadonlyArrayDyn, 
PyUntypedArrayMethods};
+use numpy::{PyReadonlyArray1, PyReadonlyArray2, PyUntypedArrayMethods};
 use pyo3::exceptions::PyRuntimeError;
 use pyo3::ffi;
 use pyo3::prelude::*;
@@ -178,6 +178,32 @@ fn is_cuda_tensor(tensor: &Bound<'_, PyAny>) -> 
PyResult<bool> {
     Ok(device_type == "cuda")
 }
 
+/// Validate array/tensor shape (must be 1D or 2D)
+///
+/// Args:
+///     ndim: Number of dimensions
+///     context: Context string for error message (e.g., "array", "tensor", 
"CUDA tensor")
+///
+/// Returns:
+///     Ok(()) if shape is valid (1D or 2D), otherwise returns an error
+fn validate_shape(ndim: usize, context: &str) -> PyResult<()> {
+    match ndim {
+        1 | 2 => Ok(()),
+        _ => {
+            let item_type = if context.contains("array") {
+                "array"
+            } else {
+                "tensor"
+            };
+            Err(PyRuntimeError::new_err(format!(
+                "Unsupported {} shape: {}D. Expected 1D {} for single sample \
+                 encoding or 2D {} (batch_size, features) for batch encoding.",
+                context, ndim, item_type, item_type
+            )))
+        }
+    }
+}
+
 /// Get the CUDA device index from a PyTorch tensor
 fn get_tensor_device_id(tensor: &Bound<'_, PyAny>) -> PyResult<i32> {
     let device = tensor.getattr("device")?;
@@ -238,17 +264,43 @@ fn validate_cuda_tensor_for_encoding(
     Ok(())
 }
 
-/// CUDA tensor information extracted directly from PyTorch tensor
-struct CudaTensorInfo {
+/// DLPack tensor information extracted from a PyCapsule
+///
+/// This struct owns the DLManagedTensor pointer and ensures proper cleanup
+/// via the DLPack deleter when dropped (RAII pattern).
+struct DLPackTensorInfo {
+    /// Raw DLManagedTensor pointer from PyTorch DLPack capsule
+    /// This is owned by this struct and will be freed via deleter on drop
+    managed_ptr: *mut DLManagedTensor,
+    /// Data pointer inside dl_tensor (GPU memory, owned by managed_ptr)
     data_ptr: *const f64,
     shape: Vec<i64>,
+    /// CUDA device ID from DLPack metadata.
+    /// Currently unused but kept for potential future device validation or 
multi-GPU support.
+    #[allow(dead_code)]
+    device_id: i32,
+}
+
+impl Drop for DLPackTensorInfo {
+    fn drop(&mut self) {
+        unsafe {
+            if !self.managed_ptr.is_null() {
+                // Per DLPack protocol: consumer must call deleter exactly once
+                if let Some(deleter) = (*self.managed_ptr).deleter {
+                    deleter(self.managed_ptr);
+                }
+                // Prevent double-free
+                self.managed_ptr = std::ptr::null_mut();
+            }
+        }
+    }
 }
 
-/// Extract GPU pointer directly from PyTorch CUDA tensor
+/// Extract GPU pointer from PyTorch tensor's __dlpack__() capsule
 ///
-/// Uses PyTorch's `data_ptr()` and `shape` APIs directly instead of DLPack 
protocol.
-/// This avoids the DLPack capsule lifecycle complexity and potential memory 
leaks
-/// from the capsule renaming pattern.
+/// Uses the DLPack protocol to obtain a zero-copy view of the tensor's GPU 
memory.
+/// The returned `DLPackTensorInfo` owns the DLManagedTensor and will 
automatically
+/// call the deleter when dropped, ensuring proper resource cleanup.
 ///
 /// # Safety
 /// The returned `data_ptr` points to GPU memory owned by the source tensor.
@@ -256,19 +308,59 @@ struct CudaTensorInfo {
 /// for the entire duration that `data_ptr` is in use. Python's GIL ensures
 /// the tensor won't be garbage collected during `encode()`, but the caller
 /// must not deallocate or resize the tensor while encoding is in progress.
-fn extract_cuda_tensor_info(tensor: &Bound<'_, PyAny>) -> 
PyResult<CudaTensorInfo> {
-    // Get GPU pointer directly via tensor.data_ptr()
-    let data_ptr_int: isize = tensor.call_method0("data_ptr")?.extract()?;
-    if data_ptr_int == 0 {
-        return Err(PyRuntimeError::new_err("CUDA tensor has null data 
pointer"));
-    }
-    let data_ptr = data_ptr_int as *const f64;
+fn extract_dlpack_tensor(_py: Python<'_>, tensor: &Bound<'_, PyAny>) -> 
PyResult<DLPackTensorInfo> {
+    // Call tensor.__dlpack__() to get PyCapsule
+    // Note: PyTorch's __dlpack__ uses the default stream when called without 
arguments
+    let capsule = tensor.call_method0("__dlpack__")?;
+
+    // Extract the DLManagedTensor pointer from the capsule
+    const DLTENSOR_NAME: &[u8] = b"dltensor\0";
+
+    unsafe {
+        let capsule_ptr = capsule.as_ptr();
+        let managed_ptr =
+            ffi::PyCapsule_GetPointer(capsule_ptr, DLTENSOR_NAME.as_ptr() as 
*const i8)
+                as *mut DLManagedTensor;
+
+        if managed_ptr.is_null() {
+            return Err(PyRuntimeError::new_err(
+                "Failed to extract DLManagedTensor from PyCapsule",
+            ));
+        }
 
-    // Get shape directly via tensor.shape
-    let shape_obj = tensor.getattr("shape")?;
-    let shape: Vec<i64> = shape_obj.extract()?;
+        let dl_tensor = &(*managed_ptr).dl_tensor;
+
+        // Extract data pointer with null check
+        if dl_tensor.data.is_null() {
+            return Err(PyRuntimeError::new_err(
+                "DLPack tensor has null data pointer",
+            ));
+        }
+        let data_ptr = dl_tensor.data as *const f64;
+
+        // Extract shape
+        let ndim = dl_tensor.ndim as usize;
+        let shape = if ndim > 0 && !dl_tensor.shape.is_null() {
+            std::slice::from_raw_parts(dl_tensor.shape, ndim).to_vec()
+        } else {
+            vec![]
+        };
 
-    Ok(CudaTensorInfo { data_ptr, shape })
+        // Extract device_id
+        let device_id = dl_tensor.device.device_id;
+
+        // Rename the capsule to "used_dltensor" as per DLPack protocol
+        // This prevents PyTorch from trying to delete it when the capsule is 
garbage collected
+        const USED_DLTENSOR_NAME: &[u8] = b"used_dltensor\0";
+        ffi::PyCapsule_SetName(capsule_ptr, USED_DLTENSOR_NAME.as_ptr() as 
*const i8);
+
+        Ok(DLPackTensorInfo {
+            managed_ptr,
+            data_ptr,
+            shape,
+            device_id,
+        })
+    }
 }
 
 /// PyO3 wrapper for QdpEngine
@@ -358,237 +450,253 @@ impl QdpEngine {
 
         // Check if it's a NumPy array
         if data.hasattr("__array_interface__")? {
-            // Get the array's ndim for shape validation
-            let ndim: usize = data.getattr("ndim")?.extract()?;
-
-            match ndim {
-                1 => {
-                    // 1D array: single sample encoding (zero-copy if already 
contiguous)
-                    let array_1d = 
data.extract::<PyReadonlyArray1<f64>>().map_err(|_| {
-                        PyRuntimeError::new_err(
-                            "Failed to extract 1D NumPy array. Ensure dtype is 
float64.",
-                        )
-                    })?;
-                    let data_slice = array_1d.as_slice().map_err(|_| {
-                        PyRuntimeError::new_err("NumPy array must be 
contiguous (C-order)")
-                    })?;
-                    let ptr = self
-                        .engine
-                        .encode(data_slice, num_qubits, encoding_method)
-                        .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
-                    return Ok(QuantumTensor {
-                        ptr,
-                        consumed: false,
-                    });
-                }
-                2 => {
-                    // 2D array: batch encoding (zero-copy if already 
contiguous)
-                    let array_2d = 
data.extract::<PyReadonlyArray2<f64>>().map_err(|_| {
-                        PyRuntimeError::new_err(
-                            "Failed to extract 2D NumPy array. Ensure dtype is 
float64.",
-                        )
-                    })?;
-                    let shape = array_2d.shape();
-                    let num_samples = shape[0];
-                    let sample_size = shape[1];
-                    let data_slice = array_2d.as_slice().map_err(|_| {
-                        PyRuntimeError::new_err("NumPy array must be 
contiguous (C-order)")
-                    })?;
-                    let ptr = self
-                        .engine
-                        .encode_batch(
-                            data_slice,
-                            num_samples,
-                            sample_size,
-                            num_qubits,
-                            encoding_method,
-                        )
-                        .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
-                    return Ok(QuantumTensor {
-                        ptr,
-                        consumed: false,
-                    });
-                }
-                _ => {
-                    return Err(PyRuntimeError::new_err(format!(
-                        "Unsupported array shape: {}D. Expected 1D array for 
single sample \
-                         encoding or 2D array (batch_size, features) for batch 
encoding.",
-                        ndim
-                    )));
-                }
-            }
+            return self.encode_from_numpy(data, num_qubits, encoding_method);
         }
 
         // Check if it's a PyTorch tensor
         if is_pytorch_tensor(data)? {
-            // Check if it's a CUDA tensor - use zero-copy GPU encoding
-            if is_cuda_tensor(data)? {
-                // Validate CUDA tensor for direct GPU encoding
-                validate_cuda_tensor_for_encoding(
-                    data,
-                    self.engine.device().ordinal(),
-                    encoding_method,
-                )?;
-
-                // Extract GPU pointer directly from PyTorch tensor
-                let tensor_info = extract_cuda_tensor_info(data)?;
-
-                let ndim: usize = data.call_method0("dim")?.extract()?;
-
-                match ndim {
-                    1 => {
-                        // 1D CUDA tensor: single sample encoding
-                        let input_len = tensor_info.shape[0] as usize;
-                        // SAFETY: tensor_info.data_ptr was obtained via 
PyTorch's data_ptr() from a
-                        // valid CUDA tensor. The tensor remains alive during 
this call
-                        // (held by Python's GIL), and we validated 
dtype/contiguity/device above.
-                        let ptr = unsafe {
-                            self.engine
-                                .encode_from_gpu_ptr(
-                                    tensor_info.data_ptr,
-                                    input_len,
-                                    num_qubits,
-                                    encoding_method,
-                                )
-                                .map_err(|e| {
-                                    PyRuntimeError::new_err(format!("Encoding 
failed: {}", e))
-                                })?
-                        };
-                        return Ok(QuantumTensor {
-                            ptr,
-                            consumed: false,
-                        });
-                    }
-                    2 => {
-                        // 2D CUDA tensor: batch encoding
-                        let num_samples = tensor_info.shape[0] as usize;
-                        let sample_size = tensor_info.shape[1] as usize;
-                        // SAFETY: Same as above - pointer from validated 
PyTorch CUDA tensor
-                        let ptr = unsafe {
-                            self.engine
-                                .encode_batch_from_gpu_ptr(
-                                    tensor_info.data_ptr,
-                                    num_samples,
-                                    sample_size,
-                                    num_qubits,
-                                    encoding_method,
-                                )
-                                .map_err(|e| {
-                                    PyRuntimeError::new_err(format!("Encoding 
failed: {}", e))
-                                })?
-                        };
-                        return Ok(QuantumTensor {
-                            ptr,
-                            consumed: false,
-                        });
-                    }
-                    _ => {
-                        return Err(PyRuntimeError::new_err(format!(
-                            "Unsupported CUDA tensor shape: {}D. Expected 1D 
tensor for single \
-                             sample encoding or 2D tensor (batch_size, 
features) for batch encoding.",
-                            ndim
-                        )));
-                    }
-                }
-            }
+            return self.encode_from_pytorch(data, num_qubits, encoding_method);
+        }
 
-            // CPU tensor path (existing code)
-            validate_tensor(data)?;
-            // PERF: Avoid Tensor -> Python list -> Vec deep copies.
-            //
-            // For CPU tensors, `tensor.detach().numpy()` returns a NumPy view 
that shares the same
-            // underlying memory (zero-copy) when the tensor is C-contiguous. 
We can then borrow a
-            // `&[f64]` directly via pyo3-numpy.
-            let ndim: usize = data.call_method0("dim")?.extract()?;
-            let numpy_view = data
-                .call_method0("detach")?
-                .call_method0("numpy")
-                .map_err(|_| {
+        // Fallback: try to extract as Vec<f64> (Python list)
+        self.encode_from_list(data, num_qubits, encoding_method)
+    }
+
+    /// Encode from NumPy array (1D or 2D)
+    fn encode_from_numpy(
+        &self,
+        data: &Bound<'_, PyAny>,
+        num_qubits: usize,
+        encoding_method: &str,
+    ) -> PyResult<QuantumTensor> {
+        let ndim: usize = data.getattr("ndim")?.extract()?;
+        validate_shape(ndim, "array")?;
+
+        match ndim {
+            1 => {
+                // 1D array: single sample encoding (zero-copy if already 
contiguous)
+                let array_1d = 
data.extract::<PyReadonlyArray1<f64>>().map_err(|_| {
                     PyRuntimeError::new_err(
-                        "Failed to convert torch.Tensor to NumPy view. Ensure 
the tensor is on CPU \
-                         and does not require grad (try: tensor = 
tensor.detach().cpu())",
+                        "Failed to extract 1D NumPy array. Ensure dtype is 
float64.",
                     )
                 })?;
-
-            let array = numpy_view
-                .extract::<PyReadonlyArrayDyn<f64>>()
-                .map_err(|_| {
+                let data_slice = array_1d.as_slice().map_err(|_| {
+                    PyRuntimeError::new_err("NumPy array must be contiguous 
(C-order)")
+                })?;
+                let ptr = self
+                    .engine
+                    .encode(data_slice, num_qubits, encoding_method)
+                    .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                Ok(QuantumTensor {
+                    ptr,
+                    consumed: false,
+                })
+            }
+            2 => {
+                // 2D array: batch encoding (zero-copy if already contiguous)
+                let array_2d = 
data.extract::<PyReadonlyArray2<f64>>().map_err(|_| {
                     PyRuntimeError::new_err(
-                        "Failed to extract NumPy view as float64 array. Ensure 
dtype is float64 \
-                         (try: tensor = tensor.to(torch.float64))",
+                        "Failed to extract 2D NumPy array. Ensure dtype is 
float64.",
                     )
                 })?;
+                let shape = array_2d.shape();
+                let num_samples = shape[0];
+                let sample_size = shape[1];
+                let data_slice = array_2d.as_slice().map_err(|_| {
+                    PyRuntimeError::new_err("NumPy array must be contiguous 
(C-order)")
+                })?;
+                let ptr = self
+                    .engine
+                    .encode_batch(
+                        data_slice,
+                        num_samples,
+                        sample_size,
+                        num_qubits,
+                        encoding_method,
+                    )
+                    .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                Ok(QuantumTensor {
+                    ptr,
+                    consumed: false,
+                })
+            }
+            _ => unreachable!("validate_shape() should have caught invalid 
ndim"),
+        }
+    }
 
-            let data_slice = array.as_slice().map_err(|_| {
-                PyRuntimeError::new_err(
-                    "Tensor must be contiguous (C-order) to get zero-copy 
slice \
-                     (try: tensor = tensor.contiguous())",
-                )
-            })?;
+    /// Encode from PyTorch tensor (1D or 2D)
+    fn encode_from_pytorch(
+        &self,
+        data: &Bound<'_, PyAny>,
+        num_qubits: usize,
+        encoding_method: &str,
+    ) -> PyResult<QuantumTensor> {
+        // Check if it's a CUDA tensor - use zero-copy GPU encoding via DLPack
+        if is_cuda_tensor(data)? {
+            // Validate CUDA tensor for direct GPU encoding
+            validate_cuda_tensor_for_encoding(
+                data,
+                self.engine.device().ordinal(),
+                encoding_method,
+            )?;
+
+            // Extract GPU pointer via DLPack (RAII wrapper ensures deleter is 
called)
+            let dlpack_info = extract_dlpack_tensor(data.py(), data)?;
+
+            let ndim: usize = data.call_method0("dim")?.extract()?;
+            validate_shape(ndim, "CUDA tensor")?;
 
             match ndim {
                 1 => {
-                    // 1D tensor: single sample encoding
-                    let ptr = self
-                        .engine
-                        .encode(data_slice, num_qubits, encoding_method)
-                        .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                    // 1D CUDA tensor: single sample encoding
+                    let input_len = dlpack_info.shape[0] as usize;
+                    // SAFETY: dlpack_info.data_ptr was validated via DLPack 
protocol from a
+                    // valid PyTorch CUDA tensor. The tensor remains alive 
during this call
+                    // (held by Python's GIL), and we validated 
dtype/contiguity/device above.
+                    // The DLPackTensorInfo RAII wrapper will call deleter 
when dropped.
+                    let ptr = unsafe {
+                        self.engine
+                            .encode_from_gpu_ptr(
+                                dlpack_info.data_ptr,
+                                input_len,
+                                num_qubits,
+                                encoding_method,
+                            )
+                            .map_err(|e| {
+                                PyRuntimeError::new_err(format!("Encoding 
failed: {}", e))
+                            })?
+                    };
                     return Ok(QuantumTensor {
                         ptr,
                         consumed: false,
                     });
                 }
                 2 => {
-                    // 2D tensor: batch encoding
-                    let shape = array.shape();
-                    if shape.len() != 2 {
-                        return Err(PyRuntimeError::new_err(format!(
-                            "Unsupported tensor shape: {}D. Expected 2D tensor 
(batch_size, features).",
-                            shape.len()
-                        )));
-                    }
-                    let num_samples = shape[0];
-                    let sample_size = shape[1];
-                    let ptr = self
-                        .engine
-                        .encode_batch(
-                            data_slice,
-                            num_samples,
-                            sample_size,
-                            num_qubits,
-                            encoding_method,
-                        )
-                        .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                    // 2D CUDA tensor: batch encoding
+                    let num_samples = dlpack_info.shape[0] as usize;
+                    let sample_size = dlpack_info.shape[1] as usize;
+                    // SAFETY: Same as above - pointer from validated DLPack 
tensor
+                    let ptr = unsafe {
+                        self.engine
+                            .encode_batch_from_gpu_ptr(
+                                dlpack_info.data_ptr,
+                                num_samples,
+                                sample_size,
+                                num_qubits,
+                                encoding_method,
+                            )
+                            .map_err(|e| {
+                                PyRuntimeError::new_err(format!("Encoding 
failed: {}", e))
+                            })?
+                    };
                     return Ok(QuantumTensor {
                         ptr,
                         consumed: false,
                     });
                 }
-                _ => {
-                    return Err(PyRuntimeError::new_err(format!(
-                        "Unsupported tensor shape: {}D. Expected 1D tensor for 
single sample \
-                         encoding or 2D tensor (batch_size, features) for 
batch encoding.",
-                        ndim
-                    )));
-                }
+                _ => unreachable!("validate_shape() should have caught invalid 
ndim"),
             }
         }
 
-        // Fallback: try to extract as Vec<f64> (Python list)
-        if let Ok(vec_data) = data.extract::<Vec<f64>>() {
-            let ptr = self
-                .engine
-                .encode(&vec_data, num_qubits, encoding_method)
-                .map_err(|e| PyRuntimeError::new_err(format!("Encoding failed: 
{}", e)))?;
-            return Ok(QuantumTensor {
-                ptr,
-                consumed: false,
-            });
+        // CPU tensor path
+        validate_tensor(data)?;
+        // PERF: Avoid Tensor -> Python list -> Vec deep copies.
+        //
+        // For CPU tensors, `tensor.detach().numpy()` returns a NumPy view 
that shares the same
+        // underlying memory (zero-copy) when the tensor is C-contiguous. We 
can then borrow a
+        // `&[f64]` directly via pyo3-numpy.
+        let ndim: usize = data.call_method0("dim")?.extract()?;
+        validate_shape(ndim, "tensor")?;
+        let numpy_view = data
+            .call_method0("detach")?
+            .call_method0("numpy")
+            .map_err(|_| {
+                PyRuntimeError::new_err(
+                    "Failed to convert torch.Tensor to NumPy view. Ensure the 
tensor is on CPU \
+                     and does not require grad (try: tensor = 
tensor.detach().cpu())",
+                )
+            })?;
+
+        match ndim {
+            1 => {
+                // 1D tensor: single sample encoding
+                let array_1d = 
numpy_view.extract::<PyReadonlyArray1<f64>>().map_err(|_| {
+                    PyRuntimeError::new_err(
+                        "Failed to extract NumPy view as float64 array. Ensure 
dtype is float64 \
+                             (try: tensor = tensor.to(torch.float64))",
+                    )
+                })?;
+                let data_slice = array_1d.as_slice().map_err(|_| {
+                    PyRuntimeError::new_err(
+                        "Tensor must be contiguous (C-order) to get zero-copy 
slice \
+                         (try: tensor = tensor.contiguous())",
+                    )
+                })?;
+                let ptr = self
+                    .engine
+                    .encode(data_slice, num_qubits, encoding_method)
+                    .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                Ok(QuantumTensor {
+                    ptr,
+                    consumed: false,
+                })
+            }
+            2 => {
+                // 2D tensor: batch encoding
+                let array_2d = 
numpy_view.extract::<PyReadonlyArray2<f64>>().map_err(|_| {
+                    PyRuntimeError::new_err(
+                        "Failed to extract NumPy view as float64 array. Ensure 
dtype is float64 \
+                             (try: tensor = tensor.to(torch.float64))",
+                    )
+                })?;
+                let shape = array_2d.shape();
+                let num_samples = shape[0];
+                let sample_size = shape[1];
+                let data_slice = array_2d.as_slice().map_err(|_| {
+                    PyRuntimeError::new_err(
+                        "Tensor must be contiguous (C-order) to get zero-copy 
slice \
+                         (try: tensor = tensor.contiguous())",
+                    )
+                })?;
+                let ptr = self
+                    .engine
+                    .encode_batch(
+                        data_slice,
+                        num_samples,
+                        sample_size,
+                        num_qubits,
+                        encoding_method,
+                    )
+                    .map_err(|e| PyRuntimeError::new_err(format!("Encoding 
failed: {}", e)))?;
+                Ok(QuantumTensor {
+                    ptr,
+                    consumed: false,
+                })
+            }
+            _ => unreachable!("validate_shape() should have caught invalid 
ndim"),
         }
+    }
 
-        Err(PyRuntimeError::new_err(
-            "Unsupported data type. Expected: list, NumPy array, PyTorch 
tensor, or file path",
-        ))
+    /// Encode from Python list
+    fn encode_from_list(
+        &self,
+        data: &Bound<'_, PyAny>,
+        num_qubits: usize,
+        encoding_method: &str,
+    ) -> PyResult<QuantumTensor> {
+        let vec_data = data.extract::<Vec<f64>>().map_err(|_| {
+            PyRuntimeError::new_err(
+                "Unsupported data type. Expected: list, NumPy array, PyTorch 
tensor, or file path",
+            )
+        })?;
+        let ptr = self
+            .engine
+            .encode(&vec_data, num_qubits, encoding_method)
+            .map_err(|e| PyRuntimeError::new_err(format!("Encoding failed: 
{}", e)))?;
+        Ok(QuantumTensor {
+            ptr,
+            consumed: false,
+        })
     }
 
     /// Internal helper to encode from file based on extension
diff --git a/testing/qdp/test_bindings.py b/testing/qdp/test_bindings.py
index 3823db3d1..56b2da7b2 100644
--- a/testing/qdp/test_bindings.py
+++ b/testing/qdp/test_bindings.py
@@ -17,6 +17,7 @@
 """Simple tests for PyO3 bindings."""
 
 import pytest
+import torch
 import _qdp
 
 
@@ -138,43 +139,43 @@ def test_pytorch_integration():
 
 
 @pytest.mark.gpu
-def test_pytorch_precision_float64():
-    """Verify optional float64 precision produces complex128 tensors."""
[email protected](
+    "precision,expected_dtype",
+    [
+        ("float32", "complex64"),
+        ("float64", "complex128"),
+    ],
+)
+def test_precision(precision, expected_dtype):
+    """Test different precision settings produce correct output dtypes."""
     pytest.importorskip("torch")
     import torch
     from _qdp import QdpEngine
 
-    engine = QdpEngine(0, precision="float64")
+    engine = QdpEngine(0, precision=precision)
     data = [1.0, 2.0, 3.0, 4.0]
     qtensor = engine.encode(data, 2, "amplitude")
 
     torch_tensor = torch.from_dlpack(qtensor)
-    assert torch_tensor.dtype == torch.complex128
-
-
[email protected]
-def test_encode_tensor_cpu():
-    """Test encoding from CPU PyTorch tensor (1D, single sample)."""
-    pytest.importorskip("torch")
-    import torch
-    from _qdp import QdpEngine
-
-    if not torch.cuda.is_available():
-        pytest.skip("GPU required for QdpEngine")
-
-    engine = QdpEngine(0)
-    data = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float64)
-    qtensor = engine.encode(data, 2, "amplitude")
-
-    # Verify result
-    torch_tensor = torch.from_dlpack(qtensor)
-    assert torch_tensor.is_cuda
-    assert torch_tensor.shape == (1, 4)
+    expected = getattr(torch, expected_dtype)
+    assert torch_tensor.dtype == expected, (
+        f"Expected {expected_dtype}, got {torch_tensor.dtype}"
+    )
 
 
 @pytest.mark.gpu
-def test_encode_tensor_batch():
-    """Test encoding from CPU PyTorch tensor (2D, batch encoding with 
zero-copy)."""
[email protected](
+    "data_shape,expected_shape",
+    [
+        ([1.0, 2.0, 3.0, 4.0], (1, 4)),  # 1D tensor -> single sample
+        (
+            [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 
12.0]],
+            (3, 4),
+        ),  # 2D tensor -> batch
+    ],
+)
+def test_encode_tensor_cpu(data_shape, expected_shape):
+    """Test encoding from CPU PyTorch tensor (1D or 2D, zero-copy)."""
     pytest.importorskip("torch")
     import torch
     from _qdp import QdpEngine
@@ -183,19 +184,16 @@ def test_encode_tensor_batch():
         pytest.skip("GPU required for QdpEngine")
 
     engine = QdpEngine(0)
-    # Create 2D tensor (batch_size=3, features=4)
-    data = torch.tensor(
-        [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]],
-        dtype=torch.float64,
-    )
-    assert data.is_contiguous(), "Test tensor should be contiguous for 
zero-copy"
+    data = torch.tensor(data_shape, dtype=torch.float64)
+    if len(data_shape) > 1:
+        assert data.is_contiguous(), "Test tensor should be contiguous for 
zero-copy"
 
     qtensor = engine.encode(data, 2, "amplitude")
 
     # Verify result
     torch_tensor = torch.from_dlpack(qtensor)
     assert torch_tensor.is_cuda
-    assert torch_tensor.shape == (3, 4), "Batch encoding should preserve batch 
size"
+    assert torch_tensor.shape == expected_shape
 
 
 @pytest.mark.gpu
@@ -255,34 +253,19 @@ def test_encode_errors():
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_1d():
-    """Test encoding from 1D CUDA tensor (single sample, zero-copy)."""
-    pytest.importorskip("torch")
-    import torch
-    from _qdp import QdpEngine
-
-    if not torch.cuda.is_available():
-        pytest.skip("GPU required for QdpEngine")
-
-    engine = QdpEngine(0)
-
-    # Create 1D CUDA tensor
-    data = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float64, 
device="cuda:0")
-    qtensor = engine.encode(data, 2, "amplitude")
-
-    # Verify result
-    result = torch.from_dlpack(qtensor)
-    assert result.is_cuda
-    assert result.shape == (1, 4)  # 2^2 = 4 amplitudes
-
-    # Verify normalization (amplitudes should have unit norm)
-    norm = torch.sqrt(torch.sum(torch.abs(result) ** 2))
-    assert torch.isclose(norm, torch.tensor(1.0, device="cuda:0"), atol=1e-6)
-
-
[email protected]
-def test_encode_cuda_tensor_2d_batch():
-    """Test encoding from 2D CUDA tensor (batch, zero-copy)."""
[email protected](
+    "data_shape,expected_shape,expected_batch_size",
+    [
+        ([1.0, 2.0, 3.0, 4.0], (1, 4), 1),  # 1D tensor -> single sample
+        (
+            [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 
12.0]],
+            (3, 4),
+            3,
+        ),  # 2D tensor -> batch
+    ],
+)
+def test_encode_cuda_tensor(data_shape, expected_shape, expected_batch_size):
+    """Test encoding from CUDA tensor (1D or 2D, zero-copy)."""
     pytest.importorskip("torch")
     import torch
     from _qdp import QdpEngine
@@ -292,21 +275,17 @@ def test_encode_cuda_tensor_2d_batch():
 
     engine = QdpEngine(0)
 
-    # Create 2D CUDA tensor (batch_size=3, features=4)
-    data = torch.tensor(
-        [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0], [9.0, 10.0, 11.0, 12.0]],
-        dtype=torch.float64,
-        device="cuda:0",
-    )
+    # Create CUDA tensor
+    data = torch.tensor(data_shape, dtype=torch.float64, device="cuda:0")
     qtensor = engine.encode(data, 2, "amplitude")
 
     # Verify result
     result = torch.from_dlpack(qtensor)
     assert result.is_cuda
-    assert result.shape == (3, 4)  # batch_size=3, 2^2=4
+    assert result.shape == expected_shape
 
-    # Verify each sample is normalized
-    for i in range(3):
+    # Verify normalization (each sample should have unit norm)
+    for i in range(expected_batch_size):
         norm = torch.sqrt(torch.sum(torch.abs(result[i]) ** 2))
         assert torch.isclose(norm, torch.tensor(1.0, device="cuda:0"), 
atol=1e-6)
 
@@ -389,8 +368,15 @@ def test_encode_cuda_tensor_empty():
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_preserves_input():
-    """Test that input CUDA tensor is not modified after encoding."""
[email protected](
+    "data_shape,is_batch",
+    [
+        ([1.0, 2.0, 3.0, 4.0], False),  # 1D tensor
+        ([[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]], True),  # 2D tensor 
(batch)
+    ],
+)
+def test_encode_cuda_tensor_preserves_input(data_shape, is_batch):
+    """Test that input CUDA tensor (1D or 2D) is not modified after 
encoding."""
     pytest.importorskip("torch")
     import torch
     from _qdp import QdpEngine
@@ -401,8 +387,7 @@ def test_encode_cuda_tensor_preserves_input():
     engine = QdpEngine(0)
 
     # Create CUDA tensor and save a copy
-    original_data = [1.0, 2.0, 3.0, 4.0]
-    data = torch.tensor(original_data, dtype=torch.float64, device="cuda:0")
+    data = torch.tensor(data_shape, dtype=torch.float64, device="cuda:0")
     data_clone = data.clone()
 
     # Encode
@@ -414,7 +399,8 @@ def test_encode_cuda_tensor_preserves_input():
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_unsupported_encoding():
[email protected]("encoding_method", ["basis", "angle"])
+def test_encode_cuda_tensor_unsupported_encoding(encoding_method):
     """Test error when using CUDA tensor with unsupported encoding method."""
     pytest.importorskip("torch")
     import torch
@@ -430,53 +416,23 @@ def test_encode_cuda_tensor_unsupported_encoding():
     data = torch.tensor([1.0, 0.0, 0.0, 0.0], dtype=torch.float64, 
device="cuda:0")
 
     with pytest.raises(RuntimeError, match="only supports 'amplitude' method"):
-        engine.encode(data, 2, "basis")
-
-    with pytest.raises(RuntimeError, match="only supports 'amplitude' method"):
-        engine.encode(data, 2, "angle")
-
-
[email protected]
-def test_encode_cuda_tensor_3d_rejected():
-    """Test error when CUDA tensor has 3+ dimensions."""
-    pytest.importorskip("torch")
-    import torch
-    from _qdp import QdpEngine
-
-    if not torch.cuda.is_available():
-        pytest.skip("GPU required for QdpEngine")
-
-    engine = QdpEngine(0)
-
-    # Create 3D CUDA tensor (should be rejected)
-    data = torch.randn(2, 3, 4, dtype=torch.float64, device="cuda:0")
-    with pytest.raises(RuntimeError, match="Unsupported CUDA tensor shape: 
3D"):
-        engine.encode(data, 2, "amplitude")
-
-
[email protected]
-def test_encode_cuda_tensor_zero_values():
-    """Test error when CUDA tensor contains all zeros (zero norm)."""
-    pytest.importorskip("torch")
-    import torch
-    from _qdp import QdpEngine
-
-    if not torch.cuda.is_available():
-        pytest.skip("GPU required for QdpEngine")
-
-    engine = QdpEngine(0)
-
-    # Create CUDA tensor with all zeros (cannot be normalized)
-    data = torch.zeros(4, dtype=torch.float64, device="cuda:0")
-    with pytest.raises(RuntimeError, match="zero or non-finite norm"):
-        engine.encode(data, 2, "amplitude")
+        engine.encode(data, 2, encoding_method)
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_nan_values():
-    """Test error when CUDA tensor contains NaN values."""
[email protected](
+    "input_type,error_match",
+    [
+        ("cuda_tensor", "Unsupported CUDA tensor shape: 3D"),
+        ("cpu_tensor", "Unsupported tensor shape: 3D"),
+        ("numpy_array", "Unsupported array shape: 3D"),
+    ],
+)
+def test_encode_3d_rejected(input_type, error_match):
+    """Test error when input has 3+ dimensions (CUDA tensor, CPU tensor, or 
NumPy array)."""
     pytest.importorskip("torch")
     import torch
+    import numpy as np
     from _qdp import QdpEngine
 
     if not torch.cuda.is_available():
@@ -484,17 +440,41 @@ def test_encode_cuda_tensor_nan_values():
 
     engine = QdpEngine(0)
 
-    # Create CUDA tensor with NaN
-    data = torch.tensor(
-        [1.0, float("nan"), 3.0, 4.0], dtype=torch.float64, device="cuda:0"
-    )
-    with pytest.raises(RuntimeError, match="zero or non-finite norm"):
+    # Create 3D data based on input type
+    if input_type == "cuda_tensor":
+        data = torch.randn(2, 3, 4, dtype=torch.float64, device="cuda:0")
+    elif input_type == "cpu_tensor":
+        data = torch.randn(2, 3, 4, dtype=torch.float64)
+    elif input_type == "numpy_array":
+        data = np.random.randn(2, 3, 4).astype(np.float64)
+    else:
+        raise ValueError(f"Unknown input_type: {input_type}")
+
+    with pytest.raises(RuntimeError, match=error_match):
         engine.encode(data, 2, "amplitude")
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_inf_values():
-    """Test error when CUDA tensor contains Inf values."""
[email protected](
+    "tensor_factory,description",
+    [
+        (lambda: torch.zeros(4, dtype=torch.float64, device="cuda:0"), 
"zeros"),
+        (
+            lambda: torch.tensor(
+                [1.0, float("nan"), 3.0, 4.0], dtype=torch.float64, 
device="cuda:0"
+            ),
+            "NaN",
+        ),
+        (
+            lambda: torch.tensor(
+                [1.0, float("inf"), 3.0, 4.0], dtype=torch.float64, 
device="cuda:0"
+            ),
+            "Inf",
+        ),
+    ],
+)
+def test_encode_cuda_tensor_non_finite_values(tensor_factory, description):
+    """Test error when CUDA tensor contains non-finite values (zeros, NaN, 
Inf)."""
     pytest.importorskip("torch")
     import torch
     from _qdp import QdpEngine
@@ -503,17 +483,21 @@ def test_encode_cuda_tensor_inf_values():
         pytest.skip("GPU required for QdpEngine")
 
     engine = QdpEngine(0)
+    data = tensor_factory()
 
-    # Create CUDA tensor with Inf
-    data = torch.tensor(
-        [1.0, float("inf"), 3.0, 4.0], dtype=torch.float64, device="cuda:0"
-    )
     with pytest.raises(RuntimeError, match="zero or non-finite norm"):
         engine.encode(data, 2, "amplitude")
 
 
 @pytest.mark.gpu
-def test_encode_cuda_tensor_output_dtype():
[email protected](
+    "precision,expected_dtype",
+    [
+        ("float32", torch.complex64),
+        ("float64", torch.complex128),
+    ],
+)
+def test_encode_cuda_tensor_output_dtype(precision, expected_dtype):
     """Test that CUDA tensor encoding produces correct output dtype."""
     pytest.importorskip("torch")
     import torch
@@ -522,45 +506,12 @@ def test_encode_cuda_tensor_output_dtype():
     if not torch.cuda.is_available():
         pytest.skip("GPU required for QdpEngine")
 
-    # Test default precision (float32 -> complex64)
-    engine_f32 = QdpEngine(0, precision="float32")
-    data = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float64, 
device="cuda:0")
-    result = torch.from_dlpack(engine_f32.encode(data, 2, "amplitude"))
-    assert result.dtype == torch.complex64, f"Expected complex64, got 
{result.dtype}"
-
-    # Test float64 precision (float64 -> complex128)
-    engine_f64 = QdpEngine(0, precision="float64")
+    engine = QdpEngine(0, precision=precision)
     data = torch.tensor([1.0, 2.0, 3.0, 4.0], dtype=torch.float64, 
device="cuda:0")
-    result = torch.from_dlpack(engine_f64.encode(data, 2, "amplitude"))
-    assert result.dtype == torch.complex128, f"Expected complex128, got 
{result.dtype}"
-
-
[email protected]
-def test_encode_cuda_tensor_preserves_input_batch():
-    """Test that input 2D CUDA tensor (batch) is not modified after 
encoding."""
-    pytest.importorskip("torch")
-    import torch
-    from _qdp import QdpEngine
-
-    if not torch.cuda.is_available():
-        pytest.skip("GPU required for QdpEngine")
-
-    engine = QdpEngine(0)
-
-    # Create 2D CUDA tensor and save a copy
-    data = torch.tensor(
-        [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
-        dtype=torch.float64,
-        device="cuda:0",
+    result = torch.from_dlpack(engine.encode(data, 2, "amplitude"))
+    assert result.dtype == expected_dtype, (
+        f"Expected {expected_dtype}, got {result.dtype}"
     )
-    data_clone = data.clone()
-
-    # Encode
-    qtensor = engine.encode(data, 2, "amplitude")
-    _ = torch.from_dlpack(qtensor)
-
-    # Verify original tensor is unchanged
-    assert torch.equal(data, data_clone)
 
 
 @pytest.mark.gpu
@@ -766,7 +717,70 @@ def test_angle_encode_errors():
         engine.encode([float("nan"), 0.0], 2, "angle")
 
 
-# ==================== IQP Encoding Tests ====================
[email protected]
[email protected](
+    "data_shape,expected_shape",
+    [
+        ([1.0, 2.0, 3.0, 4.0], (1, 4)),  # 1D array -> single sample
+        (
+            [[1.0, 2.0, 3.0, 4.0], [5.0, 6.0, 7.0, 8.0]],
+            (2, 4),
+        ),  # 2D array -> batch
+    ],
+)
+def test_encode_numpy_array(data_shape, expected_shape):
+    """Test encoding from NumPy array (1D or 2D)."""
+    pytest.importorskip("torch")
+    import numpy as np
+    import torch
+    from _qdp import QdpEngine
+
+    if not torch.cuda.is_available():
+        pytest.skip("GPU required for QdpEngine")
+
+    engine = QdpEngine(0)
+    data = np.array(data_shape, dtype=np.float64)
+    qtensor = engine.encode(data, 2, "amplitude")
+
+    # Verify result
+    torch_tensor = torch.from_dlpack(qtensor)
+    assert torch_tensor.is_cuda
+    assert torch_tensor.shape == expected_shape
+
+
[email protected]
+def test_encode_pathlib_path():
+    """Test encoding from pathlib.Path object."""
+    pytest.importorskip("torch")
+    import numpy as np
+    import torch
+    from pathlib import Path
+    import tempfile
+    import os
+    from _qdp import QdpEngine
+
+    if not torch.cuda.is_available():
+        pytest.skip("GPU required for QdpEngine")
+
+    engine = QdpEngine(0)
+    num_qubits = 2
+    sample_size = 2**num_qubits
+
+    # Create temporary .npy file
+    data = np.array([[1.0, 2.0, 3.0, 4.0], [0.5, 0.5, 0.5, 0.5]], 
dtype=np.float64)
+    with tempfile.NamedTemporaryFile(suffix=".npy", delete=False) as f:
+        npy_path = Path(f.name)
+        np.save(npy_path, data)
+
+    try:
+        # Test with pathlib.Path
+        qtensor = engine.encode(npy_path, num_qubits, "amplitude")
+        torch_tensor = torch.from_dlpack(qtensor)
+        assert torch_tensor.is_cuda
+        assert torch_tensor.shape == (2, sample_size)
+    finally:
+        if os.path.exists(npy_path):
+            os.remove(npy_path)
 
 
 @pytest.mark.gpu


Reply via email to