sarah-ek / faer-rs

Linear algebra foundation for the Rust programming language
https://faer-rs.github.io
MIT License
1.79k stars 58 forks source link

Proptest tests #88

Open pnevyk opened 9 months ago

pnevyk commented 9 months ago

Few notes:

sarah-ek commented 9 months ago

i think using epsilon for relative comparisons is too strict.

in fact, the elementwise absolute/relative comparisons might both be unsuitable in this case. what we'd ideally want is to compare the norm of the difference of vectors/matrices.

this is because matrix operations aren't going to act on elements individually. they're going to combine all the elements in different ways

pnevyk commented 9 months ago

I now compare the numbers using absolute difference with a threshold that is relative to the norms and dimensions of the matrices, hope my attempt is how you imagined it. I am sure you will find improvements to the code.

We still need to use custom macro and comparator if we want to support matrices of complex numbers.

The QR and eigendecomposition tests are now passing. The lower triangular still fails, is the failure valid or do I have a bug in the proptest code?

sarah-ek commented 9 months ago

the failure is caused by the bad conditioning of the input matrix. for solving linear systems, the error also depends on the condition number of the matrix we're inverting (in this case the lower triangular one)

given the singular values singvals of an invertible matrix A, the condition number is defined as max(singvals) / min(singvals)

sarah-ek commented 9 months ago

i'm trying to go through the formulas and i think this should be good

norm_l2(Ax - b) <= eps × (r + r × cond(A)) × norm_l2(b)

where r is some relaxation parameter >= 1, again we could try something like 10 or 100

pnevyk commented 9 months ago

Here is my implementation of eps × (r + r × cond(A)) × norm_l2(b)

pub fn relative_epsilon_cond<E>(
    a: impl AsMatRef<E>,
    b: impl AsMatRef<E>,
    threshold: E::Real,
) -> E::Real
where
    E: ComplexField,
{
    let a = a.as_mat_ref();
    let b = b.as_mat_ref();

    let nrows = a.nrows();
    if nrows == 0 {
        return E::Real::faer_zero();
    }

    let svd = Svd::new(a);

    let mut sing_min = E::Real::faer_from_f64(f64::INFINITY);
    let mut sing_max = E::Real::faer_from_f64(0.0);
    zipped!(svd.s_diagonal()).for_each(|unzipped!(sing)| {
        let sing = sing.read().faer_real();

        if sing < sing_min {
            sing_min = sing;
        }

        if sing > sing_max {
            sing_max = sing;
        }
    });

    let a_cond = sing_max.faer_div(sing_min);
    let b_norm = b.norm_l2();

    let eps = E::Real::faer_epsilon().unwrap();

    eps.faer_mul(threshold.faer_add(threshold.faer_mul(a_cond)))
        .faer_mul(b_norm)
}

Now a few questions:

  1. Is my implementation for condition number correct?
  2. If so, could it be somehow improved?
  3. Proptest is able to generate matrices for which there is a singular value equal to 0. What to do in this case?
sarah-ek commented 9 months ago

the implementation is correct, though you can also use Faer::singular_values (https://docs.rs/faer/latest/faer/trait.Faer.html#tymethod.singular_values) instead of computing the full svd, since that's more efficient as for the issue with singular matrices, maybe we can skip the proptests for solving linear systems, and just merge the existing stuff that works for now (qr and eigendecompositions)

pnevyk commented 9 months ago

I polished the proptest strategy implementation a bit. And I commented out the test for solving linear systems. Can you have a final look? I will then resolve the conflict and we can merge it.

sarah-ek commented 9 months ago

yeah, looks good to me!

pnevyk commented 8 months ago

Rebased on the current main. Apologies for the delay.

What would be the next steps in your opinion? I could add more proptest tests (in a separate PR), maybe you would have some good candidates?