Exponential#94
Conversation
Codecov Report❌ Patch coverage is
... and 36 files with indirect coverage changes 🚀 New features to boost your workflow:
|
|
|
||
| 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.
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.
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.
Why not just diagview(D) .= exp.(diagview(D))?
There was a problem hiding this comment.
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.
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.
|
|
||
| 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.
Why not just diagview(D) .= exp.(diagview(D))?
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`
|
I don't get why you say the current code would not work with a real scalar. The argument Apart from the fact that I don't see why the changes would make the difference between In terms of differentiating between If you really feel strongly about any of this, feel free to tailor the implementation to how you see things. The only thing that I really care about is an implementation that works, preferably as generic as possible (again, because I would like to have this in the next TensorKit version). As long as the tests pass, this is all fine by me. I might not be a big fan of the choice of implementation, but naming conventions and interface choices are way less important to me than having either implementation. |
The difference is a bit subtle, but as I mentioned before this is about type stability. function exponentialr(tau, A)
expA = tau isa Complex ? complex(A) : A
return exponentialr!(tau, A, expA) # this computes expA .= exp(A * tau) in whatever way
end
function exponentiali(t, A)
expA = iszero(real(t)) ? A : complex(A)
return exponentiali!(t, A, expA) # this computes expA .= exp(A * t * im) in whatever way
endThis first implementation is type stable for both purely real, purely imaginary and complex values of tau, while the second isn't.
The reason we opted for For
I’m afraid that might be difficult: we’re trying to get a version released this week, so I don’t think integrating and polishing this in time is very realistic. That said, if your main goal is to get something working for your use case in the next TensorKit release, you could already move ahead with something along the following lines: function myexp(A::AbstractTensorMap)
D, V = eig_full(A)
D.data .= exp.(D.data)
return V * D * inv(V)
endFrom my side, I think it’s important that we take a bit of time here to come up with a good, consistent interface and to think through allocations and long-term maintenance, especially since this will likely influence all of the matrix functions. I’d really like to avoid having to introduce multiple breaking changes later. But that shouldn’t block you from experimenting or using a simpler version for your immediate needs. |
|
Revisiting this after quite some time. I think the last main issue was about the type stability of the different methods. Indeed, we can rewrite
Let me know which of the two you prefer, or whether there is another option that I'm missing here. Additionally, should there be any changes to this PR in light of #QuantumKitHub/TensorKit.jl#342? |
|
I have been reading a bit about matrix exponentials recently, and the conclusion seems to be that, for higher precision, you basically want to evaluate it as a Taylor series (potentially after some rescaling, followed by squaring the result, i.e. exp(A) = (exp_via_taylor_up_to_order_m(A/2^n))^(2^n) where order m and scaling exponent n is chosen according to some cost function. I hope to implement this soon. |
|
I think you left out the third option here, which is to add an |
|
@lkdvos I guess this is equivalent to supporting |
|
yes, this is exactly what I meant! :) |
|
I finally got around to working on this. Some comments
|
| function exponentiali!(τ::Number, A::AbstractMatrix, expA::AbstractMatrix, alg::MatrixFunctionViaEigh) | ||
| check_input(exponentiali!, A, expA, alg) | ||
| function exponentialr!(τ::Number, A::AbstractMatrix, expA::AbstractMatrix, alg::MatrixFunctionViaEigh) | ||
| check_input(exponentialr!, A, expA, alg) | ||
| D, V = eigh_full!(A, alg.eigh_alg) | ||
| expD = map_diagonal(x -> exp(x * im * τ), D) | ||
| if eltype(A) <: Real | ||
| VexpD = V * expD | ||
| expD = map_diagonal(x -> exp(x * τ), D) | ||
| VexpD = V * expD | ||
| if eltype(A) <: Real && eltype(τ) <: Real | ||
| return expA .= real.(VexpD * V') | ||
| else | ||
| VexpD = rmul!(V, expD) | ||
| return mul!(expA, VexpD, V') | ||
| end | ||
| end |
There was a problem hiding this comment.
In the complex case, V was overwritten, such that V' was not the 'original' V', but also contained the exponential. This could be resolved where tau is real, where we could absorb a factor exp(tau * D / 2). Is this something that would be preferred? Then we would have a case where tau is complex and a case where tau is real, where the latter will be further divided based on the eltype of A.
lkdvos
left a comment
There was a problem hiding this comment.
Overall this is looking quite nice! Apologies if this is a premature review, just showed up in my notifications and figured I'd have a look.
The only thing I'm still wondering is if we want to just replace exponentialr(t, A) = exponential((t, A)) and not have the two functions. I know we had this discussion before, I'm not sure what came out of that to be honest.
| # -------------- | ||
| function exponential!(A, expA, alg::MatrixFunctionViaLA) | ||
| check_input(exponential!, A, expA, alg) | ||
| return LinearAlgebra.exp!(A) |
There was a problem hiding this comment.
I know we had a lot of discussions about this in the past, but I think we now settled on actually copying over the data (see also the GLA extension, I think), so this should probably become:
| return LinearAlgebra.exp!(A) | |
| LinearAlgebra.exp!(A) | |
| A === expA || copy!(expA, A) | |
| return expA |
There was a problem hiding this comment.
I know we had a lot of discussions about this in the past, but I think we now settled on actually copying over the data (see also the GLA extension, I think), so this should probably become:
This does not work, for the very bizarre reason that LinearAlgebra.exp!(A) does not guarantee that the result is directly stored in the matrix A (at least when I test it locally with version 1.12.0). Everything is fine if I replace line 74 with A = LinearAlgebra.exp!(A), but probably there is a better way to get around this?
Co-authored-by: Lukas Devos <ldevos98@gmail.com>
Co-authored-by: Lukas Devos <ldevos98@gmail.com>
Co-authored-by: Lukas Devos <ldevos98@gmail.com>
I also don't know whether there was a consensus. I would be in favour of indeed just using |
|
Yes, let's do this and then push this PR over the edge 🎉 |
| if scalar_check | ||
| @check_scalar(expA, A) | ||
| end |
There was a problem hiding this comment.
I added these lines to be able to differentiate the check_input test between exponential(A) and exponential(tau,A), since the latter does not require @check_scalar. Feel free to make suggestions to do this in a different way.
There was a problem hiding this comment.
Why does the latter not require that? If A is real and tau is complex, expA has to be complex right?
There was a problem hiding this comment.
You're right, I forgot about that. I added a method check_input for exponential((tau,A)), where I dispatch on whether tau is real.
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.