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