Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QZ-decomposition #1067

Merged
merged 31 commits into from Apr 30, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7230ae1
First attempt at xgges (qz decomposition), passing tests. Serializati…
metric-space Jan 19, 2022
6f7ef38
Format file
metric-space Jan 19, 2022
769f20c
Comments more tailored to QZ
metric-space Jan 19, 2022
b2c6c6b
Add non-naive way of calculate generalized eigenvalue, write spotty t…
metric-space Jan 20, 2022
6a28306
Commented out failing tests, refactored checks for almost zeroes
metric-space Jan 20, 2022
7e9345d
Correction for not calculating absolurte value
metric-space Jan 21, 2022
7d8fb3d
New wrapper for generalized eigenvalues and associated eigenvectors v…
metric-space Jan 25, 2022
748848f
Cleanup of QZ module and added GE's calculation of eigenvalues as a t…
metric-space Jan 25, 2022
ae35d1c
New code and modified tests for generalized_eigenvalues
metric-space Feb 3, 2022
162bb32
New code and modified tests for qz
metric-space Feb 3, 2022
d88337e
Formatting
metric-space Feb 3, 2022
55fdd84
Formatting
metric-space Feb 3, 2022
4362f00
Added comment on logic
metric-space Feb 4, 2022
4038e66
Correction to keep naming of left and right eigenvector matrices cons…
metric-space Feb 4, 2022
d5069f3
Removed extra memory allocation for buffer (now redundant)
metric-space Feb 6, 2022
a4de6a8
Corrected deserialization term in serialization impls
metric-space Feb 9, 2022
497a4d8
Correction in eigenvector matrices build up algorithm
metric-space Feb 12, 2022
fb0cb51
Remove condition number, tests pass without. Add proper test generato…
metric-space Feb 12, 2022
b7fe6b9
Name change
metric-space Feb 12, 2022
91b7e05
Change name of test generator
metric-space Feb 12, 2022
7510d48
Doc string corrections
metric-space Feb 12, 2022
e52f117
Change name of copied macro base
metric-space Feb 12, 2022
5e10ca4
Add another case for when eigenvalues should be mapped to zero. Make …
metric-space Feb 15, 2022
c8a920f
Minimal post-processing and fix to documentation
metric-space Feb 27, 2022
3413ab7
Correct typos, move doc portion to comment and fix borrow to clone
metric-space Mar 5, 2022
4413a35
Fix doc
metric-space Mar 5, 2022
adf50a6
Fix formatting
metric-space Mar 5, 2022
2743eef
Add in explicit type of matrix element to module overview docs
metric-space Mar 5, 2022
80a844a
Update check for zero
metric-space Apr 16, 2022
bc31012
Add newline
metric-space Apr 16, 2022
ff2d431
Remove repeated docs
metric-space Apr 16, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
399 changes: 399 additions & 0 deletions nalgebra-lapack/src/generalized_eigenvalues.rs
@@ -0,0 +1,399 @@
#[cfg(feature = "serde-serialize")]
use serde::{Deserialize, Serialize};

use num::Zero;
use num_complex::Complex;

use simba::scalar::RealField;

use crate::ComplexHelper;
use na::allocator::Allocator;
use na::dimension::{Const, Dim};
use na::{DefaultAllocator, Matrix, OMatrix, OVector, Scalar};

use lapack;

/// Generalized eigenvalues and generalized eigenvectors (left and right) of a pair of N*N square matrices.
///
/// Each generalized eigenvalue (lambda) satisfies determinant(A - lambda*B) = 0
///
/// The right eigenvector v(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// A * v(j) = lambda(j) * B * v(j).
///
/// The left eigenvector u(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// u(j)**H * A = lambda(j) * u(j)**H * B .
/// where u(j)**H is the conjugate-transpose of u(j).
#[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))]
#[cfg_attr(
feature = "serde-serialize",
serde(
bound(serialize = "DefaultAllocator: Allocator<T, D, D> + Allocator<T, D>,
OVector<T, D>: Serialize,
OMatrix<T, D, D>: Serialize")
)
)]
#[cfg_attr(
feature = "serde-serialize",
serde(
bound(deserialize = "DefaultAllocator: Allocator<T, D, D> + Allocator<T, D>,
OVector<T, D>: Deserialize<'de>,
OMatrix<T, D, D>: Deserialize<'de>")
)
)]
#[derive(Clone, Debug)]
pub struct GeneralizedEigen<T: Scalar, D: Dim>
where
DefaultAllocator: Allocator<T, D> + Allocator<T, D, D>,
{
alphar: OVector<T, D>,
alphai: OVector<T, D>,
beta: OVector<T, D>,
vsl: OMatrix<T, D, D>,
vsr: OMatrix<T, D, D>,
}

