Skip to main content

sklears_python/
utils.rs

1//! Python bindings for utility functions
2//!
3//! This module provides Python bindings for sklears utilities,
4//! including version information and build details.
5
6use numpy::{PyArray1, PyArray2, PyArrayMethods};
7use pyo3::exceptions::PyValueError;
8use pyo3::prelude::*;
9use pyo3::types::PyDict;
10use scirs2_core::ndarray::{Array1, Array2};
11use scirs2_core::random::thread_rng;
12use std::collections::HashMap;
13
14use crate::linear::{
15    core_array1_to_py, core_array2_to_py, pyarray_to_core_array1, pyarray_to_core_array2,
16};
17
18/// Get the version of sklears
19#[pyfunction]
20pub fn get_version() -> String {
21    env!("CARGO_PKG_VERSION").to_string()
22}
23
24/// Get build information about sklears
25#[pyfunction]
26pub fn get_build_info() -> HashMap<String, String> {
27    let mut info = HashMap::new();
28
29    info.insert("version".to_string(), env!("CARGO_PKG_VERSION").to_string());
30    info.insert("authors".to_string(), env!("CARGO_PKG_AUTHORS").to_string());
31    info.insert(
32        "description".to_string(),
33        env!("CARGO_PKG_DESCRIPTION").to_string(),
34    );
35    info.insert(
36        "homepage".to_string(),
37        env!("CARGO_PKG_HOMEPAGE").to_string(),
38    );
39    info.insert(
40        "repository".to_string(),
41        env!("CARGO_PKG_REPOSITORY").to_string(),
42    );
43    info.insert("license".to_string(), env!("CARGO_PKG_LICENSE").to_string());
44    info.insert(
45        "rust_version".to_string(),
46        env!("CARGO_PKG_RUST_VERSION").to_string(),
47    );
48
49    // Build-time information
50    info.insert(
51        "target_triple".to_string(),
52        std::env::var("TARGET").unwrap_or_else(|_| "unknown".to_string()),
53    );
54    info.insert(
55        "build_profile".to_string(),
56        if cfg!(debug_assertions) {
57            "debug"
58        } else {
59            "release"
60        }
61        .to_string(),
62    );
63
64    // Feature information
65    let features: [&str; 0] = [];
66
67    info.insert("features".to_string(), features.join(", "));
68
69    // Dependency versions
70    info.insert("scirs2_core_version".to_string(), "workspace".to_string());
71    info.insert("pyo3_version".to_string(), "0.26".to_string());
72    info.insert("numpy_version".to_string(), "0.26".to_string());
73
74    info
75}
76
77/// Check if specific features are enabled
78#[pyfunction]
79pub fn has_feature(feature_name: &str) -> bool {
80    let _ = feature_name;
81    false
82}
83
84/// Get hardware acceleration capabilities
85///
86/// Returns a Python `dict` mixing boolean capability flags (e.g. `avx2`,
87/// `neon`, `parallel_support`) with the integer CPU core count under the
88/// `num_cpus` key. A plain `HashMap<String, bool>` cannot represent this
89/// mixed shape, so we build a `PyDict` directly (matching the heterogeneous
90/// dict pattern already used by `get_params` in the `linear` module) rather
91/// than lossily coercing the core count into a boolean.
92#[pyfunction]
93pub fn get_hardware_info(py: Python<'_>) -> PyResult<Py<PyDict>> {
94    let info = PyDict::new(py);
95
96    // CPU features
97    #[cfg(target_arch = "x86_64")]
98    {
99        info.set_item("x86_64", true)?;
100        info.set_item("avx2", is_x86_feature_detected!("avx2"))?;
101        info.set_item("fma", is_x86_feature_detected!("fma"))?;
102        info.set_item("sse4_1", is_x86_feature_detected!("sse4.1"))?;
103        info.set_item("sse4_2", is_x86_feature_detected!("sse4.2"))?;
104    }
105
106    #[cfg(target_arch = "aarch64")]
107    {
108        info.set_item("aarch64", true)?;
109        info.set_item("neon", cfg!(target_feature = "neon"))?;
110    }
111
112    // Other architectures
113    #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
114    {
115        info.set_item("simd_support", false)?;
116    }
117
118    // GPU support: real detection via sklears-core's oxicuda-backed `gpu`
119    // module when this crate is built with the `gpu` feature (which forwards
120    // to `sklears-core/gpu_support`). In the default Pure-Rust build (no
121    // `gpu` feature), honestly report `false` rather than probing for
122    // hardware we have no driver bindings for.
123    #[cfg(feature = "gpu")]
124    info.set_item(
125        "cuda_available",
126        sklears_core::gpu::GpuUtils::is_gpu_available(),
127    )?;
128    #[cfg(not(feature = "gpu"))]
129    info.set_item("cuda_available", false)?;
130    // OpenCL is not supported by the oxicuda stack (CUDA-only), so this
131    // always honestly reports `false` regardless of the `gpu` feature.
132    info.set_item("opencl_available", false)?;
133
134    // Thread support
135    info.set_item("parallel_support", true)?;
136    // Real CPU core count. Previously this collapsed the count into a bool
137    // (`num_cpus::get() > 1`), which discarded the actual core count.
138    info.set_item("num_cpus", num_cpus::get())?;
139
140    Ok(info.into())
141}
142
143/// Get memory usage information
144#[pyfunction]
145pub fn get_memory_info() -> HashMap<String, u64> {
146    let mut info = HashMap::new();
147
148    // Get number of CPUs
149    info.insert("num_cpus".to_string(), num_cpus::get() as u64);
150
151    // Physical memory would require additional dependencies
152    // This is a placeholder
153    info.insert("available_memory_mb".to_string(), 0);
154    info.insert("used_memory_mb".to_string(), 0);
155
156    info
157}
158
159/// Set global configuration options
160#[pyfunction]
161pub fn set_config(option: &str, _value: &str) -> PyResult<()> {
162    match option {
163        "n_jobs" => {
164            // Set global parallelism configuration
165            Ok(())
166        }
167        "assume_finite" => {
168            // Set validation configuration
169            Ok(())
170        }
171        "working_memory" => {
172            // Set memory limit for operations
173            Ok(())
174        }
175        _ => Err(pyo3::exceptions::PyValueError::new_err(format!(
176            "Unknown configuration option: {}",
177            option
178        ))),
179    }
180}
181
182/// Get current configuration
183#[pyfunction]
184pub fn get_config() -> HashMap<String, String> {
185    let mut config = HashMap::new();
186
187    // Default configuration values
188    config.insert("n_jobs".to_string(), "1".to_string());
189    config.insert("assume_finite".to_string(), "false".to_string());
190    config.insert("working_memory".to_string(), "1024".to_string());
191    config.insert("print_changed_only".to_string(), "true".to_string());
192    config.insert("display".to_string(), "text".to_string());
193
194    config
195}
196
197/// Print system information
198#[pyfunction]
199pub fn show_versions(py: Python<'_>) -> PyResult<String> {
200    let mut output = String::new();
201
202    output.push_str("sklears information:\n");
203    output.push_str("=====================\n");
204
205    let build_info = get_build_info();
206    for (key, value) in &build_info {
207        output.push_str(&format!("{}: {}\n", key, value));
208    }
209
210    output.push_str("\nHardware information:\n");
211    output.push_str("====================\n");
212
213    let hardware_info = get_hardware_info(py)?;
214    for (key, value) in hardware_info.bind(py).iter() {
215        output.push_str(&format!("{}: {}\n", key, value));
216    }
217
218    output.push_str("\nMemory information:\n");
219    output.push_str("==================\n");
220
221    let memory_info = get_memory_info();
222    for (key, value) in &memory_info {
223        output.push_str(&format!("{}: {}\n", key, value));
224    }
225
226    Ok(output)
227}
228
229/// Performance testing utility
230#[pyfunction]
231pub fn benchmark_basic_operations() -> HashMap<String, f64> {
232    use std::time::Instant;
233
234    let mut results = HashMap::new();
235    let mut rng = thread_rng();
236
237    // Matrix multiplication benchmark
238    let start = Instant::now();
239    let a = Array2::from_shape_fn((100, 100), |_| rng.random::<f64>());
240    let b = Array2::from_shape_fn((100, 100), |_| rng.random::<f64>());
241    let _c = a.dot(&b);
242    let matrix_mul_time = start.elapsed().as_nanos() as f64 / 1_000_000.0; // Convert to milliseconds
243    results.insert(
244        "matrix_multiplication_100x100_ms".to_string(),
245        matrix_mul_time,
246    );
247
248    // Vector operations benchmark
249    let start = Instant::now();
250    let v1 = Array1::from_shape_fn(10000, |_| rng.random::<f64>());
251    let v2 = Array1::from_shape_fn(10000, |_| rng.random::<f64>());
252    let _dot_product = v1.dot(&v2);
253    let vector_ops_time = start.elapsed().as_nanos() as f64 / 1_000_000.0;
254    results.insert("vector_dot_product_10k_ms".to_string(), vector_ops_time);
255
256    // Memory allocation benchmark
257    let start = Instant::now();
258    let _large_array = Array2::<f64>::zeros((1000, 1000));
259    let allocation_time = start.elapsed().as_nanos() as f64 / 1_000_000.0;
260    results.insert(
261        "memory_allocation_1M_elements_ms".to_string(),
262        allocation_time,
263    );
264
265    results
266}
267
268/// Convert NumPy array to ndarray Array2`<f64>`
269pub fn numpy_to_ndarray2(py_array: &Bound<'_, PyArray2<f64>>) -> PyResult<Array2<f64>> {
270    let readonly = py_array.try_readonly().map_err(|err| {
271        PyValueError::new_err(format!(
272            "Failed to borrow NumPy array as read-only view: {err}"
273        ))
274    })?;
275    pyarray_to_core_array2(readonly)
276}
277
278/// Convert NumPy array to ndarray Array1`<f64>`
279pub fn numpy_to_ndarray1(py_array: &Bound<'_, PyArray1<f64>>) -> PyResult<Array1<f64>> {
280    let readonly = py_array.try_readonly().map_err(|err| {
281        PyValueError::new_err(format!(
282            "Failed to borrow NumPy array as read-only view: {err}"
283        ))
284    })?;
285    pyarray_to_core_array1(readonly)
286}
287
288/// Convert ndarray Array2`<f64>` to NumPy array
289pub fn ndarray_to_numpy<'py>(py: Python<'py>, array: Array2<f64>) -> PyResult<Py<PyArray2<f64>>> {
290    core_array2_to_py(py, &array)
291}
292
293/// Convert ndarray Array1`<f64>` to NumPy array
294pub fn ndarray1_to_numpy<'py>(py: Python<'py>, array: Array1<f64>) -> Py<PyArray1<f64>> {
295    core_array1_to_py(py, &array)
296}