Skip to content

Commit 7a1a8cc

Browse files
authored
Fix mutating bug in expm! (#52)
* Fix bug with mutating A, add expm() * Add mul! methods for expm! * Test expm! and expm
1 parent 594f693 commit 7a1a8cc

3 files changed

Lines changed: 108 additions & 32 deletions

File tree

src/blochmatrix.jl

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,7 @@ function Base.copyto!(dst::FreePrecessionMatrix, src::FreePrecessionMatrix)
244244

245245
end
246246

247+
247248
"""
248249
ExchangeDynamicsMatrix(r)
249250
ExchangeDynamicsMatrix{T}()
@@ -978,13 +979,25 @@ function LinearAlgebra.mul!(A::BlochDynamicsMatrix, t::Real)
978979

979980
end
980981

982+
function LinearAlgebra.mul!(C::BlochDynamicsMatrix, A::BlochDynamicsMatrix, t::Real)
983+
C.R1 = A.R1 * t
984+
C.R2 = A.R2 * t
985+
C.Δω = A.Δω * t
986+
return nothing
987+
end
988+
981989
function LinearAlgebra.mul!(E::ExchangeDynamicsMatrix, t::Real)
982990

983991
E.r *= t
984992
return nothing
985993

986994
end
987995

996+
function LinearAlgebra.mul!(C::ExchangeDynamicsMatrix, A::ExchangeDynamicsMatrix, t::Real)
997+
C.r = A.r * t
998+
return nothing
999+
end
1000+
9881001
function LinearAlgebra.mul!(A::BlochMcConnellDynamicsMatrix, t::Real)
9891002

9901003
for A in A.A
@@ -996,6 +1009,21 @@ function LinearAlgebra.mul!(A::BlochMcConnellDynamicsMatrix, t::Real)
9961009

9971010
end
9981011

1012+
# used in expm!()
1013+
# should work when B is Real or a dual Number
1014+
function LinearAlgebra.mul!(
1015+
C::BlochMcConnellDynamicsMatrix{T1,N}, A::BlochMcConnellDynamicsMatrix{T2,N}, B,
1016+
) where {T1, T2, N}
1017+
1018+
for (C, A) in zip(C.A, A.A)
1019+
mul!(C, A, B)
1020+
end
1021+
for (C, A) in zip(C.E, A.E)
1022+
mul!(C, A, B)
1023+
end
1024+
end
1025+
1026+
9991027
"""
10001028
mul!(C, A, B)
10011029

src/expm.jl

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ function expmchk()
2828
return m_vals, theta
2929
end
3030

31+
3132
function getPadeCoefficients(m)
3233
# GETPADECOEFFICIENTS Coefficients of numerator P of Pade approximant
3334
# C = GETPADECOEFFICIENTS returns coefficients of numerator
@@ -51,7 +52,9 @@ function getPadeCoefficients(m)
5152
return c
5253
end
5354

55+
5456
struct MatrixExponentialWorkspace{T<:Real,N}
57+
Ascaled::BlochMcConnellDynamicsMatrix{T,N} # A / 2^s
5558
expA2::BlochMcConnellMatrix{T,N}
5659
A2::BlochMcConnellMatrix{T,N}
5760
A4::BlochMcConnellMatrix{T,N}
@@ -65,18 +68,15 @@ struct MatrixExponentialWorkspace{T<:Real,N}
6568
mat2::Matrix{T}
6669
end
6770

68-
MatrixExponentialWorkspace{T}(N) where {T} =
69-
MatrixExponentialWorkspace(BlochMcConnellMatrix{T}(N),
70-
BlochMcConnellMatrix{T}(N),
71-
BlochMcConnellMatrix{T}(N),
72-
BlochMcConnellMatrix{T}(N),
73-
BlochMcConnellMatrix{T}(N),
74-
BlochMcConnellMatrix{T}(N),
75-
BlochMcConnellMatrix{T}(N),
76-
BlochMcConnellMatrix{T}(N),
77-
BlochMcConnellMatrix{T}(N),
78-
Matrix{T}(undef, 3N, 3N),
79-
Matrix{T}(undef, 3N, 3N))
71+
72+
# constructor
73+
MatrixExponentialWorkspace{T}(N) where {T} = MatrixExponentialWorkspace(
74+
BlochMcConnellDynamicsMatrix{T}(N),
75+
ntuple(_ -> BlochMcConnellMatrix{T}(N), 9)...,
76+
Matrix{T}(undef, 3N, 3N),
77+
Matrix{T}(undef, 3N, 3N),
78+
)
79+
8080

8181
"""
8282
expm!(expA, A, [workspace])
@@ -101,10 +101,10 @@ function expm!(
101101

102102
normA = absolutesum(A)
103103

104-
if normA <= theta[end]
104+
if normA theta[end]
105105
# no scaling and squaring is required
106-
for i = 1:length(m_vals)
107-
if normA <= theta[i]
106+
for i in 1:length(m_vals)
107+
if normA theta[i]
108108
PadeApproximantOfDegree!(expA, A, workspace, m_vals[i])
109109
break
110110
end
@@ -114,28 +114,35 @@ function expm!(
114114
t = frexp1(tmp)
115115
s = frexp2(tmp)
116116
s = s - (t == 0.5) # adjust s if normA / theta[end] is a power of 2
117-
mul!(A, 1 / 2^s) # Scaling
118-
PadeApproximantOfDegree!(expA, A, workspace, m_vals[end])
117+
mul!(workspace.Ascaled, A, 1 / 2^s) # scaling
118+
PadeApproximantOfDegree!(expA, workspace.Ascaled, workspace, m_vals[end])
119119

120-
for i = 1:s
121-
mul!(workspace.expA2, expA, expA) # Squaring
120+
for i in 1:s
121+
mul!(workspace.expA2, expA, expA) # squaring
122122
copyto!(expA, workspace.expA2)
123123
end
124124
end
125125

126126
end
127127

128+
129+
"""
130+
PadeApproximantOfDegree!(expA, A, workspace, m)
131+
132+
Pade approximant to exponential.
133+
134+
Based on `PADEAPPROXIMANTOFDEGREE`
135+
`F = PADEAPPROXIMANTOFDEGREE(M)` is the degree M diagonal
136+
Pade approximant to EXP(A), where M = 3, 5, 7, 9 or 13.
137+
Series are evaluated in decreasing order of powers,
138+
which is in approx. increasing order of maximum norms of the terms.
139+
"""
128140
function PadeApproximantOfDegree!(
129141
expA::BlochMcConnellMatrix{T1,N},
130142
A::BlochMcConnellDynamicsMatrix{T2,N,M},
131143
workspace::MatrixExponentialWorkspace{T3,N},
132144
m::Integer
133145
) where {T1,T2,T3,N,M}
134-
#PADEAPPROXIMANTOFDEGREE Pade approximant to exponential.
135-
# F = PADEAPPROXIMANTOFDEGREE(M) is the degree M diagonal
136-
# Pade approximant to EXP(A), where M = 3, 5, 7, 9 or 13.
137-
# Series are evaluated in decreasing order of powers, which is
138-
# in approx. increasing order of maximum norms of the terms.
139146

140147
n = 3N
141148
c = getPadeCoefficients(m)
@@ -146,7 +153,7 @@ function PadeApproximantOfDegree!(
146153

147154
# Evaluate Pade approximant
148155
if m == 13
149-
# For optimal evaluation need different formula for m >= 12
156+
# For optimal evaluation need different formula for m 12
150157
mul!(workspace.tmp1, workspace.A6, c[14])
151158
muladd!(workspace.tmp1, workspace.A4, c[12])
152159
muladd!(workspace.tmp1, workspace.A2, c[10])
@@ -169,16 +176,16 @@ function PadeApproximantOfDegree!(
169176
fill!(workspace.tmp1, zero(T3))
170177
fill!(workspace.V, zero(T3))
171178

172-
if m >= 9
179+
if m 9
173180
mul!(workspace.A8, workspace.A2, workspace.A6)
174181
muladd!(workspace.tmp1, workspace.A8, c[10])
175182
muladd!(workspace.V, workspace.A8, c[9])
176183
end
177-
if m >= 7
184+
if m 7
178185
muladd!(workspace.tmp1, workspace.A6, c[8])
179186
muladd!(workspace.V, workspace.A6, c[7])
180187
end
181-
if m >= 5
188+
if m 5
182189
muladd!(workspace.tmp1, workspace.A4, c[6])
183190
muladd!(workspace.V, workspace.A4, c[5])
184191
end
@@ -197,6 +204,25 @@ function PadeApproximantOfDegree!(
197204

198205
end
199206

207+
208+
"""
209+
expA = expm(A, [workspace])
210+
211+
Return the matrix exponential of `BlochMcConnellDynamicsMatrix` `A`,
212+
where
213+
`workspace isa MatrixExponentialWorkspace`.
214+
"""
215+
function expm(
216+
A::BlochMcConnellDynamicsMatrix{Ta,N},
217+
workspace::MatrixExponentialWorkspace{Tw,N} = MatrixExponentialWorkspace{Ta}(N)
218+
) where {Ta,Tw,N}
219+
expA = BlochMcConnellMatrix{Tw}(N)
220+
expm!(expA, A, workspace)
221+
return expA
222+
end
223+
224+
225+
# helpers
200226
frexp1(x) = frexp(x)[1]
201227
frexp2(x) = frexp(x)[2]
202228
dfrexp1(x) = 2.0^(-floor(log2(abs(x))) - 1)

test/expm.jl

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1+
# expm.jl
2+
13
using BlochSim: BlochMcConnellMatrix, InstantaneousRF
24
using BlochSim: ExcitationMatrix, FreePrecessionMatrix
35
using BlochSim: Magnetization, MagnetizationMC, Spin, SpinMC
46
using BlochSim: applydynamics!, excite!, freeprecess!, signal
5-
import BlochSim # BlochMcConnellDynamicsMatrix, expm! (etc)
7+
import BlochSim # BlochMcConnellDynamicsMatrix, expm!, expm (etc)
8+
using BlochSim: MatrixExponentialWorkspace
69
using ForwardDiff: ForwardDiff
710
import ForwardDiff: derivative, gradient
811
using Test: @inferred, @test, @testset
912

13+
1014
function expm1()
1115

1216
T11 = 1
@@ -16,7 +20,6 @@ function expm1()
1620
τ12 = 0.05
1721
τ21 = 0.1
1822

19-
expA = BlochMcConnellMatrix(2)
2023
A = BlochSim.BlochMcConnellDynamicsMatrix(2)
2124
A.A[1].R2 = -1 / T21 - 1 / τ12
2225
A.A[1].Δω = 2π
@@ -28,12 +31,23 @@ function expm1()
2831
A.E[2].r = 1 / τ21
2932

3033
correct = exp(Matrix(A))
34+
35+
Acopy = deepcopy(A)
36+
expA = BlochMcConnellMatrix(2)
3137
BlochSim.expm!(expA, A)
38+
@test Matrix(expA) correct
39+
@test Acopy == A # ensure A was not mutated
3240

33-
return Matrix(expA) correct
41+
work = MatrixExponentialWorkspace{Float64}(2) # todo: fails @inferred
42+
expB = BlochMcConnellMatrix(2)
43+
BlochSim.expm!(expB, A, work)
44+
@test Matrix(expB) correct
3445

46+
expC = BlochSim.expm(A)
47+
@test Matrix(expC) correct
3548
end
3649

50+
3751
function dfrexp1()
3852

3953
f = x -> 2x * @inferred BlochSim.frexp1(x)
@@ -46,6 +60,7 @@ function dfrexp1()
4660

4761
end
4862

63+
4964
function dfrexp2()
5065

5166
f = x -> 2x * @inferred BlochSim.frexp2(x)
@@ -58,6 +73,7 @@ function dfrexp2()
5873

5974
end
6075

76+
6177
function autodiff1()
6278

6379
Ae = ExcitationMatrix()
@@ -81,6 +97,7 @@ function autodiff1()
8197

8298
end
8399

100+
84101
function autodiff2()
85102

86103
Ae = ExcitationMatrix()
@@ -104,6 +121,7 @@ function autodiff2()
104121

105122
end
106123

124+
107125
function autodiff3()
108126

109127
Ae = ExcitationMatrix()
@@ -127,6 +145,7 @@ function autodiff3()
127145

128146
end
129147

148+
130149
function autodiff4()
131150

132151
Ae = ExcitationMatrix()
@@ -150,6 +169,7 @@ function autodiff4()
150169

151170
end
152171

172+
153173
function autodiff5()
154174

155175
Ae = ExcitationMatrix()
@@ -173,6 +193,7 @@ function autodiff5()
173193

174194
end
175195

196+
176197
function autodiff6()
177198

178199
Ae = ExcitationMatrix()
@@ -196,11 +217,12 @@ function autodiff6()
196217

197218
end
198219

220+
199221
@testset "Matrix Exponential" begin
200222

201223
@testset "expm Accuracy" begin
202224

203-
@test expm1()
225+
expm1()
204226

205227
end
206228

0 commit comments

Comments
 (0)