-
Notifications
You must be signed in to change notification settings - Fork 5
Exponential #94
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
base: main
Are you sure you want to change the base?
Exponential #94
Conversation
Codecov Report❌ Patch coverage is
... and 4 files with indirect coverage changes 🚀 New features to boost your workflow:
|
| end | ||
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) | ||
| D, V = eigh_full(A, alg.eigh_alg) |
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.
Why not allocate DV yourself then use eigh_full! so that you don't have to copy A?
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 think we actually quite frequently have needed this, how about we add the signature to the @functiondef implementations and have this everywhere?
$f!(A, alg::AbstractAlgorithm) = $f!(A, initialize_output($f!, A, alg), alg)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.
(recommend doing this in a separate PR, easier to get merged 😉 )
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.
For me, it is definitely fine to leave it like this and think about it in a separate PR.
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.
Let me rephrase: I mean to first add that and then rebase this on top.
src/implementations/exponential.jl
Outdated
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) | ||
| D, V = eigh_full(A, alg.eigh_alg) | ||
| copyto!(expA, V * Diagonal(exp.(diagview(D))) * inv(V)) |
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.
Reduced allocation strategy:
| copyto!(expA, V * Diagonal(exp.(diagview(D))) * inv(V)) | |
| iV = inv(V) | |
| map!(exp, diagview(D)) | |
| mul!(expA, rmul!(V, D), iV) |
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 has to be map!(exp, diagview(D), diagview(D)) instead of map!(exp, diagview(D)), but good suggestion otherwise. I have also added it for the ExponentialViaEig.
EDIT: the suggested change works only for Julia 1.12 onwards. That's why I will keep the version with
3 arguments.
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.
Why not just diagview(D) .= exp.(diagview(D))?
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 that more efficient than the current code? If not, I'd prefer to keep it that way, since it feels a bit more natural to me.
lkdvos
left a comment
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.
Overall I'm not fully convinced by the interface of exponential(!), especially in its current form and implementation this looks slightly strange.
LinearAlgebra uses an in-place version, i.e. it reuses the input array to return exp!, and looking at the different implementations you have here, it is not obvious that trying to fit this into a exponentiate!(A, expA, alg) signature is really helping us - on the contrary, all this is really doing is creating an additional copy at the end just to make sure that it is allocated in the provided output.
As we discussed for your previous PR, this really is not the purpose of being able to provide the output argument.
For the algorithms, thinking a bit ahead, it might be appropriate to just call these something along the lines of matrix functions via eig, since presumably these approaches are actually generic for all of these implementations.
| end | ||
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) | ||
| D, V = eigh_full(A, alg.eigh_alg) |
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.
Let me rephrase: I mean to first add that and then rebase this on top.
src/implementations/exponential.jl
Outdated
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) | ||
| D, V = eigh_full(A, alg.eigh_alg) | ||
| copyto!(expA, V * Diagonal(exp.(diagview(D))) * inv(V)) |
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.
Why not just diagview(D) .= exp.(diagview(D))?
| end | ||
|
|
||
| function MatrixAlgebraKit.default_exponential_algorithm(E::Type{T}; kwargs...) where {T <: StridedMatrix{<:Union{BigFloat, Complex{BigFloat}}}} | ||
| return ExponentialViaEig(GS_QRIteration(; kwargs...)) |
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.
| return ExponentialViaEig(GS_QRIteration(; kwargs...)) | |
| eig_alg = MatrixAlgebraKit.default_eig_alg(E; kwargs...) | |
| return ExponentialViaEig(eig_alg) |
or something similar
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) | ||
| D, V = eigh_full(A, alg.eigh_alg) | ||
| iV = inv(V) |
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.
| iV = inv(V) | |
| iV = adjoint(V) |
for the hermitian case this shouldn't be a full inverse, but you then do have to adapt the remainder of the implementation to avoid overwriting 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.
Yes I guess that is my fault; I didn't think of adjoint(V) in the Hermitian case when I suggested this last night. I think one can do something like
diagview(D) .= exp.( diagview(D) ./ 2)
rmul!(V, D)
mul!(expA, V, adjoint(V))
src/implementations/exponential.jl
Outdated
| function initialize_output(::typeof(exponential!), A::Diagonal, ::DiagonalAlgorithm) | ||
| return similar(A) | ||
| end |
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.
| function initialize_output(::typeof(exponential!), A::Diagonal, ::DiagonalAlgorithm) | |
| return similar(A) | |
| end | |
| initialize_output(::typeof(exponential!), A::Diagonal, ::DiagonalAlgorithm) = A |
For diagonal, we try to avoid allocations as much as possible.
| # Implementation | ||
| # -------------- | ||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaLA) | ||
| copyto!(expA, LinearAlgebra.exp(A)) |
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.
| copyto!(expA, LinearAlgebra.exp(A)) | |
| check_input(exponential!, A, expA, alg) | |
| copyto!(expA, LinearAlgebra.exp(A)) |
src/implementations/exponential.jl
Outdated
| return expA | ||
| end | ||
|
|
||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaEigh) |
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 missing check_input here
src/implementations/exponential.jl
Outdated
|
|
||
| # Implementation | ||
| # -------------- | ||
| function exponential!(A::AbstractMatrix, expA::AbstractMatrix, alg::ExponentialViaLA) |
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 implementation is a bit strange, LinearAlgebra actually uses exp!(A) as its expert driver, so at least this is allowed to destroy the input matrix, but additionally you should also try and avoid allocating expA, and simply have expA === A
| # ------- | ||
| function initialize_output(::typeof(exponential!), A::AbstractMatrix, ::AbstractAlgorithm) | ||
| n = size(A, 1) # square check will happen later | ||
| expA = similar(A, (n, n)) |
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.
For eigenvalue based decompositions, are there considerations about obtaining complex eigenvalues from real matrices and what this means for the output? I guess the output for a real matrix can always be made real, but does that work with the current implementation + finite numeric precision?
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's a good comment; I think this is one of the main reasons why the matrix exponential is typically not computed via the eigenvalue decomposition in the general (non-Hermitian) case. Also, the eigenvalue decomposition could be (close to) unstable, in the case of Jordan blocks.
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 hadn't thought of this, and indeed also the hermitian tests don't capture this. I can change the current code to
map!(exp, diagview(D), diagview(D))
rmul!(V, D)
copyto!(expA, real(V * iV))to accommodate this. This is not the most elegant solution, but I don't think we can get around this problem for the non-hermitian case.
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 a general note: copyto! really means a memcopy of the data, and has no notion of axes. Almost everywhere you really want copy! instead.
Specifically here, at least it should be:
expA .= real.(V * iV)otherwise you again have the issue of first allocating a completely new array to then immediately copy it.
The idea of putting it in this framework is to allow for different sorts of algorithms, i.e. ones that would also work for BigFloats. I get that in the BLASFloat case, we should avoid extra allocations, but could you elaborate on your suggestion for
I may be missing your point here, but that was the idea behind the |
I am indeed referring to the preallocated output, not the algorithms part. It really only makes sense to have the option of giving a preallocated output if we are actually able to use this, and for the current implementations you have this is not saving us any work, rather it is increasing it because you add an extra allocation at the beginning and an extra copy at the end. While it is definitely possible to have
Sorry I should have explained that better, I meant that I would like to avoid having to also define |
Is your suggestion then to just skip the whole
Okay, I see. I agree and will change this. |
|
Regarding the algorithm names, I agree with Lukas and also think we want to have a general Regarding the role of the output arguments, I only partially agree. The whole point of why we started MatrixAlgebraKit.jl, is because in TensorKit we first want to define the output tensor, and then compute block per block the result, where we want to store the result in the corresponding block of the output tensor. Ideally, yes, the computation is such that we also use that output data as storage during the computation, in such a way that the end result "naturally" ends up there, but if that is difficult, a final Note that the LinearAlgebra |
|
Regardless of the comment about general matrix functions, it is a fact that the exponential is by far the most useful and common one that we need, so I am also not opposed to first thinking carefully about this one, and having some part of the implementation be specific for matrix exponentials. In particular, one important consideration that we might want to include in this design, that is specific to our use case, is that we also might be interested in computing |
|
To comment on the TensorKit interaction, I definitely agree with the purpose, but this is not actually currently the design we ended up with. So basically there are two comments I have: On the one hand, there is the question about whether or not there are implementations that benefit from providing an additional output array. On the other hand, given that interface, if there is no way of naturally making the output end up in the provided destination, I would really like to avoid ending up with a final |
|
I guess I am a bit confused, because most of the implementations now do actually perform the final step in the calculation in such a way that the result is directly stored in the output array, no? It is only the algorithm that goes via Base/LinearAlgebra that requires the extra But it is also true that, by the time the final step of the calculation is reached; the memory of |
change name to `MatrixFunctionViaEig` etc change `decompositions` to `matrixfunctions` add default algorithm for Diagonal matrices add input checks add @testthrows to catch non-hermitian matrices being given to MatrixFunctionViaEigh change default exponential algorithm to e.g. `MatrixFunctionViaEig` of the default `eig_alg`
|
Your PR no longer requires formatting changes. Thank you for your contribution! |
|
The last few commits added the functions |
|
Could we revisit the names for a second?
|
There are a few comments that occur twice in the view, that I have reacted to once. The comments that I have taken into account and have reacted a 'thumbs up' to, I haven't resolved to make sure you saw they were taken into account. There are, I think, two comments left:
|
|
I think I finally managed to work away (some of) my backlog of PRs that I had to review, so this one is among the next on my list. I hope to go through it (and if necessary commit changes myself directly) before the end of the week. |
This implements the exponential of a matrix for both
BLASFloatsandBigFloats.I have named these functions
exponentialandexponential!, instead of the usualexpandexp!fromLinearAlgebra. Extending these methods while keeping the current structure using @algdef and @ functiondef results in some naming conflicts. The default for BLASFloats is to useLinearAlgebra.exp!. InTensorKit, we can still stick to theexpnaming convention.