-
Notifications
You must be signed in to change notification settings - Fork 7
Taylor series native exponential implementation
#243
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?
Changes from all commits
3c92f54
f9893c5
2a78be4
ab625a4
463fd84
7184550
fb5a1cf
00ab0d5
8f13b77
fa8a6e1
283f617
881a01e
ee2927c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """ | ||
| balance!(A::AbstractMatrix; maxiter=100) -> A, scale | ||
|
|
||
| Balance the square matrix `A` in place through a diagonal similarity `A ← D⁻¹ A D` that reduces its norm. | ||
| Each sweep computes, for every index at once, the power-of-two factor that best equalizes the | ||
| off-diagonal row and column norms (a simultaneous, Osborne-style variant of the Parlett–Reinsch scaling), | ||
| and applies all factors together; sweeps repeat until no index changes or `maxiter` is reached. | ||
|
|
||
| The scaling factors are integer powers of two, the machine radix, so that the transformation is exact even | ||
| in floating point arithmetic. The row and column norms are simultaneously adapted to avoid scalar indexing | ||
| and lots of kernel calls on GPUs. | ||
|
|
||
| The returned `scale` holds the diagonal of `D`, so that a matrix function of the original | ||
| input can be recovered from a matrix function `f` of the balanced matrix through | ||
| `D f(D⁻¹AD) D⁻¹`, i.e. `expA[i, j] = scale[i] * f(B)[i, j] / scale[j]`. | ||
| """ | ||
| function balance!(A::AbstractMatrix{T}; maxiter::Integer = 100) where {T} | ||
| n = LinearAlgebra.checksquare(A) | ||
| R = real(T) | ||
| β = convert(R, 2) | ||
| logβ = log(β) | ||
| scale = fill!(similar(A, R, n), one(R)) | ||
|
|
||
| colnorm = similar(A, R, n) | ||
| rownorm = similar(A, R, n) | ||
| f = similar(A, R, n) | ||
| colsum = reshape(colnorm, 1, n) | ||
| rowsum = reshape(rownorm, n, 1) | ||
| d = abs.(diagview(A)) | ||
| fᵀ = transpose(f) | ||
|
|
||
| for _ in 1:maxiter | ||
| fill!(colsum, zero(R)) | ||
| Base.mapreducedim!(abs, +, colsum, A) | ||
| fill!(rowsum, zero(R)) | ||
| Base.mapreducedim!(abs, +, rowsum, A) | ||
| colnorm .-= d | ||
| rownorm .-= d | ||
| f .= _balance_factor.(colnorm, rownorm, β, logβ) | ||
| all(isone, f) && break | ||
| # apply Aᵢⱼ ← Aᵢⱼ fⱼ / fᵢ (i.e. column j scaled by fⱼ, row i by 1/fᵢ) and accumulate. | ||
| A .= A .* fᵀ ./ f | ||
| scale .*= f | ||
| end | ||
|
|
||
| return A, scale | ||
| end | ||
|
|
||
| # Nearest power-of-`β` factor `f` that equalizes the scaled off-diagonal norms | ||
| # `colnorm·f` and `rownorm/f`, kept only when it reduces their sum (avoids oscillation and | ||
| # leaves degenerate rows/columns untouched). | ||
| @inline function _balance_factor(colnorm::R, rownorm::R, β::R, logβ::R) where {R} | ||
| (colnorm > 0 && rownorm > 0) || return one(R) | ||
| f = β^round(log(rownorm / colnorm) / (2 * logβ)) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the role of
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Did some digging: this is also what LAPACK
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok makes sense. Just wondering whether there is any point in using floating points for
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Anyway, I have to go now, and there are some other aspects of this PR that I would like to understand a bit better, so I'll try to get back to it tonight.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I hardcoded the base to be 2 now - this is still assuming that multiplication/division by powers of 2 are exact in floating arithmetic, and I don't really foresee any element types where this is not the case for now. I was experimenting slightly with Base's |
||
| return (colnorm * f + rownorm / f) < convert(R, 19 // 20) * (colnorm + rownorm) ? f : one(R) | ||
| end | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -114,3 +114,139 @@ function exponential!((τ, A)::Tuple{Number, AbstractMatrix}, expA, alg::Diagona | |||||||||||||||||||||||||||||
| check_input(exponential!, (τ, A), expA, alg) | ||||||||||||||||||||||||||||||
| return map_diagonal!(x -> exp(x * τ), expA, A) | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Taylor logic | ||||||||||||||||||||||||||||||
| # ------------ | ||||||||||||||||||||||||||||||
| function exponential!(A::AbstractMatrix, expA, alg::MatrixFunctionViaTaylor) | ||||||||||||||||||||||||||||||
| check_input(exponential!, A, expA, alg) | ||||||||||||||||||||||||||||||
| m = LinearAlgebra.checksquare(A) | ||||||||||||||||||||||||||||||
| T = eltype(A) | ||||||||||||||||||||||||||||||
| R = real(T) | ||||||||||||||||||||||||||||||
| tol = convert(R, get(alg.kwargs, :tol, eps(R))) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| dobalance = get(alg.kwargs, :balance, false) | ||||||||||||||||||||||||||||||
| scale = dobalance ? balance!(A)[2] : nothing | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Form a minimal set of powers of A up front and use them to sharpen the norm estimate | ||||||||||||||||||||||||||||||
| # through the Al-Mohy–Higham quantities dₚ = ‖Aᵖ‖^(1/p). | ||||||||||||||||||||||||||||||
| p₀ = min(convert(Int, get(alg.kwargs, :estimate_order, 4)), m) | ||||||||||||||||||||||||||||||
| powers = Vector{typeof(A)}(undef, p₀) | ||||||||||||||||||||||||||||||
| powers[1] = A | ||||||||||||||||||||||||||||||
| d = Vector{R}(undef, p₀) | ||||||||||||||||||||||||||||||
| d[1] = LinearAlgebra.opnorm(A, 1) | ||||||||||||||||||||||||||||||
| iszero(d[1]) && return one!(expA) | ||||||||||||||||||||||||||||||
| for p in 2:p₀ | ||||||||||||||||||||||||||||||
| powers[p] = powers[p - 1] * A | ||||||||||||||||||||||||||||||
| d[p] = LinearAlgebra.opnorm(powers[p], 1)^(1 / p) | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| order, squarings = taylor_order_and_squarings(d, tol) | ||||||||||||||||||||||||||||||
| blocksize = ceil(Int, sqrt(order + 1)) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Rescale the (≤ p₀) powers we hold into powers of A/2ˢ | ||||||||||||||||||||||||||||||
| resize!(powers, min(p₀, blocksize)) | ||||||||||||||||||||||||||||||
| if squarings > 0 | ||||||||||||||||||||||||||||||
| f = inv(convert(T, 2)^squarings) | ||||||||||||||||||||||||||||||
| fk = one(T) | ||||||||||||||||||||||||||||||
| for k in 1:length(powers) | ||||||||||||||||||||||||||||||
| fk *= f | ||||||||||||||||||||||||||||||
| rmul!(powers[k], fk) # powers[k] ← (A/2ˢ)ᵏ | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| # Extend to `blocksize` powers | ||||||||||||||||||||||||||||||
| for p in (length(powers) + 1):blocksize | ||||||||||||||||||||||||||||||
| push!(powers, powers[p - 1] * powers[1]) | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Paterson–Stockmeyer evaluation, followed by squaring | ||||||||||||||||||||||||||||||
| X = taylor_polynomial(powers, order) | ||||||||||||||||||||||||||||||
| Y = expA # can reuse this memory | ||||||||||||||||||||||||||||||
| for _ in 1:squarings | ||||||||||||||||||||||||||||||
| mul!(Y, X, X) | ||||||||||||||||||||||||||||||
| X, Y = Y, X | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| if dobalance | ||||||||||||||||||||||||||||||
| expA .= scale .* X ./ transpose(scale) | ||||||||||||||||||||||||||||||
| else | ||||||||||||||||||||||||||||||
| X === expA || copyto!(expA, X) | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| return expA | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Truncation order `m` and number of squarings `s` minimizing the Paterson–Stockmeyer | ||||||||||||||||||||||||||||||
| # matrix-multiplication count subject to the Taylor remainder bound `(θ/2ˢ)ᵐ⁺¹/(m+1)! ≤ tol`. | ||||||||||||||||||||||||||||||
| # | ||||||||||||||||||||||||||||||
| # The effective norm `θ` per candidate order is the sharpest Al-Mohy–Higham quantity | ||||||||||||||||||||||||||||||
| # `αₚ = max(dₚ, dₚ₊₁)` (with `dₚ = ‖Aᵖ‖^(1/p)`) that is valid for that order: since every | ||||||||||||||||||||||||||||||
| # `k ≥ p(p-1)` is a nonnegative combination of `p` and `p+1`, `‖Aᵏ‖ ≤ αₚᵏ` holds for `k ≥ p(p-1)`, | ||||||||||||||||||||||||||||||
| # so `αₚ` bounds the degree-`m` remainder whenever `m+1 ≥ p(p-1)`. The `length(d)` powers already | ||||||||||||||||||||||||||||||
| # formed count as free in the cost model. | ||||||||||||||||||||||||||||||
| function taylor_order_and_squarings(d::AbstractVector{<:Real}, tol::Real) | ||||||||||||||||||||||||||||||
| p₀ = length(d) | ||||||||||||||||||||||||||||||
| log2tol = log2(Float64(tol)) | ||||||||||||||||||||||||||||||
| log2factorial = 0.0 | ||||||||||||||||||||||||||||||
| best_order = 1 | ||||||||||||||||||||||||||||||
| best_squarings = 0 | ||||||||||||||||||||||||||||||
| best_cost = typemax(Int) | ||||||||||||||||||||||||||||||
| for order in 1:100 | ||||||||||||||||||||||||||||||
| log2factorial += log2(order + 1) | ||||||||||||||||||||||||||||||
| # sharpest valid αₚ = min over p with p(p-1) ≤ order+1 and p+1 ≤ p₀ | ||||||||||||||||||||||||||||||
| θ = Float64(d[1]) | ||||||||||||||||||||||||||||||
| for p in 1:(p₀ - 1) | ||||||||||||||||||||||||||||||
| p * (p - 1) ≤ order + 1 || break | ||||||||||||||||||||||||||||||
| θ = min(θ, max(Float64(d[p]), Float64(d[p + 1]))) | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| # `θ == 0` (a nilpotent Aᵖ) makes the remainder vanish exactly ⇒ no squarings needed; | ||||||||||||||||||||||||||||||
| # guard the `log2(0) = -Inf` before it reaches `ceil(Int, ⋅)`. | ||||||||||||||||||||||||||||||
| excess = log2(θ) - (log2tol + log2factorial) / (order + 1) | ||||||||||||||||||||||||||||||
| squarings = excess > 0 ? ceil(Int, excess) : 0 | ||||||||||||||||||||||||||||||
| blocksize = ceil(Int, sqrt(order + 1)) | ||||||||||||||||||||||||||||||
| cost = max(0, blocksize - p₀) + (cld(order + 1, blocksize) - 1) + squarings | ||||||||||||||||||||||||||||||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have two comments:
with A[i] indicating A^i, and A[i]:A[j] just the sum of terms of these powers. So indeed, as remarked in my earlier comment, the number of "blocks" is
However, you can easily see that for the same cost we can build an order 16 polynomial as well, namely by doing the blocking differently as
i.e. by separating the very first term in the Taylor decomposition. Alternatively, you can also modify the inner most block to do
So actually I think the cost model should be: blocksize = max(p₀, ceil(Int, sqrt(order))) # use that you anyway have `p₀` powers available for free
cost = max(0, blocksize - p₀) + (cld(order, blocksize) - 1) + squaring
orders = 1:100
costs = map(orders) do order
blocksize = max(p₀, ceil(Int, sqrt(order)))
return max(0, blocksize - p₀) + (cld(order, blocksize) - 1)
end
hcat(orders, costs)then you get the cost per order, not counting the coming from https://www.sciencedirect.com/science/article/pii/S0024379519301454 By looping over all orders, you would of course find the optimal one, but with too much work. Of course, in order to avoid this, one needs to tabulate the optimal values of |
||||||||||||||||||||||||||||||
| if cost < best_cost | ||||||||||||||||||||||||||||||
| best_cost = cost | ||||||||||||||||||||||||||||||
| best_order = order | ||||||||||||||||||||||||||||||
| best_squarings = squarings | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| return best_order, best_squarings | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Evaluate ∑ₖ₌₀ᵐ Aᵏ/k! via the Paterson–Stockmeyer scheme, returning a freshly allocated matrix. | ||||||||||||||||||||||||||||||
| # `powers` holds A, A², …, A^blocksize (already scaled), with blocksize = ceil(√(order+1)). | ||||||||||||||||||||||||||||||
| function taylor_polynomial(powers::AbstractVector{<:AbstractMatrix}, order::Integer) | ||||||||||||||||||||||||||||||
| A = powers[1] | ||||||||||||||||||||||||||||||
| T = eltype(A) | ||||||||||||||||||||||||||||||
| blocksize = length(powers) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| invfactorial = Vector{T}(undef, order + 1) | ||||||||||||||||||||||||||||||
| invfactorial[1] = one(T) | ||||||||||||||||||||||||||||||
| for k in 1:order | ||||||||||||||||||||||||||||||
| invfactorial[k + 1] = invfactorial[k] / k | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| result = similar(A) | ||||||||||||||||||||||||||||||
| block = similar(A) | ||||||||||||||||||||||||||||||
| numblocks = fld(order, blocksize) | ||||||||||||||||||||||||||||||
| taylor_block!(result, powers, invfactorial, numblocks, blocksize, order) | ||||||||||||||||||||||||||||||
| for j in (numblocks - 1):-1:0 | ||||||||||||||||||||||||||||||
| mul!(block, result, powers[blocksize]) | ||||||||||||||||||||||||||||||
| taylor_block!(result, powers, invfactorial, j, blocksize, order) | ||||||||||||||||||||||||||||||
| result .+= block | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
Comment on lines
+231
to
+237
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the name Indeed, my way of understanding the number of blocks is by reasoning that there are
Suggested change
Rather than passing
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In fact,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But that is referring to what I would call |
||||||||||||||||||||||||||||||
| return result | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| # Sub-polynomial ∑ᵢ₌₀ᵇ⁻¹ c_{jb+i} Aⁱ of degree `blocksize - 1`, written into `out`. | ||||||||||||||||||||||||||||||
| function taylor_block!(out::AbstractMatrix, powers, invfactorial, j::Integer, blocksize::Integer, order::Integer) | ||||||||||||||||||||||||||||||
| base = j * blocksize | ||||||||||||||||||||||||||||||
| fill!(out, zero(eltype(out))) | ||||||||||||||||||||||||||||||
| diagview(out) .= invfactorial[base + 1] | ||||||||||||||||||||||||||||||
| for i in 1:(blocksize - 1) | ||||||||||||||||||||||||||||||
| k = base + i | ||||||||||||||||||||||||||||||
| k > order && break | ||||||||||||||||||||||||||||||
| out .+= invfactorial[k + 1] .* powers[i] | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||
| return out | ||||||||||||||||||||||||||||||
| end | ||||||||||||||||||||||||||||||

Uh oh!
There was an error while loading. Please reload this page.