-
Notifications
You must be signed in to change notification settings - Fork 4
/
expm.rkt
239 lines (171 loc) · 6.55 KB
/
expm.rkt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
#lang racket/base
(provide expm)
;;;
;;; Matrix Exponential
;;;
; This module implements `expm` the so-called matrix exponential.
; The standard exponential function `exp` can be written as
; as an infinite sum (the Taylor series of exp).
; x x^2 x^3
; exp(x) = 1 + -- + --- + --- + ...
; 1! 2! 3!
; The matrix exponential expm(A) of a square matrix A is
; defined as the limit of:
; A A^2 A^3
; exp(A) = 1 + -- + --- + --- + ...
; 1! 2! 3!
; if the limit exist (otherwise expm(A) is undefined).
; In principle one could use this definition to calculate `expm`,
; but the convergence is slow. Instead we will use
; an approximation of `exp(x)` using a rational expression:
; a quotient of two polynomials.
; p(x)
; exp(x) ~ -------- where p and q are polynomials
; q(x)
; Henri Padé studied how to find the best rational expression
; approximating a given function. Such an approximation is
; called an "Padé approximant" today.
; The precision of the approximation depends on the number
; of terms used in the numerator and denominator. More terms
; give higher precision.
; To keep things simple, we will look at the situation where
; we use the same number of terms in both numerator and denominator.
; This is called a diagonal Padé approximation (in tables they appear
; on the diagonal.
; The first few diagonal Padé approximations are:
; 1
; exp(x) ~ ---
; 1
; 1 + 1/2 x
; exp(x) ~ -----------
; 1 - 1/2 x
; 1 + 1/2 x + 1/12 x^2
; exp(x) ~ -----------------------
; 1 - 1/2 x + 1/12 x^2
; 1 + 1/2 x + 1/10 x^2 + 1/120 x^3
; exp(x) ~ ---------------------------------
; 1 - 1/2 x + 1/10 x^2 - 1/120 x^3
; The explicit formula for the i'th coefficient of the p'th diagonal approximant is:
; (2p-i)! p!
; N_p = ----------------
; (2p)! i! (p-i)!
; (2p-i)! p!
; D_p = ---------------- *(-1)^i
; (2p)! i! (p-i)!
; Given these formulas we can compute the coefficients we need,
; when we can experiment with different number of terms.
; This is fine - but what about our matrices?
; As a simple example, let's say we have chosen to use the approximation:
; 1 + 1/2 x
; exp(x) ~ -----------
; 1 - 1/2 x
; We can rewrite this as:
; (1 - 1/2 x) exp(x) = 1 + 1/2 x
; Plugging in a matrix A, we get:
; (1 - 1/2 A) exp(A) = 1 + 1/2 A
; This can be seen as a matrix equation in which the unknown is exp(A).
; The solution can be computed by `mldivide` ("left dividing with A").
; exp(A) = mldvide( (1 - 1/2 A), 1 + 1/2 A)
; This basic idea is the back bone of the computation.
; There is a little observation that we can use to reduce
; amount of compuation in the denominator. The terms of the
; numerator and denominator are the same - except for different
; signs in the terms with odd degree. This suggests that we
; should calculate the even and odd terms of the numerator first:
; numerator = sum_of_even + sum_of_odd
; then the the denominator is a sumple sum:
; denominator = sum_of_even - sum_of_odd
; The last piece of the puzzle concerns the domain the magnitude
; of the matrix entries. The approximation works best, if
; the entries |a_ij|<=0.5. (using norms, if |A|₁ <=0.5 ).
; See [1]
; Dividing the entries of A with 2 reduces the norm
; exp(A) = exp(A/2)^2
; and if we do it s times, we get:
; exp(A) = ... = exp(A/2^s)^(2s)
; That is we use the Padé approximation on exp(A/2^s),
; then we computer the 2s'th power.
; [1] The Pade Method for computing the Matrix Exponential
; M. Arioli, B. Codenotti, C. Fassino
; https://www.sciencedirect.com/science/article/pii/0024379594001901
(require racket/list)
(require "flomat.rkt")
;;;
;;; Coefficients
;;;
(define (fact n) (if (= n 0) 1 (* n (fact (- n 1)))))
(define (ncoef p i) ; for numerator
(/ (* (fact (+ p p (- i))) (fact p))
(* (fact (+ p p)) (fact i) (fact (- p i)))))
(define (dcoef p i) ; for denominator
(* (ncoef p i)
(if (odd? i) -1 1)))
; We can compute the coefficients we need.
(define n8 (let ([p 8]) (for/list ([j (+ p 1)]) (* 1.0 (ncoef p j)))))
(define d8 (let ([p 8]) (for/list ([j (+ p 1)]) (* 1.0 (dcoef p j)))))
(define (poly cs x)
; given (list c0 c1 c2 ... cn) compute c0 + c1*x + c2*x² + ... + cn x^n
(cond
[(empty? cs) 0.]
[else (+ (first cs)
(* x (poly (rest cs) x)))]))
; We can test the accuracy of the Pade approximation on a single number:
(define (exp-pade x)
(/ (poly n8 x)
(poly d8 x)))
; Now: (exp 1.0) and (exp-pade 1.0) give the same result.
; Note: using n7,d7 give an error on the last decimal when using doubles.
; The matrix version of poly looks like this:
(define (mpoly cs A)
(define n (nrows A))
(define I (eye n))
(define Z (zeros n))
(define (loop cs)
(cond
[(empty? cs) Z]
[else (plus (.* (first cs) I)
(times A (loop (rest cs))))]))
(loop cs))
; We can directly expm as below, because we want to compute the
; the even and odd terms separately.
(define (expm-pade0 A)
(mldivide (mpoly d8 A) (mpoly n8 A)))
(define (even-coefs cs)
(if (empty? cs) '() (cons (first cs) (odd-coefs (rest cs)))))
(define (odd-coefs cs)
(if (empty? cs) '() (even-coefs (rest cs))))
; Now we without scaling, we have:
(define (expm-pade/no-scaling A)
(define AA (times A A))
(define even (mpoly (even-coefs n8) AA))
(define odd (times A (mpoly (odd-coefs n8) AA)))
(define num (plus even odd))
(define den (minus even odd))
(mldivide den num))
; With pre-scaling we get:
(define (expm A)
(define s (find-scaling-exponent A))
(define 2^s (expt 2 s))
(define A/2^s (./ A 2^s))
(repeated-squaring (expm-pade/no-scaling A/2^s) s))
(define (repeated-squaring A s)
(if (= s 0) A (repeated-squaring (times A A) (- s 1))))
(define (find-scaling-exponent A)
(define N (norm A 1))
(define s (+ 1 (ceiling (log N 2))))
s)
;; (exp-pade 1.0)
;; (exp 1.0)
;; (exp-pade 2.0)
;; (exp 2.0)
;; (expm-pade0 (matrix '([1])))
;; (expm-pade0 (matrix '([1 0] [0 2])))
;; (expm-pade/no-scaling (matrix '([1])))
;; (expm-pade/no-scaling (matrix '([1 0] [0 2])))
;; (expm-pade (matrix '([1])))
;; (expm-pade (matrix '([1 0] [0 2])))
;; (expm-pade (matrix '([1 2] [3 4])))
;;; Notes:
;; Found: The Scaling and Squaring Method for the Matrix Exponential Revisited,
; Nicolas J. Higham
; TODO: Read it and see if we can improve the algorithm.