Skip to main content

lstsq/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2
3//! Return the least-squares solution to a linear matrix equation
4//!
5//! The crate implements the linear least squares solution to a linear matrix
6//! equation.
7//!
8//! Characteristics:
9//!
10//! * Linear algebra and types from the [`nalgebra`](https://docs.rs/nalgebra)
11//!   crate.
12//! * Maximum compatibility with the
13//!   [`numpy.linalg.lstsq`](https://numpy.org/doc/stable/reference/generated/numpy.linalg.lstsq.html)
14//!   Python library function.
15//! * No standard library is required (disable the default features to disable
16//!   use of `std`) and no heap allocations. In other words, this can run on a
17//!   bare-metal microcontroller with no OS.
18//!
19//! Example:
20//!
21//! ```rust
22//! use nalgebra::{self as na, OMatrix, OVector, U2};
23//!
24//! let a = OMatrix::<f64, na::Dyn, U2>::from_row_slice(&[
25//!     1.0, 1.0,
26//!     2.0, 1.0,
27//!     3.0, 1.0,
28//!     4.0, 1.0,
29//! ]);
30//!
31//! let b = OVector::<f64, na::Dyn>::from_row_slice(&[2.5, 4.4, 6.6, 8.5]);
32//!
33//! let epsilon = 1e-14;
34//! let results = lstsq::lstsq(&a, &b, epsilon).unwrap();
35//!
36//! assert_eq!(results.solution.nrows(), 2);
37//! approx::assert_relative_eq!(results.solution[0], 2.02, epsilon = epsilon);
38//! approx::assert_relative_eq!(results.solution[1], 0.45, epsilon = epsilon);
39//! approx::assert_relative_eq!(results.residuals, 0.018, epsilon = epsilon);
40//! assert_eq!(results.rank, 2);
41//! ```
42
43use nalgebra::allocator::Allocator;
44use nalgebra::base::{OMatrix, OVector};
45use nalgebra::dimension::{Dim, DimDiff, DimMin, DimMinimum, DimSub, U1};
46use nalgebra::{DefaultAllocator, RealField};
47
48/// Results of [lstsq]
49pub struct Lstsq<R: RealField, N: Dim>
50where
51    DefaultAllocator: Allocator<N>,
52{
53    /// Least-squares solution.
54    ///
55    /// This is the variable `x` that approximatively solves the equation `a * x = b`.
56    pub solution: OVector<R, N>,
57    /// Sums of squared residuals: Squared Euclidean 2-norm.
58    pub residuals: R,
59    /// Rank of matrix `a`.
60    pub rank: usize,
61}
62
63/// Return the least-squares solution to a linear matrix equation.
64///
65/// Computes the vector x that approximatively solves the equation `a * x = b`.
66/// Usage is maximally compatible with Python's `numpy.linalg.lstsq`.
67///
68/// Arguments:
69///
70/// - `a`: "Coefficient" matrix (shape: M rows, N columns)
71/// - `b`: Ordinate or “dependent variable” values (shape: M dimensional)
72/// - `epsilon`: singular values less than this are assumed to be zero.
73///
74/// Returns:
75/// - `Result<`[Lstsq]`,&'static str>`
76///
77/// See the module level documentation for example of usage.
78pub fn lstsq<R, M, N>(
79    a: &OMatrix<R, M, N>,
80    b: &OVector<R, M>,
81    epsilon: R,
82) -> Result<Lstsq<R, N>, &'static str>
83where
84    R: RealField,
85    M: DimMin<N>,
86    N: Dim,
87    DimMinimum<M, N>: DimSub<U1>, // for Bidiagonal.
88    DefaultAllocator: Allocator<M, N>
89        + Allocator<N>
90        + Allocator<M>
91        + Allocator<DimDiff<DimMinimum<M, N>, U1>>
92        + Allocator<DimMinimum<M, N>, N>
93        + Allocator<M, DimMinimum<M, N>>
94        + Allocator<DimMinimum<M, N>>,
95{
96    // calculate solution with epsilon
97    let svd = nalgebra::linalg::SVD::new(a.clone(), true, true);
98    let solution = svd.solve(b, epsilon.clone())?;
99
100    // calculate residuals
101    let model: OVector<R, M> = a * &solution;
102    let l1: OVector<R, M> = model - b;
103    let residuals: R = l1.dot(&l1);
104
105    // calculate rank with epsilon
106    let rank = svd.rank(epsilon);
107
108    Ok(Lstsq {
109        solution,
110        residuals,
111        rank,
112    })
113}
114
115#[cfg(test)]
116mod tests {
117    use crate::lstsq;
118
119    use na::{OMatrix, OVector, RealField, U2};
120    use nalgebra as na;
121
122    fn check_residuals<R: RealField + Copy>(epsilon: R) {
123        /*
124        import numpy as np
125        A = np.array([[1.0, 1.0], [2.0, 1.0], [3.0, 1.0], [4.0, 1.0]])
126        b = np.array([2.5, 4.4, 6.6, 8.5])
127        x,residuals,rank,s = np.linalg.lstsq(A,b)
128        */
129        let a: Vec<R> = vec![1.0, 1.0, 2.0, 1.0, 3.0, 1.0, 4.0, 1.0]
130            .into_iter()
131            .map(na::convert)
132            .collect();
133
134        let a = OMatrix::<R, na::Dyn, U2>::from_row_slice(&a);
135
136        let b_data: Vec<R> = vec![2.5, 4.4, 6.6, 8.5]
137            .into_iter()
138            .map(na::convert)
139            .collect();
140        let b = OVector::<R, na::Dyn>::from_row_slice(&b_data);
141
142        let results = lstsq(&a, &b, R::default_epsilon()).unwrap();
143        assert_eq!(results.solution.nrows(), 2);
144        approx::assert_relative_eq!(results.solution[0], na::convert(2.02), epsilon = epsilon);
145        approx::assert_relative_eq!(results.solution[1], na::convert(0.45), epsilon = epsilon);
146        approx::assert_relative_eq!(results.residuals, na::convert(0.018), epsilon = epsilon);
147        assert_eq!(results.rank, 2);
148    }
149
150    #[test]
151    fn test_residuals_f64() {
152        check_residuals::<f64>(1e-14)
153    }
154
155    #[test]
156    fn test_residuals_f32() {
157        check_residuals::<f32>(1e-5)
158    }
159}