|
| 1 | +# Scaling-and-recovering algorithm for the matrix phi-functions, computing |
| 2 | +# phi_0(A), ..., phi_p(A) simultaneously in O(p*n^3) work. |
| 3 | +# |
| 4 | +# This is a port of Algorithm 5.1 of |
| 5 | +# |
| 6 | +# Awad H. Al-Mohy and Xiaobo Liu, "A scaling and recovering algorithm for the |
| 7 | +# matrix phi-functions", arXiv:2506.01193 (2025), |
| 8 | +# |
| 9 | +# and follows the authors' reference MATLAB implementation `phi_funm`. |
| 10 | +# |
| 11 | +# The idea: scale A down by 2^s, evaluate the [m/m] diagonal Pade approximant to |
| 12 | +# phi_p of the scaled matrix, recover the lower-index approximants via the |
| 13 | +# recurrence R^{(j)}_m(z) = z R^{(j+1)}_m(z) + 1/j!, then undo the scaling with |
| 14 | +# the double-argument formula |
| 15 | +# |
| 16 | +# phi_j(2A) = 2^{-j} ( phi_0(A) phi_j(A) + sum_{k=1}^{j} phi_k(A)/(j-k)! ). |
| 17 | + |
| 18 | +# theta[m, p] = theta_{m,p} from Table 3.1 of the paper (largest 1-norm of the |
| 19 | +# scaled matrix for which the [m/m] Pade approximant is backward stable to |
| 20 | +# double-precision unit roundoff). Rows are m = 1:20, columns p = 1:10. |
| 21 | +const _PHI_THETA_MP = [ |
| 22 | + 1.999463452408407e-5 3.7631213142601604e-5 7.366006416045163e-5 0.00014973317297025854 0.0003152443333771182 0.0006855209983764435 0.0015357294906993542 0.0035357368946407606 0.008345789028062234 0.02013882808928226; |
| 23 | + 0.0038062018282832713 0.006090206286125726 0.009869682746779615 0.016211831146383013 0.026984240563843326 0.0454757968381803 0.07749273259855331 0.13324895779231027 0.2304625604362521 0.3988991104549146; |
| 24 | + 0.03971636005661334 0.05806968886880692 0.08534220076759817 0.12612151517169362 0.18736524835661617 0.27955116495245524 0.41822735418681667 0.6258702800351991 0.9335970443986562 1.1616793320890249; |
| 25 | + 0.15442675548312682 0.21278117034577634 0.29371996708854947 0.40617647304246707 0.5623843002320996 0.7787883505754265 1.0464245027100287 1.2572921799132364 1.480350861451984 1.713871003185325; |
| 26 | + 0.37980898016147974 0.5014624976587007 0.6621033824456818 0.8739858777744354 1.110828442901451 1.3375805277521873 1.5770955744229305 1.8273848586096524 2.08673114576671 2.3536689190936406; |
| 27 | + 0.726177195703321 0.9281910159646274 1.1591052927815408 1.4012982671152012 1.6570386251184455 1.924026602416338 2.20029216725632 2.4841706972729956 2.7742685942850143 3.069426294589578; |
| 28 | + 1.1898666361923196 1.4469917284172122 1.7187282676344413 2.0024009817133357 2.295729523138923 2.5968019781751672 2.9040336511997444 3.216121483847106 3.531999855119243 3.8508005672569903; |
| 29 | + 1.7605812331512907 2.060907194742016 2.371480030315257 2.690078917436836 3.014877598333601 3.3443898815994957 3.677415494646134 4.01299056190377 4.350344448873932 4.688863279363638; |
| 30 | + 2.425818958547233 2.7623053687512256 3.105174730511504 3.4527057639100476 3.8035234182863555 4.156538184296475 4.510892686686127 4.865916398054041 5.2210881567935115 5.57600564043454; |
| 31 | + 3.173113456793749 3.5393251225873685 3.9086764034913664 4.279911937822335 4.652058062541427 5.024366624424222 5.396268333440519 5.767334816025469 6.1372481669904 6.505776744744432; |
| 32 | + 3.991025201329815 4.3813599805888455 4.772204432206606 5.162703837030561 5.552220615863406 5.940286915538899 6.326566605567779 6.710825187972159 7.092906173082131 7.4727126397309815; |
| 33 | + 4.869485489784578 5.279199870248916 5.687344501175957 6.09339280009268 6.496977936247796 6.8978550856394785 7.295871912058635 7.690945666690415 8.083045525424248 8.472179024890941; |
| 34 | + 5.799827904547885 6.224971321619904 6.64693480650714 7.065451733211646 7.48036740469753 7.891610269499859 8.29916985381997 8.703079946679036 9.103405852177376 9.500234768451538; |
| 35 | + 6.774681996458441 7.211997240187581 7.644910128682907 8.073354767800579 8.497338949398504 8.916922998846443 9.332203860678709 9.74330319935391 10.150358556572542 10.5535168239286; |
| 36 | + 7.787817051572435 8.234634975085694 8.676142196766529 9.112424389503664 9.543611219824884 9.969861373150518 10.391351525913848 10.808268293369911 11.220802409044003 11.629144570605774; |
| 37 | + 8.833978214833847 9.28811956766922 9.736292792866143 10.178695474892383 10.615546677548995 11.04707680323746 11.473520338548882 11.895110742359616 12.312076916038997 12.724640835984562; |
| 38 | + 9.908733823133844 10.368423129623023 10.821685340709736 11.26879859255289 11.710045790427412 12.145708155762449 12.576060822320501 13.001369927621694 13.421890788016032 13.837866852904227; |
| 39 | + 11.008341035221061 11.472133539821815 11.929195701618417 12.379861767254168 12.824458619109002 13.263302077220441 13.696694607678852 14.124924034990169 14.548262963142152 14.966968689718191; |
| 40 | + 12.129631147167878 12.596352058386916 13.056160721729096 13.509428665638305 13.956511465304834 14.397747062985934 14.83345501195776 15.263936358537684 15.689473955602649 16.110333058994453; |
| 41 | + 13.269913405576162 13.738607964511967 14.20030229038679 14.655390835644507 15.104246216977073 15.547218984875734 15.984637960610213 16.41681094336813 16.84402564730148 17.266550769620974; |
| 42 | +] |
| 43 | + |
| 44 | +# theta_{m,p} with the p>7 rule of the paper: for p>7 the p=7 column is used, and |
| 45 | +# indices are guarded so m outside 1:20 does not error. |
| 46 | +@inline function _phi_theta(m::Integer, p::Integer) |
| 47 | + return _PHI_THETA_MP[m, p > 7 ? 7 : p] |
| 48 | +end |
| 49 | + |
| 50 | +# k! as a Float64 (k small; avoids Int overflow for the recovery coefficients). |
| 51 | +@inline function _factf(k::Integer) |
| 52 | + r = 1.0 |
| 53 | + for i in 2:k |
| 54 | + r *= i |
| 55 | + end |
| 56 | + return r |
| 57 | +end |
| 58 | + |
| 59 | +# Exact 1-norm of B^k for an entrywise-nonnegative real B, via ‖B^k‖_1 = |
| 60 | +# ‖(B^T)^k 𝟙‖_∞ (k matrix-vector products, no explicit power). |
| 61 | +function _normpow_nonneg(B::AbstractMatrix{<:Real}, k::Integer) |
| 62 | + n = size(B, 1) |
| 63 | + Bt = transpose(B) |
| 64 | + v = ones(eltype(B), n) |
| 65 | + tmp = similar(v) |
| 66 | + for _ in 1:k |
| 67 | + mul!(tmp, Bt, v) |
| 68 | + v, tmp = tmp, v |
| 69 | + end |
| 70 | + return maximum(v) |
| 71 | +end |
| 72 | + |
| 73 | +# Scaling parameter `t` derived from the first term of the backward-error series |
| 74 | +# (Eq. (3.12)); mirrors the `ell` subfunction of the reference implementation. |
| 75 | +function _phi_ell(A::AbstractMatrix, m::Integer, p::Integer, phat::Integer) |
| 76 | + normT = opnorm(A, 1) |
| 77 | + normT == 0 && return 0 |
| 78 | + t0 = normT > 1 ? log2(normT) : 0.0 |
| 79 | + scalefac = exp2(t0) |
| 80 | + normTs = normT / scalefac |
| 81 | + delta = (p - 1) * (p - phat) / p + 1 |
| 82 | + coeff = (_factf(m + p) / _factf(2m + p)) * (_factf(m) / _factf(2m + p + 1)) |
| 83 | + K = 2m + p + 1 |
| 84 | + c = (coeff / normTs^delta)^(1 / K) |
| 85 | + scaledT = c .* abs.(A ./ scalefac) |
| 86 | + alpha = _normpow_nonneg(scaledT, K) |
| 87 | + t = log2(2alpha / eps(Float64)) / (K - delta) + t0 |
| 88 | + return max(ceil(Int, t), 0) |
| 89 | +end |
| 90 | + |
| 91 | +# Select the Pade degree m, scaling parameter s, and Paterson-Stockmeyer block |
| 92 | +# size tau that minimize the equivalent number of matrix products (Section 4). |
| 93 | +function _select_parameters_phi(A::AbstractMatrix, p::Integer) |
| 94 | + m_max = 12 |
| 95 | + i_max = ceil(Int, sqrt(8 * (m_max + 1)) - 3) - 1 |
| 96 | + m_max = (i_max + 3)^2 ÷ 8 |
| 97 | + phat = _phi_theta(m_max, p) < 1 ? 0 : p |
| 98 | + r_max = floor(Int, (1 + sqrt(1 + 4 * (2m_max + phat + 1))) / 2) |
| 99 | + |
| 100 | + # eta[j] = ‖A^{j+1}‖_1^{1/(j+1)} for j = 1:r_max, forming the powers exactly. |
| 101 | + # (The powers are low, j+1 ≤ r_max+1 ≲ 8; the cost is negligible next to the |
| 102 | + # rest of the algorithm and never underestimates, keeping s conservative.) |
| 103 | + eta = zeros(Float64, r_max) |
| 104 | + P = A * A |
| 105 | + eta[1] = opnorm(P, 1)^(1 / 2) |
| 106 | + for j in 2:r_max |
| 107 | + P = P * A |
| 108 | + eta[j] = opnorm(P, 1)^(1 / (j + 1)) |
| 109 | + end |
| 110 | + alpha = [max(eta[j], eta[j + 1]) for j in 1:(r_max - 1)] |
| 111 | + |
| 112 | + Cost = zeros(Float64, i_max + 1, r_max - 1) |
| 113 | + for i in 0:i_max |
| 114 | + m_i = (i + 3)^2 ÷ 8 |
| 115 | + θ = _phi_theta(m_i, p) |
| 116 | + phat_i = θ < 1 ? 0 : p |
| 117 | + t = _phi_ell(A, m_i, p, phat_i) |
| 118 | + for r in 2:r_max |
| 119 | + if 2m_i + phat_i + 1 >= r * (r - 1) |
| 120 | + a = alpha[r - 1] |
| 121 | + s0 = (a > 0 && isfinite(a)) ? max(ceil(Int, log2(a / θ)), t) : t |
| 122 | + Cost[i + 1, r - 1] = i + p + s0 * (p + 1) |
| 123 | + end |
| 124 | + end |
| 125 | + end |
| 126 | + |
| 127 | + minval = Inf |
| 128 | + for v in Cost |
| 129 | + if v > 0 && v < minval |
| 130 | + minval = v |
| 131 | + end |
| 132 | + end |
| 133 | + # Match the reference's column-major "last" tie-break for reproducibility. |
| 134 | + idx = 0 |
| 135 | + for (j, v) in enumerate(Cost) |
| 136 | + v == minval && (idx = j) |
| 137 | + end |
| 138 | + i_star = (idx - 1) % (i_max + 1) |
| 139 | + m = (i_star + 3)^2 ÷ 8 |
| 140 | + s = round(Int, (minval - i_star - p) / (p + 1)) |
| 141 | + |
| 142 | + tau = floor(Int, sqrt(2m)) |
| 143 | + if tau - 1 + 2 * (m ÷ tau) - 2 * (m % tau == 0) != i_star |
| 144 | + tau = ceil(Int, sqrt(2m)) |
| 145 | + end |
| 146 | + return m, s, tau |
| 147 | +end |
| 148 | + |
| 149 | +# Renormalized [m/m] Pade coefficients (numerator and denominator, low order |
| 150 | +# first) of the approximant to phi_p. Follows the recurrences of Berland, |
| 151 | +# Skaflestad, and Wright used in the reference `pade_coef`. |
| 152 | +function _phi_pade_coef(m::Integer, p::Integer) |
| 153 | + n1 = prod(Float64.((m + 1):(2m + 1))) # (2m+1)!/m! |
| 154 | + d1 = n1 |
| 155 | + Ncoef = Vector{Float64}(undef, m + 1) |
| 156 | + Dcoef = Vector{Float64}(undef, m + 1) |
| 157 | + Amat = Matrix{Float64}(undef, m + 1, m + 1) |
| 158 | + for k in 1:p |
| 159 | + if k == p |
| 160 | + # First column: Amat[r,1] = n1 * prod_{l=1}^{r-1} 1/(k+l). |
| 161 | + cp = 1.0 |
| 162 | + Amat[1, 1] = n1 |
| 163 | + for r in 2:(m + 1) |
| 164 | + cp /= (k + (r - 1)) |
| 165 | + Amat[r, 1] = n1 * cp |
| 166 | + end |
| 167 | + for c in 2:(m + 1), r in 2:(m + 1) |
| 168 | + I = r - 1 |
| 169 | + J = c - 1 |
| 170 | + Amat[r, c] = -(m + 1 - J) * (k + 1 + I - J) / ((2m + k + 1 - J) * J) |
| 171 | + end |
| 172 | + for r in 1:(m + 1) |
| 173 | + s = 0.0 |
| 174 | + pr = 1.0 |
| 175 | + for c in 1:r |
| 176 | + pr *= Amat[r, c] |
| 177 | + s += pr |
| 178 | + end |
| 179 | + Ncoef[r] = s |
| 180 | + end |
| 181 | + Dcoef[1] = d1 |
| 182 | + cpd = 1.0 |
| 183 | + for r in 2:(m + 1) |
| 184 | + i = r - 1 |
| 185 | + cpd *= -(m + 1 - i) / (i * (2m + k + 1 - i)) |
| 186 | + Dcoef[r] = d1 * cpd |
| 187 | + end |
| 188 | + end |
| 189 | + n1 = n1 * (2m + k + 1) / (k + 1) |
| 190 | + d1 = d1 * (2m + k + 1) |
| 191 | + end |
| 192 | + return Ncoef, Dcoef |
| 193 | +end |
| 194 | + |
| 195 | +# Simultaneously evaluate the numerator N(A) and denominator D(A) of the Pade |
| 196 | +# approximant via the Paterson-Stockmeyer scheme with block size tau. |
| 197 | +function _paterson_stockmeyer(A::AbstractMatrix{T}, Nc, Dc, tau::Integer) where {T} |
| 198 | + m = length(Nc) - 1 |
| 199 | + n = size(A, 1) |
| 200 | + Apow = Vector{Matrix{T}}(undef, tau + 1) |
| 201 | + Apow[1] = Matrix{T}(I, n, n) |
| 202 | + Apow[2] = Matrix(A) |
| 203 | + for i in 3:(tau + 1) |
| 204 | + Apow[i] = Apow[i - 1] * A |
| 205 | + end |
| 206 | + Atau = copy(Apow[tau + 1]) |
| 207 | + N = zeros(T, n, n) |
| 208 | + D = zeros(T, n, n) |
| 209 | + nu = m ÷ tau |
| 210 | + for i in 0:nu |
| 211 | + start = i * tau + 1 |
| 212 | + stop = min((i + 1) * tau, m + 1) |
| 213 | + blkN = zeros(T, n, n) |
| 214 | + blkD = zeros(T, n, n) |
| 215 | + for l in 1:(stop - start + 1) |
| 216 | + blkN .+= Nc[start + l - 1] .* Apow[l] |
| 217 | + blkD .+= Dc[start + l - 1] .* Apow[l] |
| 218 | + end |
| 219 | + if i == 0 |
| 220 | + N .+= blkN |
| 221 | + D .+= blkD |
| 222 | + else |
| 223 | + N .+= blkN * Atau |
| 224 | + D .+= blkD * Atau |
| 225 | + Atau = Atau * Apow[tau + 1] |
| 226 | + end |
| 227 | + end |
| 228 | + return N, D |
| 229 | +end |
| 230 | + |
| 231 | +""" |
| 232 | + _phi_almohy!(out, A, p) -> out |
| 233 | +
|
| 234 | +Simultaneously compute `phi_0(A), phi_1(A), ..., phi_p(A)` for a dense matrix `A` |
| 235 | +(`p >= 1`) using the scaling-and-recovering algorithm of Al-Mohy and Liu |
| 236 | +(arXiv:2506.01193). `out` must be a length-`p+1` vector of `size(A)` matrices; on |
| 237 | +return `out[j+1] == phi_j(A)`. |
| 238 | +
|
| 239 | +The cost is `O(p n^3)`, in contrast to the `O(n (n+p)^3)` of the basis-vector |
| 240 | +approach in [`phi!`](@ref). |
| 241 | +""" |
| 242 | +function _phi_almohy!( |
| 243 | + out::AbstractVector{<:AbstractMatrix}, A::AbstractMatrix{T}, |
| 244 | + p::Integer |
| 245 | + ) where {T} |
| 246 | + n = size(A, 1) |
| 247 | + m, s, tau = _select_parameters_phi(A, p) |
| 248 | + As = A ./ exp2(s) |
| 249 | + Nc, Dc = _phi_pade_coef(m, p) |
| 250 | + Nm, Dm = _paterson_stockmeyer(As, Nc, Dc, tau) |
| 251 | + |
| 252 | + Rm = Vector{Matrix{T}}(undef, p + 1) |
| 253 | + Rm[p + 1] = Dm \ Nm |
| 254 | + # Recurrence (2.9): R^{(j)} = As R^{(j+1)} + I/j!, j = p-1 : -1 : 0. |
| 255 | + for k in p:-1:1 |
| 256 | + R = As * Rm[k + 1] |
| 257 | + f = 1 / _factf(k - 1) |
| 258 | + @inbounds for d in 1:n |
| 259 | + R[d, d] += f |
| 260 | + end |
| 261 | + Rm[k] = R |
| 262 | + end |
| 263 | + |
| 264 | + # Undo the scaling with the double-argument formula (2.10), s times. |
| 265 | + for _ in 1:s |
| 266 | + for j in p:-1:1 |
| 267 | + M = Rm[1] * Rm[j + 1] |
| 268 | + for k in 1:j |
| 269 | + M .+= (1 / _factf(j - k)) .* Rm[k + 1] |
| 270 | + end |
| 271 | + M ./= exp2(j) |
| 272 | + Rm[j + 1] = M |
| 273 | + end |
| 274 | + Rm[1] = Rm[1] * Rm[1] |
| 275 | + end |
| 276 | + |
| 277 | + for j in 0:p |
| 278 | + copyto!(out[j + 1], Rm[j + 1]) |
| 279 | + end |
| 280 | + return out |
| 281 | +end |
0 commit comments