fix(gaps): G1 — re-enable Python bindings (PyO3 0.25, Python 3.14)

- Upgrade workspace pyo3 0.24 → 0.25 and numpy 0.24 → 0.25 for Python 3.14 support
- rtx-sklearn-py: replace pinned pyo3 0.20 / pyo3-asyncio 0.20 / numpy 0.20 with workspace versions;
  remove broken pyo3-asyncio async feature; update pyo3-build-config to 0.24
- rtx-bindings: uncomment pyo3/numpy/ndarray optional deps; enable python feature in Cargo.toml
- Migrate rtx-bindings python/ to PyO3 0.25 Bound API:
  &PyAny → Bound<'py, PyAny>, downcast/extract on Bound types, remove rtx_runtime import,
  remove InferenceError arm (variant not in enum), fix py_shape_to_shape signature
- Migrate rtx-sklearn-py src/ to PyO3 0.25 Bound API:
  #[pymodule] fn now takes &Bound<'_, PyModule>, &PyDict → &Bound<'py, PyDict>,
  from_array returns Bound (unbind instead of to_owned), PyTuple::new now fallible,
  use numpy::ndarray (0.16) over workspace ndarray (0.15) to resolve trait mismatches

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-26 13:55:09 +00:00
co-authored by Claude Sonnet 4.6
parent 228137555f
commit 448c0a0be5
14 changed files with 310 additions and 355 deletions
+2 -2
View File
@@ -543,8 +543,8 @@ comfy-table = "7.1"
colorful = "0.2" colorful = "0.2"
walkdir = "2.5" walkdir = "2.5"
toml = "0.8" toml = "0.8"
pyo3 = { version = "0.24", features = ["extension-module"] } pyo3 = { version = "0.25", features = ["extension-module"] }
numpy = "0.24" numpy = "0.25"
petgraph = "0.6" petgraph = "0.6"
# Federated learning specific # Federated learning specific
+5 -4
View File
@@ -16,9 +16,10 @@ crate-type = ["cdylib", "rlib"]
rtx-tensor = { workspace = true } rtx-tensor = { workspace = true }
rtx-autograd = { workspace = true } rtx-autograd = { workspace = true }
# Python bindings with PyO3 (disabled for now due to version conflicts) # Python bindings with PyO3
# pyo3 = { workspace = true, optional = true } pyo3 = { workspace = true, optional = true }
# ndarray = { workspace = true, optional = true } numpy = { workspace = true, optional = true }
ndarray = { workspace = true, optional = true }
# C API support # C API support
libc = { version = "0.2", optional = true } libc = { version = "0.2", optional = true }
@@ -59,7 +60,7 @@ path = "tests/dlpack_interop.rs"
[features] [features]
default = ["c-api"] default = ["c-api"]
# python = ["dep:pyo3", "dep:ndarray"] python = ["dep:pyo3", "dep:numpy", "dep:ndarray"]
c-api = ["dep:libc"] c-api = ["dep:libc"]
onnx = ["dep:serde_json"] onnx = ["dep:serde_json"]
dlpack = [] dlpack = []
@@ -1,35 +1,24 @@
//! Async tensor operations for Python //! Async tensor operations for Python
//!
//! pyo3_asyncio is incompatible with pyo3 0.25. Async functions are replaced
//! with sync stubs that perform the operations directly.
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3_asyncio::tokio::future_into_py; use crate::BindingError;
use tokio::time::{sleep, Duration};
use crate::{BindingError, Result};
use super::PyTensor; use super::PyTensor;
/// Async matrix multiplication /// Matrix multiplication (sync; formerly async via pyo3_asyncio)
#[pyfunction] #[pyfunction]
pub fn matmul_async(py: Python, a: PyTensor, b: PyTensor) -> PyResult<&PyAny> { pub fn matmul_async(a: PyTensor, b: PyTensor) -> PyResult<PyTensor> {
future_into_py(py, async move { let result = a.inner().matmul(b.inner())
// Simulate async computation delay (in real implementation, this would be actual GPU async ops)
sleep(Duration::from_millis(1)).await;
let result = a.inner().matmul(b.inner())
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result))
Ok(PyTensor::new(result))
})
} }
/// Async sum operation /// Sum operation (sync; formerly async via pyo3_asyncio)
#[pyfunction] #[pyfunction]
pub fn sum_async(py: Python, tensor: PyTensor) -> PyResult<&PyAny> { pub fn sum_async(tensor: PyTensor) -> PyResult<PyTensor> {
future_into_py(py, async move { let result = tensor.inner().sum(None)
// Simulate async computation delay
sleep(Duration::from_millis(1)).await;
let result = tensor.inner().sum(None)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result))
Ok(PyTensor::new(result))
})
} }
+55 -63
View File
@@ -1,31 +1,31 @@
//! Factory functions for creating tensors from Python //! Factory functions for creating tensors from Python
use pyo3::prelude::*; use pyo3::prelude::*;
use numpy::{PyArrayDyn, PyReadonlyArrayDyn}; use numpy::{PyArrayDyn, PyArrayMethods};
use rtx_tensor::{Tensor, Device, DType, Shape}; use rtx_tensor::{Tensor, Shape};
use crate::{BindingError, Result}; use crate::BindingError;
use super::{PyTensor, py_shape_to_shape, py_device_to_device, get_runtime}; use super::{PyTensor, py_shape_to_shape, py_device_to_device};
/// Create tensor filled with zeros /// Create tensor filled with zeros
#[pyfunction] #[pyfunction]
pub fn zeros(py: Python, shape: &PyAny, device: Option<&str>) -> PyResult<PyTensor> { pub fn zeros<'py>(shape: &Bound<'py, PyAny>, device: Option<&str>) -> PyResult<PyTensor> {
let tensor_shape = py_shape_to_shape(py, shape)?; let tensor_shape = py_shape_to_shape(shape)?;
let target_device = py_device_to_device(device)?; let target_device = py_device_to_device(device)?;
let tensor = Tensor::zeros(tensor_shape, &target_device) let tensor = Tensor::zeros(tensor_shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} }
/// Create tensor filled with ones /// Create tensor filled with ones
#[pyfunction] #[pyfunction]
pub fn ones(py: Python, shape: &PyAny, device: Option<&str>) -> PyResult<PyTensor> { pub fn ones<'py>(shape: &Bound<'py, PyAny>, device: Option<&str>) -> PyResult<PyTensor> {
let tensor_shape = py_shape_to_shape(py, shape)?; let tensor_shape = py_shape_to_shape(shape)?;
let target_device = py_device_to_device(device)?; let target_device = py_device_to_device(device)?;
let tensor = Tensor::ones(tensor_shape, &target_device) let tensor = Tensor::ones(tensor_shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} }
@@ -33,10 +33,10 @@ pub fn ones(py: Python, shape: &PyAny, device: Option<&str>) -> PyResult<PyTenso
/// Create tensor with sequential values (like Python range) /// Create tensor with sequential values (like Python range)
#[pyfunction] #[pyfunction]
pub fn arange( pub fn arange(
start: f32, start: f32,
end: Option<f32>, end: Option<f32>,
step: Option<f32>, step: Option<f32>,
device: Option<&str> device: Option<&str>
) -> PyResult<PyTensor> { ) -> PyResult<PyTensor> {
let (actual_start, actual_end) = match end { let (actual_start, actual_end) = match end {
Some(e) => (start, e), Some(e) => (start, e),
@@ -64,14 +64,14 @@ device: Option<&str>
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
let tensor = Tensor::from_data(data, shape, &target_device) let tensor = Tensor::from_data(data, shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} }
/// Create tensor from NumPy array /// Create tensor from NumPy array
#[pyfunction] #[pyfunction]
pub fn from_numpy(py: Python, array: &PyAny, device: Option<&str>) -> PyResult<PyTensor> { pub fn from_numpy<'py>(py: Python<'py>, array: &Bound<'py, PyAny>, device: Option<&str>) -> PyResult<PyTensor> {
let target_device = py_device_to_device(device)?; let target_device = py_device_to_device(device)?;
// Try to extract as different array types // Try to extract as different array types
@@ -82,63 +82,55 @@ pub fn from_numpy(py: Python, array: &PyAny, device: Option<&str>) -> PyResult<P
// Get shape and data // Get shape and data
let shape = Shape::new(ndarray.shape().to_vec()) let shape = Shape::new(ndarray.shape().to_vec())
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
let data: Vec<f32> = if ndarray.is_standard_layout() { let data: Vec<f32> = ndarray.iter().cloned().collect();
// Fast path for standard layout
ndarray.iter().cloned().collect()
} else {
// Handle non-standard layouts
ndarray.iter().cloned().collect()
};
let tensor = Tensor::from_data(data, shape, &target_device) let tensor = Tensor::from_data(data, shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} else if let Ok(arr) = array.downcast::<PyArrayDyn<f64>>() { } else if let Ok(arr) = array.downcast::<PyArrayDyn<f64>>() {
// Convert f64 to f32 // Convert f64 to f32
let readonly = arr.readonly(); let readonly = arr.readonly();
let ndarray = readonly.as_array(); let ndarray = readonly.as_array();
let shape = Shape::new(ndarray.shape().to_vec()) let shape = Shape::new(ndarray.shape().to_vec())
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
let data: Vec<f32> = ndarray.iter().map(|&x| x as f32).collect(); let data: Vec<f32> = ndarray.iter().map(|&x| x as f32).collect();
let tensor = Tensor::from_data(data, shape, &target_device) let tensor = Tensor::from_data(data, shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} else { } else {
// Try to convert via Python buffer protocol // Try to convert via Python buffer protocol
let numpy = py.import("numpy")?; let numpy = py.import("numpy")?;
let converted = numpy.call_method1("asarray", (array,))?; let converted = numpy.call_method1("asarray", (array,))?;
let float_array = numpy.call_method1("astype", (converted, "float32"))?; let float_array = numpy.call_method1("astype", (&converted, "float32"))?;
// Recursively call with converted array // Recursively call with converted array
from_numpy(py, float_array, device) from_numpy(py, &float_array, device)
} }
} }
/// Create tensor filled with ones and then multiply by scalar (helper for tests) /// Create tensor filled with a scalar value
#[pyfunction] #[pyfunction]
pub fn full(py: Python, shape: &PyAny, value: f32, device: Option<&str>) -> PyResult<PyTensor> { pub fn full<'py>(shape: &Bound<'py, PyAny>, value: f32, device: Option<&str>) -> PyResult<PyTensor> {
let tensor_shape = py_shape_to_shape(py, shape)?; let tensor_shape = py_shape_to_shape(shape)?;
let target_device = py_device_to_device(device)?; let target_device = py_device_to_device(device)?;
// Create ones tensor and scale it // Create ones tensor and scale it
let ones_tensor = Tensor::ones(tensor_shape, &target_device) let ones_tensor = Tensor::ones(tensor_shape, &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
// For now, we'll create the scaled data manually let mut data = ones_tensor.to_cpu()
// In a full implementation, this would use a scalar multiplication operation .map_err(|e| BindingError::TensorError(e))?;
let mut data = ones_tensor.to_cpu()
.map_err(|e| BindingError::TensorError(e))?;
for val in &mut data { for val in &mut data {
*val *= value; *val *= value;
} }
let tensor = Tensor::from_data(data, ones_tensor.shape().clone(), &target_device) let tensor = Tensor::from_data(data, ones_tensor.shape().clone(), &target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(tensor)) Ok(PyTensor::new(tensor))
} }
+69 -82
View File
@@ -6,11 +6,8 @@
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::exceptions::{PyRuntimeError, PyValueError}; use pyo3::exceptions::{PyRuntimeError, PyValueError};
use pyo3::types::{PyList, PyTuple}; use pyo3::types::{PyList, PyTuple};
use numpy::{PyArray1, PyArray2, PyArrayDyn, PyReadonlyArrayDyn};
use crate::{BindingError, Result}; use crate::{BindingError, Result};
use rtx_tensor::{Tensor, Device, DType, Shape}; use rtx_tensor::{Device, Shape};
use rtx_runtime::Runtime;
use std::sync::Arc;
pub mod exceptions; pub mod exceptions;
pub mod tensor; pub mod tensor;
@@ -54,9 +51,6 @@ impl From<BindingError> for PyErr {
BindingError::AutogradError { message } => { BindingError::AutogradError { message } => {
PyValueError::new_err(format!("Autograd error: {}", message)) PyValueError::new_err(format!("Autograd error: {}", message))
} }
BindingError::InferenceError(e) => {
PyValueError::new_err(format!("Inference error: {}", e))
}
BindingError::IoError(e) => { BindingError::IoError(e) => {
PyValueError::new_err(format!("IO error: {}", e)) PyValueError::new_err(format!("IO error: {}", e))
} }
@@ -69,84 +63,77 @@ impl From<BindingError> for PyErr {
} }
/// Convert Python shape-like objects to Shape /// Convert Python shape-like objects to Shape
fn py_shape_to_shape(py: Python, obj: &PyAny) -> PyResult<Shape> { pub(crate) fn py_shape_to_shape<'py>(obj: &Bound<'py, PyAny>) -> PyResult<Shape> {
if let Ok(list) = obj.downcast::<PyList>() { if let Ok(list) = obj.downcast::<PyList>() {
let dims: Vec<usize> = list let dims: Vec<usize> = list
.iter()
.map(|item| item.extract::<usize>())
.collect::<PyResult<Vec<_>>>()?;
Shape::new(dims).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e)))
} else if let Ok(tuple) = obj.downcast::<PyTuple>() {
let dims: Vec<usize> = tuple
.iter() .iter()
.map(|item| item.extract::<usize>()) .map(|item| item.extract::<usize>())
.collect::<PyResult<Vec<_>>>()?; .collect::<PyResult<Vec<_>>>()?;
Shape::new(dims).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e))) Shape::new(dims).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e)))
} else if let Ok(int_val) = obj.extract::<usize>() { } else if let Ok(tuple) = obj.downcast::<PyTuple>() {
Shape::new(vec![int_val]).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e))) let dims: Vec<usize> = tuple
} else { .iter()
Err(PyValueError::new_err("Invalid shape: expected list, tuple, or integer")) .map(|item| item.extract::<usize>())
} .collect::<PyResult<Vec<_>>>()?;
Shape::new(dims).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e)))
} else if let Ok(int_val) = obj.extract::<usize>() {
Shape::new(vec![int_val]).map_err(|e| PyValueError::new_err(format!("Invalid shape: {}", e)))
} else {
Err(PyValueError::new_err("Invalid shape: expected list, tuple, or integer"))
}
}
/// Convert device string to Device
pub(crate) fn py_device_to_device(device_str: Option<&str>) -> Result<Device> {
match device_str {
None | Some("cpu") => Ok(Device::cuda(0).unwrap_or(Device::default())),
Some(s) if s.starts_with("cuda") => {
if s == "cuda" {
Ok(Device::Cuda(0))
} else if let Some(id_str) = s.strip_prefix("cuda:") {
let id = id_str.parse::<usize>()
.map_err(|_| BindingError::DeviceError {
message: format!("Invalid CUDA device ID: {}", id_str)
})?;
Ok(Device::Cuda(id))
} else {
Err(BindingError::DeviceError {
message: format!("Invalid CUDA device format: {}", s)
})
} }
}
/// Convert device string to Device Some(s) if s.starts_with("rocm") => {
fn py_device_to_device(device_str: Option<&str>) -> Result<Device> { if s == "rocm" {
match device_str { Ok(Device::Rocm(0))
None | Some("cpu") => Ok(Device::cuda(0).unwrap_or(Device::default())), } else if let Some(id_str) = s.strip_prefix("rocm:") {
Some(s) if s.starts_with("cuda") => { let id = id_str.parse::<usize>()
if s == "cuda" { .map_err(|_| BindingError::DeviceError {
Ok(Device::Cuda(0)) message: format!("Invalid ROCm device ID: {}", id_str)
} else if let Some(id_str) = s.strip_prefix("cuda:") { })?;
let id = id_str.parse::<usize>() Ok(Device::Rocm(id))
.map_err(|_| BindingError::DeviceError { } else {
message: format!("Invalid CUDA device ID: {}", id_str) Err(BindingError::DeviceError {
})?; message: format!("Invalid ROCm device format: {}", s)
Ok(Device::Cuda(id)) })
} else { }
Err(BindingError::DeviceError { }
message: format!("Invalid CUDA device format: {}", s) Some(s) if s.starts_with("metal") => {
}) if s == "metal" {
} Ok(Device::Metal(0))
} } else if let Some(id_str) = s.strip_prefix("metal:") {
Some(s) if s.starts_with("rocm") => { let id = id_str.parse::<usize>()
if s == "rocm" { .map_err(|_| BindingError::DeviceError {
Ok(Device::Rocm(0)) message: format!("Invalid Metal device ID: {}", id_str)
} else if let Some(id_str) = s.strip_prefix("rocm:") { })?;
let id = id_str.parse::<usize>() Ok(Device::Metal(id))
.map_err(|_| BindingError::DeviceError { } else {
message: format!("Invalid ROCm device ID: {}", id_str) Err(BindingError::DeviceError {
})?; message: format!("Invalid Metal device format: {}", s)
Ok(Device::Rocm(id)) })
} else { }
Err(BindingError::DeviceError { }
message: format!("Invalid ROCm device format: {}", s) Some(s) => Err(BindingError::DeviceError {
}) message: format!("Unsupported device: {}", s)
} })
} }
Some(s) if s.starts_with("metal") => { }
if s == "metal" {
Ok(Device::Metal(0))
} else if let Some(id_str) = s.strip_prefix("metal:") {
let id = id_str.parse::<usize>()
.map_err(|_| BindingError::DeviceError {
message: format!("Invalid Metal device ID: {}", id_str)
})?;
Ok(Device::Metal(id))
} else {
Err(BindingError::DeviceError {
message: format!("Invalid Metal device format: {}", s)
})
}
}
Some(s) => Err(BindingError::DeviceError {
message: format!("Unsupported device: {}", s)
})
}
}
/// Initialize runtime for tensor operations
fn get_runtime() -> Arc<Runtime> {
// In a real implementation, this would be a singleton
// For now, create a new runtime each time
Arc::new(Runtime::new().expect("Failed to initialize runtime"))
}
+39 -35
View File
@@ -1,11 +1,11 @@
//! Python tensor wrapper providing PyTorch-like API //! Python tensor wrapper providing PyTorch-like API
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::{PyList, PyTuple}; use pyo3::types::PyTuple;
use numpy::{PyArrayDyn, PyReadonlyArrayDyn, ToPyArray}; use numpy::{PyArray1, PyArray2, PyArrayDyn};
use rtx_tensor::{Tensor, Device, DType, Shape}; use rtx_tensor::{Tensor, Device, DType, Shape};
use crate::{BindingError, Result}; use crate::{BindingError, Result};
use super::{py_shape_to_shape, py_device_to_device, get_runtime}; use super::{py_shape_to_shape, py_device_to_device};
/// Python wrapper for RustyTorch++ Tensor /// Python wrapper for RustyTorch++ Tensor
/// ///
@@ -54,10 +54,10 @@ impl PyTensor {
#[getter] #[getter]
fn device(&self) -> String { fn device(&self) -> String {
match self.inner.device() { match self.inner.device() {
Device::cuda(0).unwrap_or(Device::default()) => "cpu".to_string(),
Device::Cuda(id) => format!("cuda:{}", id), Device::Cuda(id) => format!("cuda:{}", id),
Device::Rocm(id) => format!("rocm:{}", id), Device::Rocm(id) => format!("rocm:{}", id),
Device::Metal(id) => format!("metal:{}", id), Device::Metal(id) => format!("metal:{}", id),
_ => "cpu".to_string(),
} }
} }
@@ -77,6 +77,7 @@ impl PyTensor {
DType::I2 => "int2".to_string(), DType::I2 => "int2".to_string(),
DType::I1 => "int1".to_string(), DType::I1 => "int1".to_string(),
DType::Bool => "bool".to_string(), DType::Bool => "bool".to_string(),
_ => "unknown".to_string(),
} }
} }
@@ -99,7 +100,7 @@ impl PyTensor {
} }
/// Get single item from tensor (for scalars or single element access) /// Get single item from tensor (for scalars or single element access)
fn item(&self, py: Python, indices: Option<&PyTuple>) -> PyResult<f32> { fn item(&self, indices: Option<&Bound<'_, PyTuple>>) -> PyResult<f32> {
match indices { match indices {
None => { None => {
// Scalar access // Scalar access
@@ -110,15 +111,15 @@ impl PyTensor {
} }
// Get the single element // Get the single element
let data = self.inner.to_cpu() let data = self.inner.to_cpu()
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(data[0]) Ok(data[0])
} }
Some(indices) => { Some(indices) => {
// Multi-dimensional indexing // Multi-dimensional indexing
let index_vec: Vec<usize> = indices let index_vec: Vec<usize> = indices
.iter() .iter()
.map(|idx| idx.extract::<usize>()) .map(|idx| idx.extract::<usize>())
.collect::<PyResult<Vec<_>>>()?; .collect::<PyResult<Vec<_>>>()?;
if index_vec.len() != self.inner.ndim() { if index_vec.len() != self.inner.ndim() {
return Err(BindingError::RuntimeError { return Err(BindingError::RuntimeError {
@@ -140,25 +141,25 @@ impl PyTensor {
} }
let data = self.inner.to_cpu() let data = self.inner.to_cpu()
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(data[linear_idx]) Ok(data[linear_idx])
} }
} }
} }
/// Reshape tensor to new shape /// Reshape tensor to new shape
fn reshape(&self, py: Python, shape: &PyAny) -> PyResult<PyTensor> { fn reshape<'py>(&self, shape: &Bound<'py, PyAny>) -> PyResult<PyTensor> {
let new_shape = py_shape_to_shape(py, shape)?; let new_shape = py_shape_to_shape(shape)?;
let reshaped = self.inner.reshape(new_shape) let reshaped = self.inner.reshape(new_shape)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(reshaped)) Ok(PyTensor::new(reshaped))
} }
/// Create view with new shape (no data copy) /// Create view with new shape (no data copy)
fn view(&self, py: Python, shape: &PyAny) -> PyResult<PyTensor> { fn view<'py>(&self, shape: &Bound<'py, PyAny>) -> PyResult<PyTensor> {
let new_shape = py_shape_to_shape(py, shape)?; let new_shape = py_shape_to_shape(shape)?;
let view = self.inner.view(new_shape) let view = self.inner.view(new_shape)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(view)) Ok(PyTensor::new(view))
} }
@@ -167,7 +168,7 @@ impl PyTensor {
let total_elements = self.inner.numel(); let total_elements = self.inner.numel();
let flattened = self.inner.view(Shape::new(vec![total_elements]) let flattened = self.inner.view(Shape::new(vec![total_elements])
.map_err(|e| BindingError::TensorError(e))?) .map_err(|e| BindingError::TensorError(e))?)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(flattened)) Ok(PyTensor::new(flattened))
} }
@@ -175,7 +176,7 @@ impl PyTensor {
fn to(&self, device: &str) -> PyResult<PyTensor> { fn to(&self, device: &str) -> PyResult<PyTensor> {
let target_device = py_device_to_device(Some(device))?; let target_device = py_device_to_device(Some(device))?;
let moved_tensor = self.inner.to_device(&target_device) let moved_tensor = self.inner.to_device(&target_device)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(moved_tensor)) Ok(PyTensor::new(moved_tensor))
} }
@@ -201,34 +202,37 @@ impl PyTensor {
/// Compute backward pass through computational graph /// Compute backward pass through computational graph
fn backward(&self) -> PyResult<()> { fn backward(&self) -> PyResult<()> {
self.inner.backward() self.inner.backward()
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(()) Ok(())
} }
/// Convert to NumPy array (CPU only) /// Convert to NumPy array (CPU only)
fn numpy(&self, py: Python) -> PyResult<PyObject> { fn numpy(&self, py: Python) -> PyResult<PyObject> {
let data = self.inner.to_cpu() let data = self.inner.to_cpu()
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
let shape = self.inner.shape().dims(); let shape = self.inner.shape().dims();
// Use numpy's bundled ndarray to avoid version mismatch between
// workspace ndarray 0.15 and numpy 0.25's ndarray 0.16.
let ndarray = match shape.len() { let ndarray = match shape.len() {
1 => { 1 => {
let arr = ndarray::Array1::from_vec(data); let arr = numpy::ndarray::Array1::from_vec(data);
arr.to_pyarray(py).to_object(py) PyArray1::from_owned_array(py, arr).into_any().unbind()
} }
2 => { 2 => {
let arr = ndarray::Array2::from_shape_vec((shape[0], shape[1]), data) let arr = numpy::ndarray::Array2::from_shape_vec(
.map_err(|e| BindingError::RuntimeError { (shape[0], shape[1]), data,
).map_err(|e| BindingError::RuntimeError {
message: format!("Failed to create 2D array: {}", e) message: format!("Failed to create 2D array: {}", e)
})?; })?;
arr.to_pyarray(py).to_object(py) PyArray2::from_owned_array(py, arr).into_any().unbind()
} }
_ => { _ => {
let arr = ndarray::ArrayD::from_shape_vec(shape, data) let arr = numpy::ndarray::ArrayD::from_shape_vec(shape.to_vec(), data)
.map_err(|e| BindingError::RuntimeError { .map_err(|e| BindingError::RuntimeError {
message: format!("Failed to create ND array: {}", e) message: format!("Failed to create ND array: {}", e)
})?; })?;
arr.to_pyarray(py).to_object(py) PyArrayDyn::from_owned_array(py, arr).into_any().unbind()
} }
}; };
@@ -238,33 +242,33 @@ impl PyTensor {
// Arithmetic operations // Arithmetic operations
fn __add__(&self, other: &PyTensor) -> PyResult<PyTensor> { fn __add__(&self, other: &PyTensor) -> PyResult<PyTensor> {
let result = self.inner.add(&other.inner) let result = self.inner.add(&other.inner)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result)) Ok(PyTensor::new(result))
} }
fn __iadd__(&mut self, other: &PyTensor) -> PyResult<()> { fn __iadd__(&mut self, other: &PyTensor) -> PyResult<()> {
let result = self.inner.add(&other.inner) let result = self.inner.add(&other.inner)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
self.inner = result; self.inner = result;
Ok(()) Ok(())
} }
fn __mul__(&self, other: &PyTensor) -> PyResult<PyTensor> { fn __mul__(&self, other: &PyTensor) -> PyResult<PyTensor> {
let result = self.inner.mul(&other.inner) let result = self.inner.mul(&other.inner)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result)) Ok(PyTensor::new(result))
} }
fn __matmul__(&self, other: &PyTensor) -> PyResult<PyTensor> { fn __matmul__(&self, other: &PyTensor) -> PyResult<PyTensor> {
let result = self.inner.matmul(&other.inner) let result = self.inner.matmul(&other.inner)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result)) Ok(PyTensor::new(result))
} }
/// Sum reduction along specified dimension /// Sum reduction along specified dimension
fn sum(&self, dim: Option<usize>) -> PyResult<PyTensor> { fn sum(&self, dim: Option<usize>) -> PyResult<PyTensor> {
let result = self.inner.sum(dim) let result = self.inner.sum(dim)
.map_err(|e| BindingError::TensorError(e))?; .map_err(|e| BindingError::TensorError(e))?;
Ok(PyTensor::new(result)) Ok(PyTensor::new(result))
} }
+5 -9
View File
@@ -20,14 +20,11 @@ rtx-validation = { path = "../../core/rtx-validation" }
rtx-tensor = { path = "../../core/rtx-tensor" } rtx-tensor = { path = "../../core/rtx-tensor" }
# PyO3 for Python bindings # PyO3 for Python bindings
# Note: extension-module is enabled by default but disabled for tests pyo3 = { workspace = true, features = ["multiple-pymethods", "abi3-py38"] }
# When running tests, use: cargo test --no-default-features --features gpu,async
pyo3 = { version = "0.20", features = ["multiple-pymethods", "abi3-py38", "auto-initialize"] }
pyo3-asyncio = { version = "0.20", features = ["tokio-runtime"], optional = true }
# Numerical computing # Numerical computing
numpy = "0.20" numpy = { workspace = true }
ndarray = "0.15" ndarray = { workspace = true }
# Async support # Async support
tokio = { workspace = true } tokio = { workspace = true }
@@ -48,16 +45,15 @@ rand = "0.8"
num_cpus = "1.16" num_cpus = "1.16"
[build-dependencies] [build-dependencies]
pyo3-build-config = "0.20" pyo3-build-config = "0.24"
[dev-dependencies] [dev-dependencies]
proptest = { workspace = true } proptest = { workspace = true }
tokio-test = { workspace = true } tokio-test = { workspace = true }
[features] [features]
default = ["gpu", "async"] default = ["gpu"]
gpu = [] gpu = []
async = ["pyo3-asyncio"]
extension-module = ["pyo3/extension-module"] extension-module = ["pyo3/extension-module"]
# The extension-module feature should only be enabled when building the Python module, # The extension-module feature should only be enabled when building the Python module,
+10 -9
View File
@@ -33,12 +33,11 @@ clf = RandomForestClassifier(n_estimators=100).fit(X, y)
*/ */
// Allow unsafe operations in unsafe functions generated by PyO3 macros // Allow unsafe operations in unsafe functions generated by PyO3 macros
// This is a known issue with PyO3 0.20 and Rust 2024 edition
// The macro-generated code contains unsafe operations that need to be allowed
#![allow(unsafe_op_in_unsafe_fn)] #![allow(unsafe_op_in_unsafe_fn)]
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple}; use pyo3::types::{PyDict, PyTuple};
use pyo3::types::PyModuleMethods;
use pyo3::wrap_pyfunction; use pyo3::wrap_pyfunction;
pub mod error; pub mod error;
@@ -53,7 +52,9 @@ use wrappers::{
/// Initialize the rustytorch_ml Python module /// Initialize the rustytorch_ml Python module
#[pymodule] #[pymodule]
fn rustytorch_ml(_py: Python, m: &PyModule) -> PyResult<()> { fn rustytorch_ml(m: &Bound<'_, PyModule>) -> PyResult<()> {
let py = m.py();
// Add module metadata // Add module metadata
m.add("__version__", env!("CARGO_PKG_VERSION"))?; m.add("__version__", env!("CARGO_PKG_VERSION"))?;
m.add("__author__", "RustyTorch++ Team")?; m.add("__author__", "RustyTorch++ Team")?;
@@ -65,7 +66,7 @@ fn rustytorch_ml(_py: Python, m: &PyModule) -> PyResult<()> {
// Register exception classes // Register exception classes
m.add( m.add(
"SklearnError", "SklearnError",
_py.get_type::<pyo3::exceptions::PyRuntimeError>(), py.get_type::<pyo3::exceptions::PyRuntimeError>(),
)?; )?;
// Classifiers // Classifiers
@@ -103,15 +104,15 @@ fn rustytorch_ml(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_function(wrap_pyfunction!(utils::benchmark_against_sklearn, m)?)?; m.add_function(wrap_pyfunction!(utils::benchmark_against_sklearn, m)?)?;
// Module-level configuration // Module-level configuration
setup_module_config(_py, m)?; setup_module_config(py, m)?;
Ok(()) Ok(())
} }
/// Setup module-level configuration and logging /// Setup module-level configuration and logging
fn setup_module_config(_py: Python, m: &PyModule) -> PyResult<()> { fn setup_module_config(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> {
// Set default configuration // Set default configuration
let config = PyDict::new(_py); let config = PyDict::new(py);
config.set_item("default_device", "cpu")?; config.set_item("default_device", "cpu")?;
config.set_item("enable_gpu", false)?; config.set_item("enable_gpu", false)?;
config.set_item("default_batch_size", 1000)?; config.set_item("default_batch_size", 1000)?;
@@ -121,7 +122,7 @@ fn setup_module_config(_py: Python, m: &PyModule) -> PyResult<()> {
m.add("config", config)?; m.add("config", config)?;
// Add version info // Add version info
let version_info = PyTuple::new(_py, [0, 1, 0]); let version_info = PyTuple::new(py, [0_u32, 1, 0])?;
m.add("version_info", version_info)?; m.add("version_info", version_info)?;
// Add sklearn compatibility info // Add sklearn compatibility info
@@ -144,7 +145,7 @@ mod tests {
fn test_module_creation() { fn test_module_creation() {
Python::with_gil(|py| { Python::with_gil(|py| {
let module = PyModule::new(py, "rustytorch_ml").unwrap(); let module = PyModule::new(py, "rustytorch_ml").unwrap();
assert!(rustytorch_ml(py, module).is_ok()); assert!(rustytorch_ml(&module).is_ok());
// Check that classes are registered // Check that classes are registered
assert!(module.hasattr("DecisionTreeClassifier").unwrap()); assert!(module.hasattr("DecisionTreeClassifier").unwrap());
+30 -50
View File
@@ -3,8 +3,10 @@ Utility functions and types for sklearn-py bindings
*/ */
use crate::error::SklearnResult; use crate::error::SklearnResult;
use ndarray::{Array1, Array2, ArrayViewD}; // Use numpy's bundled ndarray (0.16) to avoid version mismatch with workspace ndarray (0.15).
use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn}; // PyReadonlyArray and PyArray types require numpy's ndarray version.
use numpy::ndarray::{Array1, Array2, ArrayViewD};
use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn, PyArrayMethods};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyDict; use pyo3::types::PyDict;
use std::collections::HashMap; use std::collections::HashMap;
@@ -65,7 +67,7 @@ impl DeviceConfig {
pub struct ArrayConverter; pub struct ArrayConverter;
impl ArrayConverter { impl ArrayConverter {
/// Convert Python array to ndarray ArrayView2 (dynamic view) /// Convert Python array to ndarray ArrayViewD (expects 2D)
pub fn to_array_view2<'py, T>( pub fn to_array_view2<'py, T>(
py_array: &'py PyReadonlyArrayDyn<T>, py_array: &'py PyReadonlyArrayDyn<T>,
) -> SklearnResult<ArrayViewD<'py, T>> ) -> SklearnResult<ArrayViewD<'py, T>>
@@ -84,7 +86,7 @@ impl ArrayConverter {
Ok(array) Ok(array)
} }
/// Convert Python array to ndarray ArrayView1 /// Convert Python array to ndarray ArrayViewD (expects 1D)
pub fn to_array_view1<'py, T>( pub fn to_array_view1<'py, T>(
py_array: &'py PyReadonlyArrayDyn<T>, py_array: &'py PyReadonlyArrayDyn<T>,
) -> SklearnResult<ArrayViewD<'py, T>> ) -> SklearnResult<ArrayViewD<'py, T>>
@@ -103,51 +105,32 @@ impl ArrayConverter {
Ok(array) Ok(array)
} }
/// Convert ndarray Array2 to Python array /// Convert ndarray Array2 to Python array.
pub fn from_array2<T>(py: Python<'_>, array: Array2<T>) -> PyResult<&PyArray2<T>> /// Returns a `Bound<'py, PyArray2<T>>` — call `.into_py(py)` or use directly.
pub fn from_array2<T>(py: Python<'_>, array: Array2<T>) -> PyResult<Bound<'_, PyArray2<T>>>
where where
T: numpy::Element, T: numpy::Element,
{ {
Ok(PyArray2::from_owned_array(py, array)) Ok(PyArray2::from_owned_array(py, array))
} }
/// Convert ndarray Array1 to Python array /// Convert ndarray Array1 to Python array.
pub fn from_array1<T>(py: Python<'_>, array: Array1<T>) -> PyResult<&PyArray1<T>> /// Returns a `Bound<'py, PyArray1<T>>` — call `.into_py(py)` or use directly.
pub fn from_array1<T>(py: Python<'_>, array: Array1<T>) -> PyResult<Bound<'_, PyArray1<T>>>
where where
T: numpy::Element, T: numpy::Element,
{ {
Ok(PyArray1::from_owned_array(py, array)) Ok(PyArray1::from_owned_array(py, array))
} }
/// Convert PyReadonlyArray1 to ArrayViewD (for type compatibility) /// Convert any readonly dyn array to dynamic view (generic helper)
pub fn to_array_view1_from_readonly1<'py, T>( pub fn to_dyn_view<'py, T>(
py_array: &'py numpy::PyReadonlyArray<'py, T, ndarray::Ix1>, py_array: &'py PyReadonlyArrayDyn<T>,
) -> SklearnResult<ArrayViewD<'py, T>>
where
T: numpy::Element,
{
Ok(py_array.as_array().into_dyn())
}
/// Convert PyReadonlyArray2 to ArrayViewD (for type compatibility)
pub fn to_array_view2_from_readonly2<'py, T>(
py_array: &'py numpy::PyReadonlyArray<'py, T, ndarray::Ix2>,
) -> SklearnResult<ArrayViewD<'py, T>>
where
T: numpy::Element,
{
Ok(py_array.as_array().into_dyn())
}
/// Convert any readonly array to dynamic view (generic helper)
pub fn to_dyn_view<'py, T, D>(
py_array: &'py numpy::PyReadonlyArray<'py, T, D>,
) -> ArrayViewD<'py, T> ) -> ArrayViewD<'py, T>
where where
T: numpy::Element, T: numpy::Element,
D: ndarray::Dimension,
{ {
py_array.as_array().into_dyn() py_array.as_array()
} }
/// Validate input shapes for sklearn methods /// Validate input shapes for sklearn methods
@@ -272,10 +255,8 @@ impl ArrayConverter {
#[pyfunction] #[pyfunction]
pub fn set_random_state(seed: Option<u64>) -> PyResult<()> { pub fn set_random_state(seed: Option<u64>) -> PyResult<()> {
// Set global random state for reproducibility // Set global random state for reproducibility
// This would integrate with the underlying ML libraries
match seed { match seed {
Some(s) => { Some(s) => {
// Set seeds for various RNGs used in the crate
println!("Setting random state to: {}", s); println!("Setting random state to: {}", s);
} }
None => { None => {
@@ -297,7 +278,7 @@ pub fn get_device_info(py: Python) -> PyResult<PyObject> {
#[cfg(feature = "gpu")] #[cfg(feature = "gpu")]
{ {
info.set_item("gpu_available", true)?; info.set_item("gpu_available", true)?;
info.set_item("gpu_count", 1)?; // Would query actual GPU count info.set_item("gpu_count", 1)?;
info.set_item("gpu_memory", "Unknown")?; info.set_item("gpu_memory", "Unknown")?;
} }
#[cfg(not(feature = "gpu"))] #[cfg(not(feature = "gpu"))]
@@ -330,7 +311,6 @@ pub fn enable_gpu_acceleration(enable: bool) -> PyResult<bool> {
#[pyfunction] #[pyfunction]
pub fn get_memory_usage() -> PyResult<f64> { pub fn get_memory_usage() -> PyResult<f64> {
// Return current memory usage in MB
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
use std::fs; use std::fs;
@@ -339,7 +319,7 @@ pub fn get_memory_usage() -> PyResult<f64> {
if line.starts_with("VmRSS:") { if line.starts_with("VmRSS:") {
if let Some(kb_str) = line.split_whitespace().nth(1) { if let Some(kb_str) = line.split_whitespace().nth(1) {
if let Ok(kb) = kb_str.parse::<f64>() { if let Ok(kb) = kb_str.parse::<f64>() {
return Ok(kb / 1024.0); // Convert to MB return Ok(kb / 1024.0);
} }
} }
} }
@@ -347,7 +327,6 @@ pub fn get_memory_usage() -> PyResult<f64> {
} }
} }
// Fallback - return 0 if we can't determine memory usage
Ok(0.0) Ok(0.0)
} }
@@ -362,12 +341,13 @@ pub fn benchmark_against_sklearn(
let runs = n_runs.unwrap_or(5); let runs = n_runs.unwrap_or(5);
let result = PyDict::new(py); let result = PyDict::new(py);
let x_array = x.as_array();
result.set_item("model", model_name)?; result.set_item("model", model_name)?;
result.set_item("n_samples", x.shape()[0])?; result.set_item("n_samples", x_array.shape()[0])?;
result.set_item("n_features", x.shape()[1])?; result.set_item("n_features", x_array.shape()[1])?;
result.set_item("n_runs", runs)?; result.set_item("n_runs", runs)?;
// Mock benchmark results (would perform actual benchmarking) // Mock benchmark results
result.set_item("rustytorch_fit_time", 0.1)?; result.set_item("rustytorch_fit_time", 0.1)?;
result.set_item("sklearn_fit_time", 0.25)?; result.set_item("sklearn_fit_time", 0.25)?;
result.set_item("speedup_factor", 2.5)?; result.set_item("speedup_factor", 2.5)?;
@@ -386,13 +366,14 @@ pub struct ParamValidator;
impl ParamValidator { impl ParamValidator {
/// Validate parameter dictionary from Python /// Validate parameter dictionary from Python
pub fn validate_params( pub fn validate_params<'py>(
params: &PyDict, params: &Bound<'py, PyDict>,
valid_params: &[&str], valid_params: &[&str],
) -> SklearnResult<HashMap<String, PyObject>> { ) -> SklearnResult<HashMap<String, PyObject>> {
use pyo3::types::PyDictMethods;
let mut validated = HashMap::new(); let mut validated = HashMap::new();
for (key, value) in params { for (key, value) in params.iter() {
let key_str: String = key.extract().map_err(|_| { let key_str: String = key.extract().map_err(|_| {
crate::sklearn_error!(InvalidParameter, "Parameter key must be string") crate::sklearn_error!(InvalidParameter, "Parameter key must be string")
})?; })?;
@@ -406,7 +387,7 @@ impl ParamValidator {
)); ));
} }
validated.insert(key_str, value.into()); validated.insert(key_str, value.unbind());
} }
Ok(validated) Ok(validated)
@@ -450,8 +431,8 @@ impl AsyncHelper {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use ndarray::arr2; use numpy::ndarray::arr2;
use numpy::PyArray2; use numpy::PyArrayDyn;
#[test] #[test]
fn test_device_config() { fn test_device_config() {
@@ -470,9 +451,8 @@ mod tests {
fn test_array_converter() { fn test_array_converter() {
Python::with_gil(|py| { Python::with_gil(|py| {
let arr = arr2(&[[1.0, 2.0], [3.0, 4.0]]); let arr = arr2(&[[1.0, 2.0], [3.0, 4.0]]);
// Convert to dynamic dimensionality array
let arr_dyn = arr.into_dyn(); let arr_dyn = arr.into_dyn();
let py_arr = numpy::PyArrayDyn::from_owned_array(py, arr_dyn.clone()); let py_arr = PyArrayDyn::from_owned_array(py, arr_dyn);
let readonly = py_arr.readonly(); let readonly = py_arr.readonly();
let view = ArrayConverter::to_array_view2(&readonly).unwrap(); let view = ArrayConverter::to_array_view2(&readonly).unwrap();
@@ -2,7 +2,7 @@
Simplified sklearn-compatible classifier wrappers that compile with current API Simplified sklearn-compatible classifier wrappers that compile with current API
*/ */
use ndarray::{Array1, Array2}; use numpy::ndarray::{Array1, Array2};
use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn}; use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::PyDict; use pyo3::types::PyDict;
@@ -196,7 +196,7 @@ impl DecisionTreeClassifier {
let predictions: Array1<i64> = let predictions: Array1<i64> =
Array1::from_iter(predictions_data.iter().map(|&x| x.round() as i64)); Array1::from_iter(predictions_data.iter().map(|&x| x.round() as i64));
Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, predictions)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, predictions)?.unbind()))
} }
/// Predict class probabilities /// Predict class probabilities
@@ -251,7 +251,7 @@ impl DecisionTreeClassifier {
)) ))
})?; })?;
Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, probabilities)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, probabilities)?.unbind()))
} }
/// Score the model on test data /// Score the model on test data
@@ -259,8 +259,11 @@ impl DecisionTreeClassifier {
let predictions = self.predict(x)?; let predictions = self.predict(x)?;
Python::with_gil(|py| { Python::with_gil(|py| {
let pred_array = predictions.as_ref(py).readonly(); use numpy::PyArrayMethods;
let pred_view = ArrayConverter::to_dyn_view(&pred_array); let pred_bound = predictions.bind(py);
let pred_array = pred_bound.readonly();
// Convert Ix1 readonly array view to dynamic view directly
let pred_view = pred_array.as_array();
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?; let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
if pred_view.len() != y_view.len() { if pred_view.len() != y_view.len() {
@@ -281,26 +284,21 @@ impl DecisionTreeClassifier {
/// Get model parameters /// Get model parameters
fn get_params(&self) -> PyResult<HashMap<String, PyObject>> { fn get_params(&self) -> PyResult<HashMap<String, PyObject>> {
use pyo3::IntoPyObjectExt;
Python::with_gil(|py| { Python::with_gil(|py| {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("criterion".to_string(), self.criterion.to_object(py)); params.insert("criterion".to_string(), self.criterion.clone().into_py_any(py)?);
params.insert("max_depth".to_string(), self.max_depth.to_object(py)); params.insert("max_depth".to_string(), self.max_depth.into_py_any(py)?);
params.insert( params.insert("min_samples_split".to_string(), self.min_samples_split.into_py_any(py)?);
"min_samples_split".to_string(), params.insert("min_samples_leaf".to_string(), self.min_samples_leaf.into_py_any(py)?);
self.min_samples_split.to_object(py), params.insert("random_state".to_string(), self.random_state.into_py_any(py)?);
); params.insert("device".to_string(), self.device.to_string().into_py_any(py)?);
params.insert(
"min_samples_leaf".to_string(),
self.min_samples_leaf.to_object(py),
);
params.insert("random_state".to_string(), self.random_state.to_object(py));
params.insert("device".to_string(), self.device.to_string().to_object(py));
Ok(params) Ok(params)
}) })
} }
/// Set model parameters /// Set model parameters
fn set_params(&mut self, params: &PyDict) -> PyResult<()> { fn set_params<'py>(&mut self, params: &Bound<'py, PyDict>) -> PyResult<()> {
let valid_params = vec![ let valid_params = vec![
"criterion", "criterion",
"max_depth", "max_depth",
@@ -343,7 +341,7 @@ impl DecisionTreeClassifier {
match &self.classes { match &self.classes {
Some(classes) => Python::with_gil(|py| { Some(classes) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, classes.clone())?.to_owned(), ArrayConverter::from_array1(py, classes.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -375,7 +373,7 @@ impl DecisionTreeClassifier {
Python::with_gil(|py| { Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, importances)?.to_owned(), ArrayConverter::from_array1(py, importances)?.unbind(),
)) ))
}) })
} }
@@ -2,7 +2,7 @@
Simplified sklearn-compatible clustering wrappers Simplified sklearn-compatible clustering wrappers
*/ */
use ndarray::{Array1, Array2, Axis}; use numpy::ndarray::{Array1, Array2, Axis};
use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn}; use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn};
use pyo3::prelude::*; use pyo3::prelude::*;
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
@@ -190,7 +190,7 @@ impl KMeans {
labels[sample_idx] = best_cluster as i64; labels[sample_idx] = best_cluster as i64;
} }
Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, labels)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, labels)?.unbind()))
} }
fn fit_predict(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray1<i64>>> { fn fit_predict(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray1<i64>>> {
@@ -198,7 +198,7 @@ impl KMeans {
match &self.labels_ { match &self.labels_ {
Some(labels) => Python::with_gil(|py| { Some(labels) => Python::with_gil(|py| {
Ok(ArrayConverter::from_array1(py, labels.clone())?.to_owned()) Ok(ArrayConverter::from_array1(py, labels.clone())?.unbind())
}), }),
None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>( None => Err(PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(
"fit_predict failed: no labels available", "fit_predict failed: no labels available",
@@ -227,16 +227,17 @@ impl KMeans {
} }
} }
Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, distances)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, distances)?.unbind()))
} }
fn get_params(&self) -> PyResult<HashMap<String, PyObject>> { fn get_params(&self) -> PyResult<HashMap<String, PyObject>> {
use pyo3::IntoPyObjectExt;
Python::with_gil(|py| { Python::with_gil(|py| {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert("n_clusters".to_string(), self.n_clusters.to_object(py)); params.insert("n_clusters".to_string(), self.n_clusters.into_py_any(py)?);
params.insert("random_state".to_string(), self.random_state.to_object(py)); params.insert("random_state".to_string(), self.random_state.into_py_any(py)?);
params.insert("max_iter".to_string(), self.max_iter.to_object(py)); params.insert("max_iter".to_string(), self.max_iter.into_py_any(py)?);
params.insert("device".to_string(), self.device.to_string().to_object(py)); params.insert("device".to_string(), self.device.to_string().into_py_any(py)?);
Ok(params) Ok(params)
}) })
} }
@@ -246,7 +247,7 @@ impl KMeans {
match &self.cluster_centers_ { match &self.cluster_centers_ {
Some(centers) => Python::with_gil(|py| { Some(centers) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array2(py, centers.clone())?.to_owned(), ArrayConverter::from_array2(py, centers.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -258,7 +259,7 @@ impl KMeans {
match &self.labels_ { match &self.labels_ {
Some(labels) => Python::with_gil(|py| { Some(labels) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, labels.clone())?.to_owned(), ArrayConverter::from_array1(py, labels.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -299,12 +300,12 @@ impl DBSCAN {
fn fit_predict(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray1<i64>>> { fn fit_predict(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray1<i64>>> {
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?; let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
// Simplified: assign random labels for demonstration // Simplified: assign zero labels for demonstration
let labels = Array1::zeros(x_view.shape()[0]); let labels = Array1::zeros(x_view.shape()[0]);
self.labels_ = Some(labels.clone()); self.labels_ = Some(labels.clone());
self.is_fitted = true; self.is_fitted = true;
Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, labels)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, labels)?.unbind()))
} }
#[getter] #[getter]
@@ -312,7 +313,7 @@ impl DBSCAN {
match &self.labels_ { match &self.labels_ {
Some(labels) => Python::with_gil(|py| { Some(labels) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, labels.clone())?.to_owned(), ArrayConverter::from_array1(py, labels.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -2,7 +2,7 @@
Simplified sklearn-compatible model selection wrappers Simplified sklearn-compatible model selection wrappers
*/ */
use ndarray::{Array1, Array2, Axis}; use numpy::ndarray::{Array1, Array2, Axis};
use numpy::{PyArray1, PyReadonlyArrayDyn}; use numpy::{PyArray1, PyReadonlyArrayDyn};
use pyo3::prelude::*; use pyo3::prelude::*;
use pyo3::types::{PyDict, PyTuple}; use pyo3::types::{PyDict, PyTuple};
@@ -27,7 +27,7 @@ pub struct GridSearchCV {
impl GridSearchCV { impl GridSearchCV {
#[new] #[new]
#[pyo3(signature = (estimator, param_grid, cv=5))] #[pyo3(signature = (estimator, param_grid, cv=5))]
fn new(estimator: PyObject, param_grid: &PyDict, cv: Option<usize>) -> PyResult<Self> { fn new<'py>(estimator: PyObject, param_grid: &Bound<'py, PyDict>, cv: Option<usize>) -> PyResult<Self> {
// Convert param_grid from PyDict to HashMap (simplified) // Convert param_grid from PyDict to HashMap (simplified)
let param_grid_map = HashMap::new(); // Placeholder let param_grid_map = HashMap::new(); // Placeholder
@@ -56,7 +56,7 @@ impl GridSearchCV {
Python::with_gil(|py| { Python::with_gil(|py| {
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?; let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
let dummy_preds = Array1::<f64>::zeros(x_view.shape()[0]); let dummy_preds = Array1::<f64>::zeros(x_view.shape()[0]);
Ok(ArrayConverter::from_array1(py, dummy_preds)?.to_object(py)) Ok(ArrayConverter::from_array1(py, dummy_preds)?.into_any().unbind())
}) })
} }
@@ -66,8 +66,15 @@ impl GridSearchCV {
} }
#[getter] #[getter]
fn best_params_(&self) -> Option<HashMap<String, PyObject>> { fn best_params_(&self) -> PyResult<Option<PyObject>> {
self.best_params_.clone() match &self.best_params_ {
Some(_params) => Python::with_gil(|py| {
let dict = PyDict::new(py);
// Return empty dict for the placeholder
Ok(Some(dict.into_any().unbind()))
}),
None => Ok(None),
}
} }
} }
@@ -88,7 +95,7 @@ pub fn cross_val_score(
// Simplified - return dummy cross-validation scores // Simplified - return dummy cross-validation scores
let scores = Array1::from_vec(vec![0.9, 0.92, 0.88, 0.91, 0.89][..cv_folds].to_vec()); let scores = Array1::from_vec(vec![0.9, 0.92, 0.88, 0.91, 0.89][..cv_folds].to_vec());
Ok(ArrayConverter::from_array1(py, scores)?.to_owned()) Ok(ArrayConverter::from_array1(py, scores)?.unbind())
} }
/// Simplified sklearn-compatible train_test_split function /// Simplified sklearn-compatible train_test_split function
@@ -154,21 +161,16 @@ pub fn train_test_split(
let y_train_1d = Array1::from_vec(y_train.iter().copied().collect()); let y_train_1d = Array1::from_vec(y_train.iter().copied().collect());
let y_test_1d = Array1::from_vec(y_test.iter().copied().collect()); let y_test_1d = Array1::from_vec(y_test.iter().copied().collect());
let x_train_py = ArrayConverter::from_array2(py, x_train_2d)?; let x_train_py = ArrayConverter::from_array2(py, x_train_2d)?.into_any().unbind();
let x_test_py = ArrayConverter::from_array2(py, x_test_2d)?; let x_test_py = ArrayConverter::from_array2(py, x_test_2d)?.into_any().unbind();
let y_train_py = ArrayConverter::from_array1(py, y_train_1d)?; let y_train_py = ArrayConverter::from_array1(py, y_train_1d)?.into_any().unbind();
let y_test_py = ArrayConverter::from_array1(py, y_test_1d)?; let y_test_py = ArrayConverter::from_array1(py, y_test_1d)?.into_any().unbind();
// Return tuple // Return tuple
let result = PyTuple::new( let result = PyTuple::new(
py, py,
&[ &[x_train_py, x_test_py, y_train_py, y_test_py],
x_train_py.to_object(py), )?;
x_test_py.to_object(py),
y_train_py.to_object(py),
y_test_py.to_object(py),
],
);
Ok(result.to_object(py)) Ok(result.into_any().unbind())
} }
@@ -2,7 +2,7 @@
Simplified sklearn-compatible preprocessing wrappers Simplified sklearn-compatible preprocessing wrappers
*/ */
use ndarray::{Array1, Array2, Axis}; use numpy::ndarray::{Array1, Array2, Axis};
use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn}; use numpy::{PyArray1, PyArray2, PyReadonlyArrayDyn};
use pyo3::prelude::*; use pyo3::prelude::*;
@@ -94,7 +94,7 @@ impl StandardScaler {
)) ))
}, },
)?; )?;
Ok(ArrayConverter::from_array2(py, transformed_2d)?.to_owned()) Ok(ArrayConverter::from_array2(py, transformed_2d)?.unbind())
}) })
} }
@@ -144,7 +144,7 @@ impl StandardScaler {
)) ))
}, },
)?; )?;
Ok(ArrayConverter::from_array2(py, transformed_2d)?.to_owned()) Ok(ArrayConverter::from_array2(py, transformed_2d)?.unbind())
}) })
} }
@@ -176,7 +176,7 @@ impl StandardScaler {
e e
)) ))
})?; })?;
Ok(ArrayConverter::from_array2(py, inv_transformed_2d)?.to_owned()) Ok(ArrayConverter::from_array2(py, inv_transformed_2d)?.unbind())
}) })
} }
@@ -185,7 +185,7 @@ impl StandardScaler {
match &self.mean_ { match &self.mean_ {
Some(mean) => Python::with_gil(|py| { Some(mean) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, mean.clone())?.to_owned(), ArrayConverter::from_array1(py, mean.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -197,7 +197,7 @@ impl StandardScaler {
match &self.scale_ { match &self.scale_ {
Some(scale) => Python::with_gil(|py| { Some(scale) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, scale.clone())?.to_owned(), ArrayConverter::from_array1(py, scale.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -282,7 +282,7 @@ impl MinMaxScaler {
} }
} }
Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, transformed)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, transformed)?.unbind()))
} }
fn fit_transform(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray2<f64>>> { fn fit_transform(&mut self, x: PyReadonlyArrayDyn<f64>) -> PyResult<Py<PyArray2<f64>>> {
@@ -320,7 +320,7 @@ impl MinMaxScaler {
} }
} }
Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, transformed)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array2(py, transformed)?.unbind()))
} }
#[getter] #[getter]
@@ -328,7 +328,7 @@ impl MinMaxScaler {
match &self.data_min_ { match &self.data_min_ {
Some(data_min) => Python::with_gil(|py| { Some(data_min) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, data_min.clone())?.to_owned(), ArrayConverter::from_array1(py, data_min.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),
@@ -355,20 +355,20 @@ impl OneHotEncoder {
}) })
} }
fn fit(&mut self, x: &PyAny) -> PyResult<()> { fn fit<'py>(&mut self, _x: &Bound<'py, PyAny>) -> PyResult<()> {
self.is_fitted = true; self.is_fitted = true;
Ok(()) Ok(())
} }
fn transform(&self, x: &PyAny) -> PyResult<Py<PyArray2<f64>>> { fn transform<'py>(&self, _x: &Bound<'py, PyAny>) -> PyResult<Py<PyArray2<f64>>> {
// Placeholder - return identity transform // Placeholder - return identity transform
Python::with_gil(|py| { Python::with_gil(|py| {
let dummy = Array2::eye(2); let dummy = Array2::eye(2);
Ok(ArrayConverter::from_array2(py, dummy)?.to_owned()) Ok(ArrayConverter::from_array2(py, dummy)?.unbind())
}) })
} }
fn fit_transform(&mut self, x: &PyAny) -> PyResult<Py<PyArray2<f64>>> { fn fit_transform<'py>(&mut self, x: &Bound<'py, PyAny>) -> PyResult<Py<PyArray2<f64>>> {
self.fit(x)?; self.fit(x)?;
self.transform(x) self.transform(x)
} }
@@ -392,20 +392,20 @@ impl LabelEncoder {
}) })
} }
fn fit(&mut self, y: &PyAny) -> PyResult<()> { fn fit<'py>(&mut self, _y: &Bound<'py, PyAny>) -> PyResult<()> {
self.is_fitted = true; self.is_fitted = true;
Ok(()) Ok(())
} }
fn transform(&self, y: &PyAny) -> PyResult<Py<PyArray1<i64>>> { fn transform<'py>(&self, _y: &Bound<'py, PyAny>) -> PyResult<Py<PyArray1<i64>>> {
// Placeholder - return dummy labels // Placeholder - return dummy labels
Python::with_gil(|py| { Python::with_gil(|py| {
let dummy = Array1::zeros(10); let dummy = Array1::zeros(10);
Ok(ArrayConverter::from_array1(py, dummy)?.to_owned()) Ok(ArrayConverter::from_array1(py, dummy)?.unbind())
}) })
} }
fn fit_transform(&mut self, y: &PyAny) -> PyResult<Py<PyArray1<i64>>> { fn fit_transform<'py>(&mut self, y: &Bound<'py, PyAny>) -> PyResult<Py<PyArray1<i64>>> {
self.fit(y)?; self.fit(y)?;
self.transform(y) self.transform(y)
} }
@@ -2,8 +2,8 @@
Simplified sklearn-compatible regressor wrappers Simplified sklearn-compatible regressor wrappers
*/ */
use ndarray::Array1; use numpy::ndarray::Array1;
use numpy::{PyArray1, PyReadonlyArrayDyn}; use numpy::{PyArray1, PyReadonlyArrayDyn, PyArrayMethods};
use pyo3::prelude::*; use pyo3::prelude::*;
use std::collections::HashMap; use std::collections::HashMap;
@@ -44,7 +44,7 @@ impl LinearRegression {
ArrayConverter::validate_fit_input(&x, Some(&y)).map_err(|e| PyErr::from(e))?; ArrayConverter::validate_fit_input(&x, Some(&y)).map_err(|e| PyErr::from(e))?;
let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?; let x_view = ArrayConverter::to_array_view2(&x).map_err(|e| PyErr::from(e))?;
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?; let _y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
self.n_features_in = Some(x_view.shape()[1]); self.n_features_in = Some(x_view.shape()[1]);
@@ -81,15 +81,18 @@ impl LinearRegression {
predictions[i] = sum + intercept; predictions[i] = sum + intercept;
} }
Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, predictions)?.to_owned())) Python::with_gil(|py| Ok(ArrayConverter::from_array1(py, predictions)?.unbind()))
} }
fn score(&self, x: PyReadonlyArrayDyn<f64>, y: PyReadonlyArrayDyn<f64>) -> PyResult<f64> { fn score(&self, x: PyReadonlyArrayDyn<f64>, y: PyReadonlyArrayDyn<f64>) -> PyResult<f64> {
let predictions = self.predict(x)?; let predictions = self.predict(x)?;
Python::with_gil(|py| { Python::with_gil(|py| {
let pred_array = predictions.as_ref(py).readonly(); use numpy::PyArrayMethods;
let pred_view = ArrayConverter::to_dyn_view(&pred_array); let pred_bound = predictions.bind(py);
let pred_array = pred_bound.readonly();
// Convert Ix1 readonly array view to ndarray view directly
let pred_view = pred_array.as_array();
let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?; let y_view = ArrayConverter::to_array_view1(&y).map_err(|e| PyErr::from(e))?;
let y_mean = y_view.iter().sum::<f64>() / y_view.len() as f64; let y_mean = y_view.iter().sum::<f64>() / y_view.len() as f64;
@@ -97,7 +100,7 @@ impl LinearRegression {
let ss_res: f64 = pred_view let ss_res: f64 = pred_view
.iter() .iter()
.zip(y_view.iter()) .zip(y_view.iter())
.map(|(pred, actual)| (actual - pred).powi(2)) .map(|(pred, actual)| (actual - pred) as f64 * (actual - pred) as f64)
.sum(); .sum();
let ss_tot: f64 = y_view.iter().map(|actual| (actual - y_mean).powi(2)).sum(); let ss_tot: f64 = y_view.iter().map(|actual| (actual - y_mean).powi(2)).sum();
@@ -107,13 +110,14 @@ impl LinearRegression {
} }
fn get_params(&self) -> PyResult<HashMap<String, PyObject>> { fn get_params(&self) -> PyResult<HashMap<String, PyObject>> {
use pyo3::IntoPyObjectExt;
Python::with_gil(|py| { Python::with_gil(|py| {
let mut params = HashMap::new(); let mut params = HashMap::new();
params.insert( params.insert(
"fit_intercept".to_string(), "fit_intercept".to_string(),
self.fit_intercept.to_object(py), self.fit_intercept.into_py_any(py)?,
); );
params.insert("device".to_string(), self.device.to_string().to_object(py)); params.insert("device".to_string(), self.device.to_string().into_py_any(py)?);
Ok(params) Ok(params)
}) })
} }
@@ -123,7 +127,7 @@ impl LinearRegression {
match &self.coef_ { match &self.coef_ {
Some(coef) => Python::with_gil(|py| { Some(coef) => Python::with_gil(|py| {
Ok(Some( Ok(Some(
ArrayConverter::from_array1(py, coef.clone())?.to_owned(), ArrayConverter::from_array1(py, coef.clone())?.unbind(),
)) ))
}), }),
None => Ok(None), None => Ok(None),