Skip to content

Commit 8cd8b4b

Browse files
authored
Fixes for nalgebra-lapack (faulty test, bugs in implementations, compile errors) (#1541)
* fix bugs in lu and schur decomposition, update tests for singularity detection * improve cholesky tests for better output * make sure in cholesky tests to have strictly positive definite matrices * relax numerical accuracy in cholesky tests * make all cholesky tests use the strictly sym pos def matrices * add nalgebra-lapack tests to CI * fix copy paste error in ci yml * fix serde-serialize bounds * add serde-serialize to CI features for na-lapack * add slow-tests feature * fix unused imports
1 parent 8964bf2 commit 8cd8b4b

File tree

9 files changed

+184
-100
lines changed

9 files changed

+184
-100
lines changed

.github/workflows/nalgebra-ci-build.yml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ jobs:
3636
run: cargo build;
3737
- name: Build --features serde-serialize
3838
run: cargo build --features serde-serialize
39-
- name: Build nalgebra-lapack
40-
run: cd nalgebra-lapack; cargo build;
39+
- name: Check nalgebra-lapack
40+
run: cd nalgebra-lapack; cargo check --features serde-serialize;
4141
- name: Build nalgebra-sparse --no-default-features
4242
run: cd nalgebra-sparse; cargo build --no-default-features;
4343
- name: Build nalgebra-sparse (default features)
@@ -105,6 +105,12 @@ jobs:
105105
- uses: actions/checkout@v4
106106
- name: test nalgebra-macros
107107
run: cargo test -p nalgebra-macros
108+
test-nalgebra-lapack:
109+
runs-on: ubuntu-latest
110+
steps:
111+
- uses: actions/checkout@v4
112+
- name: test nalgebra-lapack
113+
run: cargo test -p nalgebra-lapack --features proptest-support;
108114
build-wasm:
109115
runs-on: ubuntu-latest
110116
# env:

nalgebra-lapack/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ maintenance = { status = "actively-developed" }
2020
serde-serialize = ["serde", "nalgebra/serde-serialize"]
2121
proptest-support = ["nalgebra/proptest-support"]
2222
arbitrary = ["nalgebra/arbitrary"]
23+
slow-tests = []
2324

2425
# For BLAS/LAPACK
2526
default = ["netlib"]

nalgebra-lapack/src/eigen.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use lapack;
2323
#[cfg_attr(
2424
feature = "serde-serialize",
2525
serde(bound(deserialize = "DefaultAllocator: Allocator<D, D> + Allocator<D>,
26-
OVector<T, D>: Serialize,
26+
OVector<T, D>: Deserialize<'de>,
2727
OMatrix<T, D, D>: Deserialize<'de>"))
2828
)]
2929
#[derive(Clone, Debug)]

nalgebra-lapack/src/hessenberg.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#[cfg(feature = "serde-serialize")]
2+
use serde::{Deserialize, Serialize};
3+
14
use num::Zero;
25
use num_complex::Complex;
36

nalgebra-lapack/src/lu.rs

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
#[cfg(feature = "serde-serialize")]
2+
use serde::{Deserialize, Serialize};
3+
14
use num::{One, Zero};
25
use num_complex::Complex;
36

@@ -23,14 +26,14 @@ use lapack;
2326
serde(bound(serialize = "DefaultAllocator: Allocator<R, C> +
2427
Allocator<DimMinimum<R, C>>,
2528
OMatrix<T, R, C>: Serialize,
26-
PermutationSequence<DimMinimum<R, C>>: Serialize"))
29+
OVector<i32, DimMinimum<R, C>>: Serialize"))
2730
)]
2831
#[cfg_attr(
2932
feature = "serde-serialize",
3033
serde(bound(deserialize = "DefaultAllocator: Allocator<R, C> +
3134
Allocator<DimMinimum<R, C>>,
3235
OMatrix<T, R, C>: Deserialize<'de>,
33-
PermutationSequence<DimMinimum<R, C>>: Deserialize<'de>"))
36+
OVector<i32, DimMinimum<R, C>>: Deserialize<'de>"))
3437
)]
3538
#[derive(Clone, Debug)]
3639
pub struct LU<T: Scalar, R: DimMin<C>, C: Dim>
@@ -39,6 +42,9 @@ where
3942
{
4043
lu: OMatrix<T, R, C>,
4144
p: OVector<i32, DimMinimum<R, C>>,
45+
/// the LU decomposition can be computed for singular matrices, but we
46+
/// cannot solve linear systems for singular matrices.
47+
singular: bool,
4248
}
4349

4450
impl<T: Scalar + Copy, R: DimMin<C>, C: Dim> Copy for LU<T, R, C>
@@ -78,9 +84,30 @@ where
7884
ipiv.as_mut_slice(),
7985
&mut info,
8086
);
81-
lapack_panic!(info);
8287