impl<T: Scalar + Copy, D: Dim> Copy for GeneralizedEigen<T, D>
where
DefaultAllocator: Allocator<T, D, D> + Allocator<T, D>,
OMatrix<T, D, D>: Copy,
OVector<T, D>: Copy,
{
}

impl<T: GeneralizedEigenScalar + RealField + Copy, D: Dim> GeneralizedEigen<T, D>
where
DefaultAllocator: Allocator<T, D, D> + Allocator<T, D>,
{
/// Attempts to compute the generalized eigenvalues, and left and right associated eigenvectors
/// via the raw returns from LAPACK's dggev and sggev routines
///
/// Each generalized eigenvalue (lambda) satisfies determinant(A - lambda*B) = 0
///
/// The right eigenvector v(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// A * v(j) = lambda(j) * B * v(j).
///
/// The left eigenvector u(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// u(j)**H * A = lambda(j) * u(j)**H * B .
/// where u(j)**H is the conjugate-transpose of u(j).
///
/// Panics if the method did not converge.
pub fn new(a: OMatrix<T, D, D>, b: OMatrix<T, D, D>) -> Self {
Self::try_new(a, b).expect("Calculation of generalized eigenvalues failed.")
}

/// Attempts to compute the generalized eigenvalues (and eigenvectors) via the raw returns from LAPACK's
/// dggev and sggev routines
///
/// Each generalized eigenvalue (lambda) satisfies determinant(A - lambda*B) = 0
///
/// The right eigenvector v(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// A * v(j) = lambda(j) * B * v(j).
///
/// The left eigenvector u(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// u(j)**H * A = lambda(j) * u(j)**H * B .
/// where u(j)**H is the conjugate-transpose of u(j).
///
/// Returns `None` if the method did not converge.
pub fn try_new(mut a: OMatrix<T, D, D>, mut b: OMatrix<T, D, D>) -> Option<Self> {
assert!(
a.is_square() && b.is_square(),
"Unable to compute the generalized eigenvalues of non-square matrices."
);

assert!(
a.shape_generic() == b.shape_generic(),
"Unable to compute the generalized eigenvalues of two square matrices of different dimensions."
);

let (nrows, ncols) = a.shape_generic();
let n = nrows.value();

let mut info = 0;

let mut alphar = Matrix::zeros_generic(nrows, Const::<1>);
let mut alphai = Matrix::zeros_generic(nrows, Const::<1>);
let mut beta = Matrix::zeros_generic(nrows, Const::<1>);
let mut vsl = Matrix::zeros_generic(nrows, ncols);
let mut vsr = Matrix::zeros_generic(nrows, ncols);

let lwork = T::xggev_work_size(
b'V',
b'V',
n as i32,
a.as_mut_slice(),
n as i32,
b.as_mut_slice(),
n as i32,
alphar.as_mut_slice(),
alphai.as_mut_slice(),
beta.as_mut_slice(),
vsl.as_mut_slice(),
n as i32,
vsr.as_mut_slice(),
n as i32,
&mut info,
);
lapack_check!(info);

let mut work = vec![T::zero(); lwork as usize];

T::xggev(
b'V',
b'V',
n as i32,
a.as_mut_slice(),
n as i32,
b.as_mut_slice(),
n as i32,
alphar.as_mut_slice(),
alphai.as_mut_slice(),
beta.as_mut_slice(),
vsl.as_mut_slice(),
n as i32,
vsr.as_mut_slice(),
n as i32,
&mut work,
lwork,
&mut info,
);
lapack_check!(info);

Some(GeneralizedEigen {
alphar,
alphai,
beta,
vsl,
vsr,
})
}

/// Calculates the generalized eigenvectors (left and right) associated with the generalized eigenvalues
/// Outputs two matrices, the first one containing the left eigenvectors of the generalized eigenvalues
/// as columns and the second matrix contains the right eigenvectors of the generalized eigenvalues
/// as columns
///
/// The right eigenvector v(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// A * v(j) = lambda(j) * B * v(j).
///
/// The left eigenvector u(j) corresponding to the eigenvalue lambda(j)
/// of (A,B) satisfies
///
/// u(j)**H * A = lambda(j) * u(j)**H * B .
/// where u(j)**H is the conjugate-transpose of u(j).
///
/// What is going on below?
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an answer to your question here: #1067 (comment)

I think this doc comment is very confusing because it's not clear what "below" is. Keep in mind that the source code is not generally displayed under the documentation when browsing docs, so you certainly wouldn't expect it to mean code in that case, but rather the docs that follow.

Anyway, for what comes below, is it possible to give a more intuitive explanation, perhaps in addition? Do generalized complex eigenvalues come in conjugate pairs? Maybe you can start by actually writing this out, as since I don't have experience with generalized eigenvalues it's not something I would immediately think of. I also don't really understand the VSL notation (although the MATLAB-like indexing is OK). What is VSL again? It's not explained here in the docs.

/// If the j-th and (j+1)-th eigenvalues form a complex conjugate pair,
/// then u(j) = VSL(:,j)+i*VSL(:,j+1) and u(j+1) = VSL(:,j)-i*VSL(:,j+1).
/// and then v(j) = VSR(:,j)+i*VSR(:,j+1) and v(j+1) = VSR(:,j)-i*VSR(:,j+1).
pub fn eigenvectors(self) -> (OMatrix<Complex<T>, D, D>, OMatrix<Complex<T>, D, D>)
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing I did not realize before now: Here we take self by value, but in the implementation we still clone self.vsl and self.vsr. This seems a little unfortunate, perhaps?

where
DefaultAllocator:
Allocator<Complex<T>, D, D> + Allocator<Complex<T>, D> + Allocator<(Complex<T>, T), D>,
{
let n = self.vsl.shape().0;

let mut l = self
.vsl
.clone()
.map(|x| Complex::new(x, T::RealField::zero()));

let mut r = self
.vsr
.clone()
.map(|x| Complex::new(x, T::RealField::zero()));

let eigenvalues = &self.eigenvalues();

let mut c = 0;

let epsilon = T::RealField::default_epsilon();

while c < n {
if eigenvalues[c].im.abs() > epsilon && c + 1 < n && {
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this the first time around, but at first glance this logic does not sound to me. What does it do? I think this needs a solid comment and/or documentation.

For example, imagine scaling a matrix by 1e20, then all its eigenvalues get scaled by 1e20 and this logic will behave entirely differently. It seems to me at the very least that we'd need to look relative to the maximum absolute eigenvalue, but even then it's not bullet proof.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The scaling point is something I've never considered, it's a pretty solid point.

But in this scenario, LAPACK uses the eigenvalues to signal how to assemble eigenvectors. If the eigenvalue is complex and if the subsequent eigenvalue is the former's complex conjugate, the eigenvector to be assembled has it's real and imaginary parts in two subsequent columns.

Does the scaling edge case apply here seeing that it's more of an assembling problem by signalling than a numeric one? In the documentation, the way to check for realness is by looking at a_j values, this gets mapped to the imaginary value of an eigenvalue. While it's interesting for me to look deeper in general and perhaps into LAPACK code, was wondering if putting the burden of getting right for the edge case is more on LAPACK to get it right?

In either case, putting in a comment here does make sense Is this not apt to explain what is being done? It is possible I've been in the trenches long enough to not know if it doesn't map well https://github.com/dimforge/nalgebra/pull/1067/files#diff-a056a4c6b4fb629735a73fcb65e0d5223386cb47dedb6ab80d5a1b905ebd9d25R198

Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think for me it's not clear why we need to do any kind of "post processing" at all. I think this would be nice to have a comment explaining this. And moreover, I would be very surprised if LAPACK expects you to use approximate comparison if it's signaling something through values - are you sure exact comparison is not more appropriate? I just don't have enough context to know what is the appropriate thing to do here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fair. I think there is some level of post processing expected but not to the extent I have done which is more a result of not trusting LAPACK which may be unwarranted. The changes reflect what I think LAPACK expects us to do minimally

let e_conj = eigenvalues[c].conj();
let e = eigenvalues[c + 1];
(&e_conj.re).ulps_eq(&e.re, epsilon, 6) && (&e_conj.im).ulps_eq(&e.im, epsilon, 6)
} {
// taking care of the left eigenvector matrix
l.column_mut(c).zip_apply(&self.vsl.column(c + 1), |r, i| {
*r = Complex::new(r.re.clone(), i.clone());
});
l.column_mut(c + 1).zip_apply(&self.vsl.column(c), |i, r| {
*i = Complex::new(r.clone(), -i.re.clone());
});

// taking care of the right eigenvector matrix
r.column_mut(c).zip_apply(&self.vsr.column(c + 1), |r, i| {
*r = Complex::new(r.re.clone(), i.clone());
});
r.column_mut(c + 1).zip_apply(&self.vsr.column(c), |i, r| {
*i = Complex::new(r.clone(), -i.re.clone());
});

c += 2;
} else {
c += 1;
}
}

(l, r)
}

/// computes the generalized eigenvalues i.e values of lambda that satisfy the following equation
/// determinant(A - lambda* B) = 0
#[must_use]
pub fn eigenvalues(&self) -> OVector<Complex<T>, D>
where
DefaultAllocator: Allocator<Complex<T>, D>,
{
let mut out = Matrix::zeros_generic(self.vsl.shape_generic().0, Const::<1>);

for i in 0..out.len() {
out[i] = if self.beta[i].clone().abs() < T::RealField::default_epsilon() {
Complex::zero()
Copy link
Sponsor Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment on check applies here

} else {
Complex::new(self.alphar[i].clone(), self.alphai[i].clone())
* (Complex::new(self.beta[i].clone(), T::RealField::zero()).inv())
}
}

out
}

/// outputs the unprocessed (almost) version of generalized eigenvalues ((alphar, alpai), beta)
/// straight from LAPACK
#[must_use]
pub fn raw_eigenvalues(&self) -> OVector<(Complex<T>, T), D>
where
DefaultAllocator: Allocator<(Complex<T>, T), D>,
{
let mut out = Matrix::from_element_generic(
self.vsl.shape_generic().0,
Const::<1>,
(Complex::zero(), T::RealField::zero()),
);

for i in 0..out.len() {
out[i] = (Complex::new(self.alphar[i], self.alphai[i]), self.beta[i])
}

out
}
}

