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
Taking Eigen seriously #48549
base: master
Are you sure you want to change the base?
Taking Eigen seriously #48549
Conversation
x-ref #41830 |
Oh wow, I see I even commented on that PR. It's not even that long ago, and I completely forgot about it... I'd say the present PR is simpler, since it does not introduce extra API (new functions or arguments) but only add dispatches for things that "should" work. It is also safer (but slightly more restrictive) because it doesn't allow |
I believe we wrap Hermitian matrices with I just wanted to raise awareness for that other PR, so that "overlapping" aspects are well-resolved. Would be awesome to also push the other PR across the finish line. @RalphAS |
My PR extends the Eigen type by adding dispatches for matrix functions, matrix multiplications, etc and only adds a "normal" field to the Eigen type. @RalphAS 's adds the "normal" field, adds a new |
I like this PR so far. This is a good opportunity to document some of the "hidden" capabilities of the I think the bulk of #41830 really needs new data structures that haven't been worked out yet, so it should be orthogonal. |
Good point. I added news and docs. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. Shall we rename are_vectors_unitary
to unitary_[e]vecs
or something?
I don't know, it's a pretty internal field so I feel being super explicit is good and long identifiers are not an issue, unitary_evecs might make somebody think it's an array of vectors that are more unitary than vectors or something. But I'm fine to rename to whatever if you want. |
NEWS.md
Outdated
@@ -47,6 +47,9 @@ Standard library changes | |||
supports precompilation and setting compatibility. | |||
#### LinearAlgebra | |||
|
|||
- The `Eigen` type now supports more methods, such as matrix functions | |||
(which return `Eigen` objects), adjoints and matrix-vector product. | |||
`inv(::Eigen)` now produces an `Eigen` type instead of a matrix. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this enough for this PR to gain the "minor change" label?
FWIW, I did an audit of my personal code, and while I never assume that inv(::Eigen)
returns a matrix, I have relied on the fact that inv(::LU)
returns a matrix.
In any case, it might be nice to say explicitly how one can continue to get the inverse matrix from ::Eigen
, if that's what a user wants.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point. x-ref #32137. And certainly this requires a pkgeval run if we go with that change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I understood the rules were more relaxed about breaking changes for stdlibs? I can either special-case inv
to return a matrix (and we can change it for 2.0) or make the change now with a more explicit NEWS. Can somebody run pkgeval, just to get an idea of how many people rely on this? (I'd be surprised, but...)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@antoine-levitt You can run pkgeval yourself! It's as easy as:
@nanosoldier runtests()
See https://github.com/JuliaCI/Nanosoldier.jl#readme for more info
Another issue is that eigenvalues are no longer ordered in ascending order, as we typically assume for |
We can for sure sort them (perf hit is likely negligible), but where is this assumed? |
``` | ||
""" | ||
struct Eigen{T,V,S<:AbstractMatrix,U<:AbstractVector} <: Factorization{T} | ||
values::U | ||
vectors::S | ||
Eigen{T,V,S,U}(values::AbstractVector{V}, vectors::AbstractMatrix{T}) where {T,V,S,U} = | ||
new(values, vectors) | ||
are_vectors_unitary::Bool |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This new field will not be required with #48657, which goes a step further in storing the left eigenvectors. For the unitary case F.vectors === F.vectorsl
will have th the same information.
Your package evaluation job has completed - possible new issues were detected. |
Doesn't look like there's anything wrong with the pkgeval job |
I think I confused it with |
This is in
|
det(A::Eigen) = prod(A.values) | ||
tr(E::Eigen) = sum(E.values) | ||
|
||
*(E::Eigen, x::AbstractVector) = E.vectors * (E.values .* (E.are_vectors_unitary ? (E.vectors'x) : (E.vectors \ x))) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That means a O(n^3)
operation for each call. It would make sense to store inv(vectors)'
in the Eigen
structure here ( and remove are_vectors_unitary
). That is also valid for the next function and all others where inv(vectors)
or similar are called on the fly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but this pays the cost upfront, even if the user doesn't need to do anything except computing the values/vectors, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes but this pays the cost upfront
Cannot understand this argument. If you want to multiply the same E
with two different vectors, and you don't know the second one before you have evaluated the first one, you have to E.vectors \ v
twice, which means two LU-factorizations of E.vectors
. That is what I meant.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, you mean in this function, compute the inverse of the vectors, and mutate E? I don't think we should do something as sophisticated as this, and having Eigen be mutable seems like begging for trouble. These are mostly convenience methods for playing around at the REPL or quick scripts, they are not meant to be used for performance-sensitive work, so over-engineering this adds complexity for not much gain.
mul!(x, E::Eigen, y::AbstractVector) = copyto!(x, E*y) | ||
|
||
inv(E::Eigen) = Eigen(map(inv, E.values), copy(E.vectors), E.are_vectors_unitary) | ||
for f in _matrix_functions |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The restriction of applicable functions to this list is a burden. What about adding an additional function
like apply(f, E)
= ... to allow for user defined functions?
remark:
unfortuantely (f::Function)(E) = ...
is not supported by Julia.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm concerned about exporting a function here, esp since the functionality is relatively half-baked (it doesn't apply to general matrices, just to Eigen). I think it would make a lot more sense to have a MatrixFunctions package that does this properly (including for non-hermitian matrices)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it doesn't apply to general matrices, just to Eigen
It would be the typical workflow to first evaluate eigen(A)
, then f1(A)
, f2(A)
, ... . Again it would not make sense to call eigen(A)
for each individual evaluation of f(A)
. That would just be a convenience, if you know in advance, that the eigen-decomposition is only needed for a single function evaluation.
Also your argument is valid for sin, cosh, exp,
and brothers in the same way as for apply
, so I don't understand your concern.
This operation is valid for all diagonalizable matrices, not only Hermitian or Symmetric, or normal ones.
In the case of multiple eigenvalues and non-diagonalizable matrices, eigen
cannot help much.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also your argument is valid for sin, cosh, exp, and brothers in the same way as for apply, so I don't understand your concern.
Adding methods to these functions does not add API surface
This operation is valid for all diagonalizable matrices, not only Hermitian or Symmetric, or normal ones.
Technically, yes. In practice, using it for strongly non-normal matrices is a bad idea (see eg the classic paper "N bad ways to compute matrix exponentials" or whatever it's called), and there are better ways to do it. Ideally, a MatrixFunctions package would deal with this. This PR is just an opportunity to define methods that ought to exist already.
Can we merge this? CI appears to be OK except for some unrelated problems |
stdlib/LinearAlgebra/src/eigen.jl
Outdated
*(x::Adjoint{<:Any, <:AbstractVector}, E::Eigen) = E.are_vectors_unitary ? | ||
((x*(E.vectors)) * Diagonal(E.values)) * (E.vectors') : | ||
((x*(E.vectors)) * Diagonal(E.values)) / E.vectors | ||
mul!(x, E::Eigen, y::AbstractVector) = copyto!(x, E*y) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the point of providing this method if it doesn't provide any improvement over E*y
(and makes it actually even worse, against expectations)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very reasonable point, I'll remove it!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the matrix functions, I don't think it makes sense to reorder the inverted eigenvalues (and the corresponding eigenvectors). For consistency, we would need to do the same for those Eigen
objects, which seems awkward. But then perhaps it would be good to document that after applying inv
or matrix functions, you can no longer assume that the first/last entry of the values
field contains the smallest/largest eigenvalue by default.
OK, I added a comment on that |
This extends the Eigen API to support a few more functions (matvecs, matrix functions), and optimizes in the case where the eigenvectors are unitary. This is very convenient for eg computing
exp(tA)x
at different times.