Skip to main content

oxiblas_ndarray/
lib.rs

1//! OxiBLAS ndarray Integration
2//!
3//! This crate provides seamless integration between OxiBLAS and the `ndarray` crate,
4//! allowing you to use OxiBLAS BLAS and LAPACK operations directly on ndarray types.
5//!
6//! # Features
7//!
8//! - **Conversions**: Efficient conversion between ndarray and OxiBLAS matrix types
9//! - **BLAS Operations**: Level 1-3 BLAS operations (dot, gemv, gemm, etc.)
10//! - **LAPACK Operations**: Decompositions (LU, QR, SVD, EVD, Cholesky)
11//! - **Linear Solve**: Direct and iterative solvers
12//!
13//! # Quick Start
14//!
15//! ```
16//! use ndarray::{array, Array2};
17//! use oxiblas_ndarray::prelude::*;
18//!
19//! // Matrix multiplication
20//! let a = Array2::from_shape_fn((2, 3), |(i, j)| (i * 3 + j + 1) as f64);
21//! let b = Array2::from_shape_fn((3, 2), |(i, j)| (i * 2 + j + 1) as f64);
22//! let c = matmul(&a, &b);
23//! assert_eq!(c.dim(), (2, 2));
24//!
25//! // Matrix-vector multiplication
26//! let x = array![1.0f64, 2.0, 3.0];
27//! let y = matvec(&a, &x);
28//! assert_eq!(y.len(), 2);
29//!
30//! // Dot product
31//! let v1 = array![1.0f64, 2.0, 3.0];
32//! let v2 = array![4.0f64, 5.0, 6.0];
33//! let d = dot_ndarray(&v1, &v2);
34//! assert!((d - 32.0).abs() < 1e-10);
35//! ```
36//!
37//! # LAPACK Operations
38//!
39//! ```
40//! use ndarray::array;
41//! use oxiblas_ndarray::prelude::*;
42//!
43//! // Solve linear system
44//! let a = array![[2.0f64, 1.0], [1.0, 3.0]];
45//! let b = array![5.0f64, 7.0];
46//! let x = solve_ndarray(&a, &b).unwrap();
47//!
48//! // LU decomposition
49//! let lu = lu_ndarray(&a).unwrap();
50//! let det = lu.det();  // Determinant
51//!
52//! // QR decomposition
53//! let qr = qr_ndarray(&a).unwrap();
54//!
55//! // SVD
56//! let svd = svd_ndarray(&a).unwrap();
57//!
58//! // Symmetric eigenvalue decomposition
59//! let evd = eig_symmetric(&a).unwrap();
60//! ```
61//!
62//! # Memory Layout
63//!
64//! OxiBLAS uses column-major (Fortran) order internally. This crate handles
65//! both row-major and column-major ndarray layouts:
66//!
67//! - **Column-major arrays**: Zero-copy or minimal-copy operations
68//! - **Row-major arrays**: Automatic conversion (with copy) when needed
69//!
70//! For best performance, use column-major arrays when possible:
71//!
72//! ```
73//! use ndarray::{Array2, ShapeBuilder};
74//! use oxiblas_ndarray::prelude::*;
75//!
76//! // Create column-major array (preferred)
77//! let a: Array2<f64> = zeros_f(100, 100);
78//! assert!(is_column_major(&a));
79//!
80//! // Or convert existing row-major array
81//! let row_major = Array2::<f64>::zeros((100, 100));
82//! let col_major = to_column_major(&row_major);
83//! ```
84
85#![warn(missing_docs)]
86#![warn(clippy::all)]
87#![allow(clippy::module_name_repetitions)]
88#![allow(clippy::must_use_candidate)]
89// Loop index variables are common in matrix operations
90#![allow(clippy::needless_range_loop)]
91// Bounds in two places for clarity
92#![allow(clippy::multiple_bound_locations)]
93
94pub mod blas;
95pub mod conversions;
96pub mod lapack;
97
98#[cfg(feature = "parallel")]
99pub mod parallel;
100
101#[cfg(feature = "sparse")]
102pub mod sparse;
103
104// Re-export core types from ndarray for convenience
105pub use ndarray::{
106    Array1, Array2, ArrayD, ArrayView1, ArrayView2, ArrayViewD, ArrayViewMut1, ArrayViewMut2,
107    ArrayViewMutD, IxDyn,
108};
109
110// Re-export complex types for convenience
111pub use num_complex::{Complex32, Complex64};
112
113/// Prelude module for convenient imports.
114///
115/// Import all commonly used functions with:
116/// ```
117/// use oxiblas_ndarray::prelude::*;
118/// ```
119pub mod prelude {
120    // Conversions (Array2)
121    pub use crate::conversions::{
122        array_view_mut_to_mat_mut, array_view_to_mat_ref, array_view_to_mat_ref_or_transposed,
123        array_view1_as_slice, array_view1_as_slice_mut, array1_to_vec, array2_into_mat,
124        array2_to_mat, filled_f, is_column_major, is_row_major, mat_ref_to_array2, mat_to_array2,
125        mat_to_array2_c, slice_to_array1, to_column_major, zeros_f,
126    };
127
128    // Conversions (ArrayD - Dynamic dimension)
129    pub use crate::conversions::{
130        array_view_mutd_to_mat_mut, array_viewd_to_mat_ref, array_viewd_to_mat_ref_or_transposed,
131        array2_to_arrayd, arrayd_into_mat, arrayd_to_array2, arrayd_to_mat, mat_ref_to_arrayd,
132        mat_to_arrayd,
133    };
134
135    // BLAS Level 1
136    pub use crate::blas::{
137        asum_ndarray, axpy_ndarray, dot_ndarray, dot_view, nrm2_ndarray, scal_ndarray,
138    };
139
140    // BLAS Level 1 - Complex
141    pub use crate::blas::{
142        asum_c32_ndarray, asum_c64_ndarray, axpy_c32_ndarray, axpy_c64_ndarray, dotc_c32_ndarray,
143        dotc_c64_ndarray, dotu_c32_ndarray, dotu_c64_ndarray, nrm2_c32_ndarray, nrm2_c64_ndarray,
144        scal_c32_ndarray, scal_c64_ndarray,
145    };
146
147    // BLAS Level 2
148    pub use crate::blas::{Transpose, gemv_ndarray, matvec, matvec_t};
149
150    // BLAS Level 3
151    pub use crate::blas::{gemm_ndarray, matmul, matmul_c, matmul_into};
152
153    // Matrix norms
154    pub use crate::blas::{frobenius_norm, norm_1, norm_inf, norm_max};
155
156    // Matrix norms - Complex
157    pub use crate::blas::{
158        frobenius_norm_c32, frobenius_norm_c64, norm_1_c32, norm_1_c64, norm_inf_c32, norm_inf_c64,
159        norm_max_c32, norm_max_c64,
160    };
161
162    // Utilities
163    pub use crate::blas::{eye, eye_f, trace, transpose};
164
165    // Utilities - Complex
166    pub use crate::blas::{
167        conj_transpose_c32, conj_transpose_c64, eye_c32, eye_c64, trace_c32, trace_c64,
168    };
169
170    // LAPACK decompositions
171    pub use crate::lapack::{
172        CholeskyResult, LuResult, QrResult, SvdResult, SymEvdResult, cholesky_ndarray,
173        eig_symmetric, eigvals_symmetric, lu_ndarray, qr_ndarray, svd_ndarray, svd_truncated,
174    };
175
176    // LAPACK - Randomized SVD
177    pub use crate::lapack::{
178        RandomizedSvdResult, low_rank_approx_ndarray, rsvd_ndarray, rsvd_power_ndarray,
179    };
180
181    // LAPACK - Schur decomposition
182    pub use crate::lapack::{SchurResult, schur_ndarray};
183
184    // LAPACK - General eigenvalue decomposition
185    pub use crate::lapack::{Eigenvalue, GeneralEvdResult, eig_ndarray, eigvals_ndarray};
186
187    // LAPACK - Tridiagonal solvers
188    pub use crate::lapack::{
189        tridiag_solve_multiple_ndarray, tridiag_solve_ndarray, tridiag_solve_spd_ndarray,
190    };
191
192    // LAPACK solvers
193    pub use crate::lapack::{lstsq_ndarray, solve_multiple_ndarray, solve_ndarray};
194
195    // Matrix operations
196    pub use crate::lapack::{cond_ndarray, det_ndarray, inv_ndarray, pinv_ndarray, rank_ndarray};
197
198    // Error types
199    pub use crate::lapack::{LapackError, LapackResult};
200
201    // Parallel BLAS operations
202    #[cfg(feature = "parallel")]
203    pub use crate::parallel::{gemm_par_ndarray, matmul_par};
204
205    // Sparse integration
206    #[cfg(feature = "sparse")]
207    pub use crate::sparse::{
208        SparseNdarrayError, array2_to_csc, array2_to_csr, csc_to_array2, csr_to_array2,
209        sparse_solve_ndarray, sparse_solve_ndarray_with_options, spmv_full_ndarray, spmv_ndarray,
210    };
211}
212
213#[cfg(test)]
214mod tests {
215    use super::prelude::*;
216    use ndarray::{Array2, array};
217
218    #[test]
219    fn test_full_workflow() {
220        // Create matrices
221        let a = Array2::from_shape_fn((3, 3), |(i, j)| (i * 3 + j + 1) as f64);
222        let b = Array2::from_shape_fn((3, 3), |(i, j)| ((i + j) % 3 + 1) as f64);
223
224        // Matrix multiplication
225        let c = matmul(&a, &b);
226        assert_eq!(c.dim(), (3, 3));
227
228        // Matrix-vector multiplication
229        let x = array![1.0f64, 2.0, 3.0];
230        let y = matvec(&a, &x);
231        assert_eq!(y.len(), 3);
232
233        // Norms
234        let fnorm = frobenius_norm(&a);
235        assert!(fnorm > 0.0);
236
237        // Solve linear system
238        let symmetric = array![[4.0f64, 1.0, 0.0], [1.0, 4.0, 1.0], [0.0, 1.0, 4.0]];
239        let rhs = array![5.0f64, 6.0, 5.0];
240        let solution = solve_ndarray(&symmetric, &rhs).unwrap();
241        assert_eq!(solution.len(), 3);
242
243        // Verify solution
244        let residual = matvec(&symmetric, &solution);
245        for i in 0..3 {
246            assert!((residual[i] - rhs[i]).abs() < 1e-10);
247        }
248    }
249
250    #[test]
251    fn test_decomposition_workflow() {
252        let a = array![[4.0f64, 2.0, 1.0], [2.0, 5.0, 2.0], [1.0, 2.0, 4.0]];
253
254        // LU decomposition
255        let lu = lu_ndarray(&a).unwrap();
256        let det = lu.det();
257        assert!(det.abs() > 1e-10); // Non-singular
258
259        // QR decomposition
260        let qr = qr_ndarray(&a).unwrap();
261        assert_eq!(qr.q.dim().0, 3);
262        assert_eq!(qr.r.dim().1, 3);
263
264        // SVD
265        let svd = svd_ndarray(&a).unwrap();
266        assert_eq!(svd.s.len(), 3);
267
268        // Symmetric EVD (a is symmetric)
269        let evd = eig_symmetric(&a).unwrap();
270        assert_eq!(evd.eigenvalues.len(), 3);
271
272        // Cholesky (a is positive definite)
273        let chol = cholesky_ndarray(&a).unwrap();
274        assert_eq!(chol.l.dim(), (3, 3));
275    }
276
277    #[test]
278    fn test_blas_operations() {
279        // Level 1
280        let x = array![1.0f64, 2.0, 3.0];
281        let y = array![4.0f64, 5.0, 6.0];
282
283        let d = dot_ndarray(&x, &y);
284        assert!((d - 32.0).abs() < 1e-10);
285
286        let n = nrm2_ndarray(&x);
287        assert!((n - 14.0f64.sqrt()).abs() < 1e-10);
288
289        let s = asum_ndarray(&x);
290        assert!((s - 6.0).abs() < 1e-10);
291
292        // Level 2
293        let a = array![[1.0f64, 2.0], [3.0, 4.0]];
294        let v = array![1.0f64, 1.0];
295        let result = matvec(&a, &v);
296        assert!((result[0] - 3.0).abs() < 1e-10);
297        assert!((result[1] - 7.0).abs() < 1e-10);
298
299        // Level 3
300        let b = array![[1.0f64, 0.0], [0.0, 1.0]];
301        let c = matmul(&a, &b);
302        for i in 0..2 {
303            for j in 0..2 {
304                assert!((c[[i, j]] - a[[i, j]]).abs() < 1e-10);
305            }
306        }
307    }
308
309    #[test]
310    fn test_column_major_efficiency() {
311        // Column-major arrays should be detected correctly
312        let col_major: Array2<f64> = zeros_f(10, 10);
313        assert!(is_column_major(&col_major));
314
315        let row_major: Array2<f64> = Array2::zeros((10, 10));
316        assert!(!is_column_major(&row_major));
317        assert!(is_row_major(&row_major));
318
319        // Conversion should work
320        let converted = to_column_major(&row_major);
321        assert!(is_column_major(&converted));
322    }
323
324    #[test]
325    fn test_identity_operations() {
326        let id: Array2<f64> = eye(3);
327
328        // Trace of identity = n
329        let tr = trace(&id);
330        assert!((tr - 3.0).abs() < 1e-15);
331
332        // Frobenius norm of identity = sqrt(n)
333        let fnorm = frobenius_norm(&id);
334        assert!((fnorm - 3.0f64.sqrt()).abs() < 1e-15);
335
336        // Determinant of identity = 1
337        let det = det_ndarray(&id).unwrap();
338        assert!((det - 1.0).abs() < 1e-10);
339
340        // Inverse of identity = identity
341        let inv = inv_ndarray(&id).unwrap();
342        for i in 0..3 {
343            for j in 0..3 {
344                let expected = if i == j { 1.0 } else { 0.0 };
345                assert!((inv[[i, j]] - expected).abs() < 1e-10);
346            }
347        }
348
349        // Condition number of identity = 1
350        let cond = cond_ndarray(&id).unwrap();
351        assert!((cond - 1.0).abs() < 1e-10);
352
353        // Rank of identity = n
354        let r = rank_ndarray(&id).unwrap();
355        assert_eq!(r, 3);
356    }
357
358    #[test]
359    fn test_large_matrices() {
360        let n = 100;
361        let a: Array2<f64> = zeros_f(n, n);
362        let mut a = a.mapv(|_| 0.0);
363
364        // Create a well-conditioned matrix
365        for i in 0..n {
366            a[[i, i]] = 10.0;
367            if i > 0 {
368                a[[i, i - 1]] = 1.0;
369            }
370            if i < n - 1 {
371                a[[i, i + 1]] = 1.0;
372            }
373        }
374
375        // Test matrix-vector multiplication
376        let x: ndarray::Array1<f64> = ndarray::Array1::ones(n);
377        let y = matvec(&a, &x);
378        assert_eq!(y.len(), n);
379
380        // Test matrix multiplication
381        let id: Array2<f64> = eye(n);
382        let c = matmul(&a, &id);
383        for i in 0..n {
384            for j in 0..n {
385                assert!((c[[i, j]] - a[[i, j]]).abs() < 1e-10);
386            }
387        }
388    }
389
390    #[test]
391    fn test_numerical_accuracy() {
392        // Test with Hilbert matrix (ill-conditioned)
393        let n = 5;
394        let mut h: Array2<f64> = Array2::zeros((n, n));
395        for i in 0..n {
396            for j in 0..n {
397                h[[i, j]] = 1.0 / ((i + j + 1) as f64);
398            }
399        }
400
401        // SVD should still work
402        let svd = svd_ndarray(&h).unwrap();
403        assert_eq!(svd.s.len(), n);
404
405        // All singular values should be positive
406        for s in svd.s.iter() {
407            assert!(*s > 0.0);
408        }
409
410        // Condition number should be large (ill-conditioned)
411        let cond = cond_ndarray(&h).unwrap();
412        assert!(cond > 1000.0);
413    }
414
415    #[test]
416    fn test_arrayd_integration() {
417        use ndarray::{ArrayD, IxDyn};
418
419        // Create a 2D ArrayD
420        let arr_d = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
421
422        // Convert to Mat and back
423        let mat = arrayd_to_mat(&arr_d);
424        assert_eq!(mat.shape(), (3, 4));
425
426        let recovered = mat_to_arrayd(&mat);
427        assert_eq!(recovered.shape(), &[3, 4]);
428
429        // Verify values
430        for i in 0..3 {
431            for j in 0..4 {
432                assert!((arr_d[[i, j].as_ref()] - recovered[[i, j].as_ref()]).abs() < 1e-15);
433            }
434        }
435
436        // Convert to Array2 for BLAS operations
437        let arr_2 = arrayd_to_array2(&arr_d);
438        let fnorm = frobenius_norm(&arr_2);
439        assert!(fnorm > 0.0);
440
441        // Convert back to ArrayD
442        let result_d = array2_to_arrayd(&arr_2);
443        assert_eq!(result_d.shape(), &[3, 4]);
444    }
445
446    #[test]
447    fn test_arrayd_matrix_operations() {
448        use ndarray::{ArrayD, IxDyn};
449
450        // Create two ArrayD matrices
451        let a = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] + idx[1] + 1) as f64);
452        let b = ArrayD::from_shape_fn(IxDyn(&[4, 2]), |idx| (idx[0] * idx[1] + 1) as f64);
453
454        // Convert to Array2 for multiplication
455        let a2 = arrayd_to_array2(&a);
456        let b2 = arrayd_to_array2(&b);
457
458        let c2 = matmul(&a2, &b2);
459        assert_eq!(c2.dim(), (3, 2));
460
461        // Convert result back to ArrayD
462        let c_d = array2_to_arrayd(&c2);
463        assert_eq!(c_d.shape(), &[3, 2]);
464    }
465}