/*
*
* Lapack functions dispatch.
*
*/
/// Trait implemented by scalars for which Lapack implements the RealField GeneralizedEigen decomposition.
pub trait GeneralizedEigenScalar: Scalar {
#[allow(missing_docs)]
fn xggev(
jobvsl: u8,
jobvsr: u8,
n: i32,
a: &mut [Self],
lda: i32,
b: &mut [Self],
ldb: i32,
alphar: &mut [Self],
alphai: &mut [Self],
beta: &mut [Self],
vsl: &mut [Self],
ldvsl: i32,
vsr: &mut [Self],
ldvsr: i32,
work: &mut [Self],
lwork: i32,
info: &mut i32,
);

#[allow(missing_docs)]
fn xggev_work_size(
jobvsl: u8,
jobvsr: u8,
n: i32,
a: &mut [Self],
lda: i32,
b: &mut [Self],
ldb: i32,
alphar: &mut [Self],
alphai: &mut [Self],
beta: &mut [Self],
vsl: &mut [Self],
ldvsl: i32,
vsr: &mut [Self],
ldvsr: i32,
info: &mut i32,
) -> i32;
}

macro_rules! generalized_eigen_scalar_impl (
($N: ty, $xggev: path) => (
impl GeneralizedEigenScalar for $N {
#[inline]
fn xggev(jobvsl: u8,
jobvsr: u8,
n: i32,
a: &mut [$N],
lda: i32,
b: &mut [$N],
ldb: i32,
alphar: &mut [$N],
alphai: &mut [$N],
beta : &mut [$N],
vsl: &mut [$N],
ldvsl: i32,
vsr: &mut [$N],
ldvsr: i32,
work: &mut [$N],
lwork: i32,
info: &mut i32) {
unsafe { $xggev(jobvsl, jobvsr, n, a, lda, b, ldb, alphar, alphai, beta, vsl, ldvsl, vsr, ldvsr, work, lwork, info); }
}


#[inline]
fn xggev_work_size(jobvsl: u8,
jobvsr: u8,
n: i32,
a: &mut [$N],
lda: i32,
b: &mut [$N],
ldb: i32,
alphar: &mut [$N],
alphai: &mut [$N],
beta : &mut [$N],
vsl: &mut [$N],
ldvsl: i32,
vsr: &mut [$N],
ldvsr: i32,
info: &mut i32)
-> i32 {
let mut work = [ Zero::zero() ];
let lwork = -1 as i32;

unsafe { $xggev(jobvsl, jobvsr, n, a, lda, b, ldb, alphar, alphai, beta, vsl, ldvsl, vsr, ldvsr, &mut work, lwork, info); }
ComplexHelper::real_part(work[0]) as i32
}
}
)
);

generalized_eigen_scalar_impl!(f32, lapack::sggev);
generalized_eigen_scalar_impl!(f64, lapack::dggev);