83-
Self { lu: m, p: ipiv }
88+
// @note(geo-ant)
89+
// the lapack documentation for xGETRF states:
90+
//
91+
// !> INFO is INTEGER
92+
// !> = 0: successful exit
93+
// !> < 0: if INFO = -i, the i-th argument had an illegal value
94+
// !> > 0: if INFO = i, U(i,i) is exactly zero. The factorization
95+
// !> has been completed, but the factor U is exactly
96+
// !> singular, and division by zero will occur if it is used
97+
// !> to solve a system of equations.
98+
// !>
99+
//
100+
// This is a bit unusual for lapack routines, so we have to check whether it is
101+
// negative before panicking.
102+
if info < 0 {
103+
lapack_panic!(info);
104+
}
105+
106+
Self {
107+
lu: m,
108+
p: ipiv,
109+
singular: info > 0,
110+
}
84111
}
85112

86113
/// Gets the lower-triangular matrix part of the decomposition.
@@ -155,6 +182,10 @@ where
155182
where
156183
DefaultAllocator: Allocator<R2, C2> + Allocator<R2>,
157184
{
185+
if self.singular {
186+
return false;
187+
}
188+
158189
let dim = self.lu.nrows();
159190

160191
assert!(
@@ -195,7 +226,7 @@ where
195226
DefaultAllocator: Allocator<R2, C2> + Allocator<R2>,
196227
{
197228
let mut res = b.clone_owned();
198-
if self.generic_solve_mut(b'T', &mut res) {
229+
if self.generic_solve_mut(b'N', &mut res) {
199230
Some(res)
200231
} else {
201232
None
@@ -231,7 +262,7 @@ where
231262
DefaultAllocator: Allocator<R2, C2> + Allocator<R2>,
232263
{
233264
let mut res = b.clone_owned();
234-
if self.generic_solve_mut(b'T', &mut res) {
265+
if self.generic_solve_mut(b'C', &mut res) {
235266
Some(res)
236267
} else {
237268
None
@@ -245,7 +276,7 @@ where
245276
where
246277
DefaultAllocator: Allocator<R2, C2> + Allocator<R2>,
247278
{
248-
self.generic_solve_mut(b'T', b)
279+
self.generic_solve_mut(b'N', b)
249280
}
250281

251282
/// Solves in-place the linear system `self.transpose() * x = b`, where `x` is the unknown to be
@@ -267,7 +298,7 @@ where
267298
where
268299
DefaultAllocator: Allocator<R2, C2> + Allocator<R2>,
269300
{
270-
self.generic_solve_mut(b'T', b)
301+
self.generic_solve_mut(b'C', b)
271302
}
272303
}
273304

@@ -279,6 +310,10 @@ where
279310
{
280311
/// Computes the inverse of the decomposed matrix.
281312
pub fn inverse(mut self) -> Option<OMatrix<T, D, D>> {
313+
if self.singular {
314+
return None;
315+
}
316+
282317
let dim = self.lu.nrows() as i32;
283318
let mut info = 0;
284319
let lwork = T::xgetri_work_size(

nalgebra-lapack/src/schur.rs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use lapack;
2424
#[cfg_attr(
2525
feature = "serde-serialize",
2626
serde(bound(deserialize = "DefaultAllocator: Allocator<D, D> + Allocator<D>,
27-
OVector<T, D>: Serialize,
27+
OVector<T, D>: Deserialize<'de>,
2828
OMatrix<T, D, D>: Deserialize<'de>"))
2929
)]
3030
#[derive(Clone, Debug)]
@@ -82,7 +82,11 @@ where
8282

8383
let lwork = T::xgees_work_size(
8484
b'V',
85-
b'T',
85+
//@note(geo-ant): this interface is bad because we could pass
86+
// 'S' here, but we cannot give a function to SELECT, which
87+
// isn't exposed here.
88+
// https://www.netlib.org/lapack/explore-html/d5/d38/group__gees.html
89+
b'N',
8690
n as i32,
8791
m.as_mut_slice(),
8892
lda,
@@ -100,7 +104,7 @@ where
100104

101105
T::xgees(
102106
b'V',
103-
b'T',
107+
b'N',
104108
n as i32,
105109
m.as_mut_slice(),
106110
lda,
@@ -169,6 +173,7 @@ pub trait SchurScalar: Scalar {
169173
fn xgees(
170174
jobvs: u8,
171175
sort: u8,
176+
//@note(geo-ant) see other comments in this file on this method
172177
// select: ???
173178
n: i32,
174179
a: &mut [Self],
@@ -208,6 +213,10 @@ macro_rules! real_eigensystem_scalar_impl (
208213
#[inline]
209214
fn xgees(jobvs: u8,
210215
sort: u8,
216+
//@note(geo-ant) see the documentation for xGEES here:
217+
// https://www.netlib.org/lapack/explore-html/d5/d38/group__gees.html
218+
// If we pass a sort argument of b'S', then we must offer a
219+
// SELECT implementation.
211220
// select: ???
212221
n: i32,
213222
a: &mut [$N],

nalgebra-lapack/src/svd.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ use lapack;
1717
serde(bound(serialize = "DefaultAllocator: Allocator<DimMinimum<R, C>> +
1818
Allocator<R, R> +
1919
Allocator<C, C>,
20-
OMatrix<T, R>: Serialize,
21-
OMatrix<T, C>: Serialize,
20+
OMatrix<T, R, R>: Serialize,
21+
OMatrix<T, C, C>: Serialize,
2222
OVector<T, DimMinimum<R, C>>: Serialize"))
2323
)]
2424
#[cfg_attr(
2525
feature = "serde-serialize",
26-
serde(bound(serialize = "DefaultAllocator: Allocator<DimMinimum<R, C>> +
27-
Allocator<R, R> +
28-
Allocator<C, C>,
29-
OMatrix<T, R>: Deserialize<'de>,
30-
OMatrix<T, C>: Deserialize<'de>,
26+
serde(bound(deserialize = "DefaultAllocator: Allocator<DimMinimum<R, C>> +
27+
Allocator<R, R> +
28+
Allocator<C, C>,
29+
OMatrix<T, R, R>: Deserialize<'de>,
30+
OMatrix<T, C, C>: Deserialize<'de>,
3131
OVector<T, DimMinimum<R, C>>: Deserialize<'de>"))
3232
)]
3333
#[derive(Clone, Debug)]
Lines changed: 55 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,44 @@
1-
use std::cmp;
2-
3-
use na::{DMatrix, DVector, Matrix4x3, Vector4};
1+
use crate::proptest::*;
2+
use core::f64;
3+
use na::{Const, DMatrix, Matrix4, Matrix4xX};
44
use nl::Cholesky;
5+
use proptest::prelude::*;
6+
7+
fn positive_definite_dmatrix() -> impl Strategy<Value = DMatrix<f64>> {
8+
// @note(geo-ant) to get positive definite matrices we use M*M^T + alpha*I,
9+
// where alpha is a constant that is chosen so that the eigenvales stay
10+
// positive.
11+
dmatrix().prop_map(|m| {
12+
let alpha = f64::EPSILON.sqrt() * m.norm_squared();
13+
let nrows = m.nrows();
14+
&m * m.transpose() + alpha * DMatrix::identity(nrows, nrows)
15+
})
16+
}
517

6-
use crate::proptest::*;
7-
use proptest::{prop_assert, proptest};
18+
fn positive_definite_matrix4() -> impl Strategy<Value = Matrix4<f64>> {
19+
matrix4().prop_map(|m| {
20+
let alpha = f64::EPSILON.sqrt() * m.norm_squared();
21+
&m * m.transpose() + alpha * Matrix4::identity()
22+
})
23+
}
24+
25+
fn positive_definite_linear_system() -> impl Strategy<Value = (DMatrix<f64>, DMatrix<f64>)> {
26+
positive_definite_dmatrix().prop_flat_map(|a| {
27+
let b = matrix(PROPTEST_F64, a.nrows(), PROPTEST_MATRIX_DIM);
28+
(Just(a), b)
29+
})
30+
}
31+
32+
fn positive_definite_linear_system_4() -> impl Strategy<Value = (Matrix4<f64>, Matrix4xX<f64>)> {
33+
positive_definite_matrix4().prop_flat_map(|a| {
34+
let b = matrix(PROPTEST_F64, Const::<4>, PROPTEST_MATRIX_DIM);
35+
(Just(a), b)
36+
})
37+
}
838

939
proptest! {
1040
#[test]
11-
fn cholesky(m in dmatrix()) {
12-
let m = &m * m.transpose();
41+
fn cholesky(m in positive_definite_dmatrix()) {
1342
if let Some(chol) = Cholesky::new(m.clone()) {
1443
let l = chol.unpack();
1544
let reconstructed_m = &l * l.transpose();
@@ -19,8 +48,7 @@ proptest! {
1948
}
2049

2150
#[test]
22-
fn cholesky_static(m in matrix3()) {
23-
let m = &m * m.transpose();
51+
fn cholesky_static(m in positive_definite_matrix4()) {
2452
if let Some(chol) = Cholesky::new(m) {
2553
let l = chol.unpack();
2654
let reconstructed_m = &l * l.transpose();
@@ -30,61 +58,37 @@ proptest! {
3058
}
3159

3260
#[test]
33-
fn cholesky_solve(n in PROPTEST_MATRIX_DIM, nb in PROPTEST_MATRIX_DIM) {
34-
let n = cmp::min(n, 15); // To avoid slowing down the test too much.
35-
let nb = cmp::min(nb, 15); // To avoid slowing down the test too much.
36-
let m = DMatrix::<f64>::new_random(n, n);
37-
let m = &m * m.transpose();
61+
fn cholesky_solve((a,b) in positive_definite_linear_system()) {
3862

39-
if let Some(chol) = Cholesky::new(m.clone()) {
40-
let b1 = DVector::new_random(n);
41-
let b2 = DMatrix::new_random(n, nb);
42-
43-
let sol1 = chol.solve(&b1).unwrap();
44-
let sol2 = chol.solve(&b2).unwrap();
45-
46-
prop_assert!(relative_eq!(&m * sol1, b1, epsilon = 1.0e-6));
47-
prop_assert!(relative_eq!(&m * sol2, b2, epsilon = 1.0e-6));
63+
if let Some(chol) = Cholesky::new(a.clone()) {
64+
let sol = chol.solve(&b).unwrap();
65+
prop_assert!(relative_eq!(&a * sol, b, epsilon = 1.0e-5));
4866
}
4967
}
5068

5169
#[test]
52-
fn cholesky_solve_static(m in matrix4()) {
53-
let m = &m * m.transpose();
54-
if let Some(chol) = Cholesky::new(m) {
55-
let b1 = Vector4::new_random();
56-
let b2 = Matrix4x3::new_random();
57-
58-
let sol1 = chol.solve(&b1).unwrap();
59-
let sol2 = chol.solve(&b2).unwrap();
60-
61-
prop_assert!(relative_eq!(m * sol1, b1, epsilon = 1.0e-4));
62-
prop_assert!(relative_eq!(m * sol2, b2, epsilon = 1.0e-4));
70+
fn cholesky_solve_static((a,b) in positive_definite_linear_system_4()) {
71+
if let Some(chol) = Cholesky::new(a) {
72+
let sol = chol.solve(&b).unwrap();
73+
prop_assert!(relative_eq!(a * sol, b, epsilon = 1.0e-5));
6374
}
6475
}
6576

6677
#[test]
67-
fn cholesky_inverse(n in PROPTEST_MATRIX_DIM) {
68-
let n = cmp::min(n, 15); // To avoid slowing down the test too much.
69-
let m = DMatrix::<f64>::new_random(n, n);
70-
let m = &m * m.transpose();
71-
72-
if let Some(m1) = Cholesky::new(m.clone()).unwrap().inverse() {
73-
let id1 = &m * &m1;
74-
let id2 = &m1 * &m;
78+
fn cholesky_inverse(a in positive_definite_dmatrix()) {
79+
let minv = Cholesky::new(a.clone()).unwrap().inverse().unwrap();
80+
let id1 = &a * &minv;
81+
let id2 = &minv * &a;
7582

76-
prop_assert!(id1.is_identity(1.0e-6) && id2.is_identity(1.0e-6));
77-
}
83+
prop_assert!(id1.is_identity(1.0e-6) && id2.is_identity(1.0e-6));
7884
}
7985

8086
#[test]
81-
fn cholesky_inverse_static(m in matrix4()) {
82-
let m = m * m.transpose();
83-
if let Some(m1) = Cholesky::new(m.clone()).unwrap().inverse() {
84-
let id1 = &m * &m1;
85-
let id2 = &m1 * &m;
87+
fn cholesky_inverse_static(a in positive_definite_matrix4()) {
88+
let minv = Cholesky::new(a.clone()).unwrap().inverse().unwrap();
89+
let id1 = &a * &minv;
90+
let id2 = &minv * &a;
8691

87-
prop_assert!(id1.is_identity(1.0e-4) && id2.is_identity(1.0e-4))
88-
}
92+
prop_assert!(id1.is_identity(1.0e-6) && id2.is_identity(1.0e-6));
8993
}
9094
}

0 commit comments

Comments
 (0)