Least Square Approx

/// Least Square Approximation <p>
/// Function that returns a polynomial which very closely passes through the given points (in 2D)
///
/// The result is made of coeficients, in descending order (from x^degree to free term)
///
/// Parameters:
///
/// points -> coordinates of given points
///
/// degree -> degree of the polynomial
///
pub fn least_square_approx<T: Into<f64> + Copy, U: Into<f64> + Copy>(
    points: &[(T, U)],
    degree: i32,
) -> Option<Vec<f64>> {
    use nalgebra::{DMatrix, DVector};

    /* Used for rounding floating numbers */
    fn round_to_decimals(value: f64, decimals: i32) -> f64 {
        let multiplier = 10f64.powi(decimals);
        (value * multiplier).round() / multiplier
    }

    /* Casting the data parsed to this function to f64 (as some points can have decimals) */
    let vals: Vec<(f64, f64)> = points
        .iter()
        .map(|(x, y)| ((*x).into(), (*y).into()))
        .collect();
    /* Because of collect we need the Copy Trait for T and U */

    /* Computes the sums in the system of equations */
    let mut sums = Vec::<f64>::new();
    for i in 1..=(2 * degree + 1) {
        sums.push(vals.iter().map(|(x, _)| x.powi(i - 1)).sum());
    }

    /* Compute the free terms column vector */
    let mut free_col = Vec::<f64>::new();
    for i in 1..=(degree + 1) {
        free_col.push(vals.iter().map(|(x, y)| y * (x.powi(i - 1))).sum());
    }
    let b = DVector::from_row_slice(&free_col);

    /* Create and fill the system's matrix */
    let size = (degree + 1) as usize;
    let a = DMatrix::from_fn(size, size, |i, j| sums[degree as usize + i - j]);

    /* Solve the system of equations: A * x = b */
    match a.qr().solve(&b) {
        Some(x) => {
            let rez: Vec<f64> = x.iter().map(|x| round_to_decimals(*x, 5)).collect();
            Some(rez)
        }
        None => None, //<-- The system cannot be solved (badly conditioned system's matrix)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn ten_points_1st_degree() {
        let points = vec![
            (5.3, 7.8),
            (4.9, 8.1),
            (6.1, 6.9),
            (4.7, 8.3),
            (6.5, 7.7),
            (5.6, 7.0),
            (5.8, 8.2),
            (4.5, 8.0),
            (6.3, 7.2),
            (5.1, 8.4),
        ];

        assert_eq!(
            least_square_approx(&points, 1),
            Some(vec![-0.49069, 10.44898])
        );
    }

    #[test]
    fn eight_points_5th_degree() {
        let points = vec![
            (4f64, 8f64),
            (8f64, 2f64),
            (1f64, 7f64),
            (10f64, 3f64),
            (11.0, 0.0),
            (7.0, 3.0),
            (10.0, 1.0),
            (13.0, 13.0),
        ];

        assert_eq!(
            least_square_approx(&points, 5),
            Some(vec![
                0.00603, -0.21304, 2.79929, -16.53468, 40.29473, -19.35771
            ])
        );
    }

    #[test]
    fn four_points_2nd_degree() {
        let points = vec![
            (2.312, 8.345344),
            (-2.312, 8.345344),
            (-0.7051, 3.49716601),
            (0.7051, 3.49716601),
        ];

        assert_eq!(least_square_approx(&points, 2), Some(vec![1.0, 0.0, 3.0]));
    }
}