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// Re-export core types from ndarray for convenience
99pub use ndarray::{
100    Array1, Array2, ArrayD, ArrayView1, ArrayView2, ArrayViewD, ArrayViewMut1, ArrayViewMut2,
101    ArrayViewMutD, IxDyn,
102};
103
104// Re-export complex types for convenience
105pub use num_complex::{Complex32, Complex64};
106
107/// Prelude module for convenient imports.
108///
109/// Import all commonly used functions with:
110/// ```
111/// use oxiblas_ndarray::prelude::*;
112/// ```
113pub mod prelude {
114    // Conversions (Array2)
115    pub use crate::conversions::{
116        array_view_mut_to_mat_mut, array_view_to_mat_ref, array_view_to_mat_ref_or_transposed,
117        array_view1_as_slice, array_view1_as_slice_mut, array1_to_vec, array2_into_mat,
118        array2_to_mat, filled_f, is_column_major, is_row_major, mat_ref_to_array2, mat_to_array2,
119        mat_to_array2_c, slice_to_array1, to_column_major, zeros_f,
120    };
121
122    // Conversions (ArrayD - Dynamic dimension)
123    pub use crate::conversions::{
124        array_view_mutd_to_mat_mut, array_viewd_to_mat_ref, array_viewd_to_mat_ref_or_transposed,
125        array2_to_arrayd, arrayd_into_mat, arrayd_to_array2, arrayd_to_mat, mat_ref_to_arrayd,
126        mat_to_arrayd,
127    };
128
129    // BLAS Level 1
130    pub use crate::blas::{
131        asum_ndarray, axpy_ndarray, dot_ndarray, dot_view, nrm2_ndarray, scal_ndarray,
132    };
133
134    // BLAS Level 1 - Complex
135    pub use crate::blas::{
136        asum_c32_ndarray, asum_c64_ndarray, axpy_c32_ndarray, axpy_c64_ndarray, dotc_c32_ndarray,
137        dotc_c64_ndarray, dotu_c32_ndarray, dotu_c64_ndarray, nrm2_c32_ndarray, nrm2_c64_ndarray,
138        scal_c32_ndarray, scal_c64_ndarray,
139    };
140
141    // BLAS Level 2
142    pub use crate::blas::{Transpose, gemv_ndarray, matvec, matvec_t};
143
144    // BLAS Level 3
145    pub use crate::blas::{gemm_ndarray, matmul, matmul_c, matmul_into};
146
147    // Matrix norms
148    pub use crate::blas::{frobenius_norm, norm_1, norm_inf, norm_max};
149
150    // Matrix norms - Complex
151    pub use crate::blas::{
152        frobenius_norm_c32, frobenius_norm_c64, norm_1_c32, norm_1_c64, norm_inf_c32, norm_inf_c64,
153        norm_max_c32, norm_max_c64,
154    };
155
156    // Utilities
157    pub use crate::blas::{eye, eye_f, trace, transpose};
158
159    // Utilities - Complex
160    pub use crate::blas::{
161        conj_transpose_c32, conj_transpose_c64, eye_c32, eye_c64, trace_c32, trace_c64,
162    };
163
164    // LAPACK decompositions
165    pub use crate::lapack::{
166        CholeskyResult, LuResult, QrResult, SvdResult, SymEvdResult, cholesky_ndarray,
167        eig_symmetric, eigvals_symmetric, lu_ndarray, qr_ndarray, svd_ndarray, svd_truncated,
168    };
169
170    // LAPACK - Randomized SVD
171    pub use crate::lapack::{
172        RandomizedSvdResult, low_rank_approx_ndarray, rsvd_ndarray, rsvd_power_ndarray,
173    };
174
175    // LAPACK - Schur decomposition
176    pub use crate::lapack::{SchurResult, schur_ndarray};
177
178    // LAPACK - General eigenvalue decomposition
179    pub use crate::lapack::{Eigenvalue, GeneralEvdResult, eig_ndarray, eigvals_ndarray};
180
181    // LAPACK - Tridiagonal solvers
182    pub use crate::lapack::{
183        tridiag_solve_multiple_ndarray, tridiag_solve_ndarray, tridiag_solve_spd_ndarray,
184    };
185
186    // LAPACK solvers
187    pub use crate::lapack::{lstsq_ndarray, solve_multiple_ndarray, solve_ndarray};
188
189    // Matrix operations
190    pub use crate::lapack::{cond_ndarray, det_ndarray, inv_ndarray, pinv_ndarray, rank_ndarray};
191
192    // Error types
193    pub use crate::lapack::{LapackError, LapackResult};
194}
195
196#[cfg(test)]
197mod tests {
198    use super::prelude::*;
199    use ndarray::{Array2, array};
200
201    #[test]
202    fn test_full_workflow() {
203        // Create matrices
204        let a = Array2::from_shape_fn((3, 3), |(i, j)| (i * 3 + j + 1) as f64);
205        let b = Array2::from_shape_fn((3, 3), |(i, j)| ((i + j) % 3 + 1) as f64);
206
207        // Matrix multiplication
208        let c = matmul(&a, &b);
209        assert_eq!(c.dim(), (3, 3));
210
211        // Matrix-vector multiplication
212        let x = array![1.0f64, 2.0, 3.0];
213        let y = matvec(&a, &x);
214        assert_eq!(y.len(), 3);
215
216        // Norms
217        let fnorm = frobenius_norm(&a);
218        assert!(fnorm > 0.0);
219
220        // Solve linear system
221        let symmetric = array![[4.0f64, 1.0, 0.0], [1.0, 4.0, 1.0], [0.0, 1.0, 4.0]];
222        let rhs = array![5.0f64, 6.0, 5.0];
223        let solution = solve_ndarray(&symmetric, &rhs).unwrap();
224        assert_eq!(solution.len(), 3);
225
226        // Verify solution
227        let residual = matvec(&symmetric, &solution);
228        for i in 0..3 {
229            assert!((residual[i] - rhs[i]).abs() < 1e-10);
230        }
231    }
232
233    #[test]
234    fn test_decomposition_workflow() {
235        let a = array![[4.0f64, 2.0, 1.0], [2.0, 5.0, 2.0], [1.0, 2.0, 4.0]];
236
237        // LU decomposition
238        let lu = lu_ndarray(&a).unwrap();
239        let det = lu.det();
240        assert!(det.abs() > 1e-10); // Non-singular
241
242        // QR decomposition
243        let qr = qr_ndarray(&a).unwrap();
244        assert_eq!(qr.q.dim().0, 3);
245        assert_eq!(qr.r.dim().1, 3);
246
247        // SVD
248        let svd = svd_ndarray(&a).unwrap();
249        assert_eq!(svd.s.len(), 3);
250
251        // Symmetric EVD (a is symmetric)
252        let evd = eig_symmetric(&a).unwrap();
253        assert_eq!(evd.eigenvalues.len(), 3);
254
255        // Cholesky (a is positive definite)
256        let chol = cholesky_ndarray(&a).unwrap();
257        assert_eq!(chol.l.dim(), (3, 3));
258    }
259
260    #[test]
261    fn test_blas_operations() {
262        // Level 1
263        let x = array![1.0f64, 2.0, 3.0];
264        let y = array![4.0f64, 5.0, 6.0];
265
266        let d = dot_ndarray(&x, &y);
267        assert!((d - 32.0).abs() < 1e-10);
268
269        let n = nrm2_ndarray(&x);
270        assert!((n - 14.0f64.sqrt()).abs() < 1e-10);
271
272        let s = asum_ndarray(&x);
273        assert!((s - 6.0).abs() < 1e-10);
274
275        // Level 2
276        let a = array![[1.0f64, 2.0], [3.0, 4.0]];
277        let v = array![1.0f64, 1.0];
278        let result = matvec(&a, &v);
279        assert!((result[0] - 3.0).abs() < 1e-10);
280        assert!((result[1] - 7.0).abs() < 1e-10);
281
282        // Level 3
283        let b = array![[1.0f64, 0.0], [0.0, 1.0]];
284        let c = matmul(&a, &b);
285        for i in 0..2 {
286            for j in 0..2 {
287                assert!((c[[i, j]] - a[[i, j]]).abs() < 1e-10);
288            }
289        }
290    }
291
292    #[test]
293    fn test_column_major_efficiency() {
294        // Column-major arrays should be detected correctly
295        let col_major: Array2<f64> = zeros_f(10, 10);
296        assert!(is_column_major(&col_major));
297
298        let row_major: Array2<f64> = Array2::zeros((10, 10));
299        assert!(!is_column_major(&row_major));
300        assert!(is_row_major(&row_major));
301
302        // Conversion should work
303        let converted = to_column_major(&row_major);
304        assert!(is_column_major(&converted));
305    }
306
307    #[test]
308    fn test_identity_operations() {
309        let id: Array2<f64> = eye(3);
310
311        // Trace of identity = n
312        let tr = trace(&id);
313        assert!((tr - 3.0).abs() < 1e-15);
314
315        // Frobenius norm of identity = sqrt(n)
316        let fnorm = frobenius_norm(&id);
317        assert!((fnorm - 3.0f64.sqrt()).abs() < 1e-15);
318
319        // Determinant of identity = 1
320        let det = det_ndarray(&id).unwrap();
321        assert!((det - 1.0).abs() < 1e-10);
322
323        // Inverse of identity = identity
324        let inv = inv_ndarray(&id).unwrap();
325        for i in 0..3 {
326            for j in 0..3 {
327                let expected = if i == j { 1.0 } else { 0.0 };
328                assert!((inv[[i, j]] - expected).abs() < 1e-10);
329            }
330        }
331
332        // Condition number of identity = 1
333        let cond = cond_ndarray(&id).unwrap();
334        assert!((cond - 1.0).abs() < 1e-10);
335
336        // Rank of identity = n
337        let r = rank_ndarray(&id).unwrap();
338        assert_eq!(r, 3);
339    }
340
341    #[test]
342    fn test_large_matrices() {
343        let n = 100;
344        let a: Array2<f64> = zeros_f(n, n);
345        let mut a = a.mapv(|_| 0.0);
346
347        // Create a well-conditioned matrix
348        for i in 0..n {
349            a[[i, i]] = 10.0;
350            if i > 0 {
351                a[[i, i - 1]] = 1.0;
352            }
353            if i < n - 1 {
354                a[[i, i + 1]] = 1.0;
355            }
356        }
357
358        // Test matrix-vector multiplication
359        let x: ndarray::Array1<f64> = ndarray::Array1::ones(n);
360        let y = matvec(&a, &x);
361        assert_eq!(y.len(), n);
362
363        // Test matrix multiplication
364        let id: Array2<f64> = eye(n);
365        let c = matmul(&a, &id);
366        for i in 0..n {
367            for j in 0..n {
368                assert!((c[[i, j]] - a[[i, j]]).abs() < 1e-10);
369            }
370        }
371    }
372
373    #[test]
374    fn test_numerical_accuracy() {
375        // Test with Hilbert matrix (ill-conditioned)
376        let n = 5;
377        let mut h: Array2<f64> = Array2::zeros((n, n));
378        for i in 0..n {
379            for j in 0..n {
380                h[[i, j]] = 1.0 / ((i + j + 1) as f64);
381            }
382        }
383
384        // SVD should still work
385        let svd = svd_ndarray(&h).unwrap();
386        assert_eq!(svd.s.len(), n);
387
388        // All singular values should be positive
389        for s in svd.s.iter() {
390            assert!(*s > 0.0);
391        }
392
393        // Condition number should be large (ill-conditioned)
394        let cond = cond_ndarray(&h).unwrap();
395        assert!(cond > 1000.0);
396    }
397
398    #[test]
399    fn test_arrayd_integration() {
400        use ndarray::{ArrayD, IxDyn};
401
402        // Create a 2D ArrayD
403        let arr_d = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] * 4 + idx[1]) as f64);
404
405        // Convert to Mat and back
406        let mat = arrayd_to_mat(&arr_d);
407        assert_eq!(mat.shape(), (3, 4));
408
409        let recovered = mat_to_arrayd(&mat);
410        assert_eq!(recovered.shape(), &[3, 4]);
411
412        // Verify values
413        for i in 0..3 {
414            for j in 0..4 {
415                assert!((arr_d[[i, j].as_ref()] - recovered[[i, j].as_ref()]).abs() < 1e-15);
416            }
417        }
418
419        // Convert to Array2 for BLAS operations
420        let arr_2 = arrayd_to_array2(&arr_d);
421        let fnorm = frobenius_norm(&arr_2);
422        assert!(fnorm > 0.0);
423
424        // Convert back to ArrayD
425        let result_d = array2_to_arrayd(&arr_2);
426        assert_eq!(result_d.shape(), &[3, 4]);
427    }
428
429    #[test]
430    fn test_arrayd_matrix_operations() {
431        use ndarray::{ArrayD, IxDyn};
432
433        // Create two ArrayD matrices
434        let a = ArrayD::from_shape_fn(IxDyn(&[3, 4]), |idx| (idx[0] + idx[1] + 1) as f64);
435        let b = ArrayD::from_shape_fn(IxDyn(&[4, 2]), |idx| (idx[0] * idx[1] + 1) as f64);
436
437        // Convert to Array2 for multiplication
438        let a2 = arrayd_to_array2(&a);
439        let b2 = arrayd_to_array2(&b);
440
441        let c2 = matmul(&a2, &b2);
442        assert_eq!(c2.dim(), (3, 2));
443
444        // Convert result back to ArrayD
445        let c_d = array2_to_arrayd(&c2);
446        assert_eq!(c_d.shape(), &[3, 2]);
447    }
448}