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