Skip to main content

scirs2_python/
autograd.rs

1//! Python bindings for scirs2-autograd
2//!
3//! This module provides Python bindings for variable management and model persistence
4//! from scirs2-autograd. Due to Rust lifetime complexities, full computational graph
5//! APIs are not exposed. For comprehensive automatic differentiation, use PyTorch
6//! or TensorFlow which integrate seamlessly with scirs2 via NumPy arrays.
7
8use pyo3::prelude::*;
9use scirs2_autograd::variable::NamespaceTrait;
10use scirs2_autograd::VariableEnvironment;
11use scirs2_numpy::{IntoPyArray, PyArray1, PyArray2, PyArrayMethods};
12
13/// Variable environment for managing trainable parameters
14///
15/// Provides save/load functionality for model persistence. For training,
16/// use PyTorch/TensorFlow and transfer weights via NumPy arrays.
17///
18/// Example:
19///     env = scirs2.VariableEnvironment()
20///     # Set variables using NumPy arrays
21///     var_id = env.set_variable("weights", np.random.randn(784, 128))
22///     # Save trained model
23///     env.save("model.json")
24///     # Load model
25///     loaded_env = scirs2.VariableEnvironment.load("model.json")
26#[pyclass(name = "VariableEnvironment", unsendable)]
27pub struct PyVariableEnvironment {
28    inner: VariableEnvironment<f64>,
29}
30
31#[pymethods]
32impl PyVariableEnvironment {
33    /// Create a new variable environment
34    #[new]
35    fn new() -> Self {
36        Self {
37            inner: VariableEnvironment::new(),
38        }
39    }
40
41    /// Set a named variable from a NumPy array
42    ///
43    /// Args:
44    ///     name (str): Variable name
45    ///     array (np.ndarray): Variable value (1D or 2D float64 array)
46    fn set_variable(&mut self, name: &str, array: &Bound<'_, PyAny>) -> PyResult<()> {
47        // Try 1D array first
48        if let Ok(arr1d) = array.cast::<PyArray1<f64>>() {
49            let binding = arr1d.readonly();
50            let data = binding.as_array().to_owned();
51            self.inner.name(name).set(data);
52            return Ok(());
53        }
54
55        // Try 2D array
56        if let Ok(arr2d) = array.cast::<PyArray2<f64>>() {
57            let binding = arr2d.readonly();
58            let data = binding.as_array().to_owned();
59            self.inner.name(name).set(data);
60            return Ok(());
61        }
62
63        Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
64            "Array must be 1D or 2D float64 numpy array",
65        ))
66    }
67
68    /// Get a named variable as a NumPy array
69    ///
70    /// Args:
71    ///     name (str): Variable name
72    ///
73    /// Returns:
74    ///     np.ndarray: Variable value
75    fn get_variable(&self, py: Python, name: &str) -> PyResult<Py<PyAny>> {
76        let namespace = self.inner.default_namespace();
77        let array_ref = namespace.get_array_by_name(name).ok_or_else(|| {
78            PyErr::new::<pyo3::exceptions::PyKeyError, _>(format!("Variable '{}' not found", name))
79        })?;
80
81        let array = array_ref.borrow();
82
83        // Return based on dimensionality
84        match array.ndim() {
85            1 => {
86                let arr1d = array
87                    .view()
88                    .into_dimensionality::<scirs2_core::ndarray::Ix1>()
89                    .map_err(|e| {
90                        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
91                            "Dimension error: {}",
92                            e
93                        ))
94                    })?;
95                Ok(arr1d.to_owned().into_pyarray(py).unbind().into())
96            }
97            2 => {
98                let arr2d = array
99                    .view()
100                    .into_dimensionality::<scirs2_core::ndarray::Ix2>()
101                    .map_err(|e| {
102                        PyErr::new::<pyo3::exceptions::PyRuntimeError, _>(format!(
103                            "Dimension error: {}",
104                            e
105                        ))
106                    })?;
107                Ok(arr2d.to_owned().into_pyarray(py).unbind().into())
108            }
109            _ => Err(PyErr::new::<pyo3::exceptions::PyValueError, _>(
110                "Only 1D and 2D arrays are currently supported",
111            )),
112        }
113    }
114
115    /// List all variable names in the default namespace
116    ///
117    /// Returns:
118    ///     list: List of variable names
119    fn list_variables(&self) -> Vec<String> {
120        self.inner
121            .default_namespace()
122            .current_var_names()
123            .into_iter()
124            .map(|s: &str| s.to_string())
125            .collect()
126    }
127
128    /// Save the variable environment to a file
129    ///
130    /// Args:
131    ///     path (str): Path to save file (.json format)
132    fn save(&self, path: &str) -> PyResult<()> {
133        self.inner
134            .save(path)
135            .map_err(|e| PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("Save error: {}", e)))
136    }
137
138    /// Load a variable environment from a file
139    ///
140    /// Args:
141    ///     path (str): Path to load file (.json format)
142    ///
143    /// Returns:
144    ///     VariableEnvironment: Loaded environment
145    #[staticmethod]
146    fn load(path: &str) -> PyResult<Self> {
147        let inner = VariableEnvironment::<f64>::load(path).map_err(|e| {
148            PyErr::new::<pyo3::exceptions::PyIOError, _>(format!("Load error: {}", e))
149        })?;
150
151        Ok(Self { inner })
152    }
153
154    /// Get the number of variables in the environment
155    fn __len__(&self) -> usize {
156        self.inner.default_namespace().current_var_names().len()
157    }
158
159    /// String representation
160    fn __repr__(&self) -> String {
161        format!(
162            "VariableEnvironment({} variables)",
163            self.inner.default_namespace().current_var_names().len()
164        )
165    }
166}
167
168// ============================================================================
169// Module Registration
170// ============================================================================
171
172pub fn register_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
173    // Register VariableEnvironment class
174    m.add_class::<PyVariableEnvironment>()?;
175
176    // Add module documentation
177    m.add("__doc__", "Automatic differentiation and variable management\n\n\
178        This module provides model parameter storage and persistence from scirs2-autograd.\n\
179        Due to Rust lifetime complexities in the computational graph API, full autodiff\n\
180        functionality is not exposed. For neural network training, we recommend:\n\n\
181        - PyTorch: Industry-standard deep learning framework\n\
182        - TensorFlow: Comprehensive ML platform\n\n\
183        scirs2 arrays are NumPy-compatible, enabling seamless integration with these frameworks.\n\n\
184        Use VariableEnvironment for:\n\
185        - Storing and managing model parameters\n\
186        - Saving/loading trained model weights\n\
187        - Transferring weights between scirs2 and PyTorch/TensorFlow")?;
188
189    Ok(())
190}