@@ -34,9 +34,9 @@ def butcher_tableau(s):
34
34
35
35
# the coefficent matrix is computed according to Hoffmann Section 2 or
36
36
# Hairer Section IV.5 (5.6) (requires some work to see the equivalence)
37
- V = np .vander (c , increasing = True )
37
+ Vc = np .vander (c , increasing = True )
38
38
R = np .diag (1 / np .arange (1 , s + 1 ))
39
- A = np .diag (c ) @ V @ R @ np .linalg .inv (V )
39
+ A = np .diag (c ) @ Vc @ R @ np .linalg .inv (Vc )
40
40
41
41
# since the method is stiffly accurate we can simply extract the
42
42
# quadrature weights
@@ -66,26 +66,26 @@ def radau_constants(s):
66
66
A_inv = np .linalg .inv (A )
67
67
68
68
# eigenvalues and corresponding eigenvectors of coefficient matrix
69
- lambdas , V = eig (A )
69
+ lambdas , Q = eig (A )
70
70
71
71
# sort eigenvalues and permut eigenvectors accordingly
72
72
idx = np .argsort (lambdas )[::- 1 ]
73
73
lambdas = lambdas [idx ]
74
- V = V [:, idx ]
74
+ Q = Q [:, idx ]
75
75
76
76
# scale eigenvectors to get a "nice" transformation matrix (used by original
77
- # scipy code) and at the same time minimizes the condition number of V
77
+ # scipy code) and at the same time minimizes the condition number of Q
78
78
for i in range (s ):
79
- V [:, i ] /= V [- 1 , i ]
79
+ Q [:, i ] /= Q [- 1 , i ]
80
80
81
81
# convert complex eigenvalues and eigenvectors to real eigenvalues
82
82
# in a block diagonal form and the associated real eigenvectors
83
- Gammas , T = cdf2rdf (lambdas , V )
83
+ Gammas , T = cdf2rdf (lambdas , Q )
84
84
TI = np .linalg .inv (T )
85
85
86
86
# sanity checks
87
- assert np .allclose (V @ np .diag (lambdas ) @ np .linalg .inv (V ), A )
88
- assert np .allclose (np .linalg .inv (V ) @ A @ V , np .diag (lambdas ))
87
+ assert np .allclose (Q @ np .diag (lambdas ) @ np .linalg .inv (Q ), A )
88
+ assert np .allclose (np .linalg .inv (Q ) @ A @ Q , np .diag (lambdas ))
89
89
assert np .allclose (T @ Gammas @ TI , A )
90
90
assert np .allclose (TI @ A @ T , Gammas )
91
91
@@ -99,12 +99,12 @@ def radau_constants(s):
99
99
100
100
# compute embedded method for error estimate
101
101
c_hat = np .array ([0 , * c ])
102
- vander = np .vander (c_hat , increasing = True ).T
102
+ Vc_hat = np .vander (c_hat , increasing = True ).T
103
103
104
104
# explicit embedded method as proposed in Hairer (8.16)
105
105
rhs_explicit = 1 / np .arange (1 , s + 1 )
106
106
rhs_explicit [0 ] -= gammas [0 ]
107
- b_hat_explicit = np .linalg .solve (vander [:- 1 , 1 :], rhs_explicit )
107
+ b_hat_explicit = np .linalg .solve (Vc_hat [:- 1 , 1 :], rhs_explicit )
108
108
v_explicit = b_hat_explicit - b
109
109
110
110
# implicit embedded method as proposed in de Swart (12.1)
@@ -113,26 +113,30 @@ def radau_constants(s):
113
113
b_hat1_implicit = DAMPING_RATIO_ERROR_ESTIMATE * b_hats_implicit
114
114
rhs_implicit [0 ] -= b_hat1_implicit
115
115
rhs_implicit -= b_hats_implicit
116
- b_hat_implicit = np .linalg .solve (vander [:- 1 , 1 :], rhs_implicit )
116
+ b_hat_implicit = np .linalg .solve (Vc_hat [:- 1 , 1 :], rhs_implicit )
117
117
v_implicit = b - b_hat_implicit
118
118
119
- # compute the inverse of the Vandermonde matrix to get the interpolation matrix P
120
- P = np .linalg .inv (vander )[1 :, 1 :]
119
+ # compute the inverse of the Vandermonde matrix to get the inverse
120
+ # interpolation matrix
121
+ P = np .linalg .inv (Vc_hat [1 :, 1 :])
121
122
122
- # compute coefficients using Vandermonde matrix
123
- vander2 = np .vander (c , increasing = True )
124
- P2 = np .linalg .inv (vander2 )
123
+ # inverse Vandermonde matrix for collocation error
124
+ Vc = np .vander (c , increasing = True )
125
+ P2 = np .linalg .inv (Vc )
125
126
126
127
# these linear combinations are used in the algorithm
127
128
MU_REAL = gammas [0 ]
128
129
MU_COMPLEX = alphas - 1j * betas
129
130
130
- return A , A_inv , c , T , TI , P , P2 , b_hat1_implicit , v_implicit , v_explicit , MU_REAL , MU_COMPLEX , b_hat_implicit , b_hat_explicit , p
131
+ return (
132
+ A , A_inv , c , T , TI , P , P2 , b_hat1_implicit , v_implicit , v_explicit ,
133
+ MU_REAL , MU_COMPLEX , b_hat_implicit , b_hat_explicit , p ,
134
+ )
131
135
132
136
133
137
def solve_collocation_system (fun , t , y , h , Yp0 , scale , tol ,
134
- LU_real , LU_complex , solve_lu ,
135
- C , T , TI , A , newton_max_iter ):
138
+ LU_real , LU_complex , solve_lu ,
139
+ C , T , TI , A , newton_max_iter ):
136
140
s , n = Yp0 .shape
137
141
ncs = s // 2
138
142
tau = t + h * C
@@ -523,13 +527,8 @@ def _step_impl(self):
523
527
if self .sol is None :
524
528
Yp0 = np .tile (yp , s ).reshape (s , - 1 )
525
529
else :
526
- # note: this only works when we extrapolate the derivative
527
- # of the collocation polynomial but do not use the sth order
528
- # collocation polynomial for the derivatives as well.
530
+ # improved guess by extrapolating the collocation polynomial
529
531
Yp0 = self .sol (t + h * C )[1 ].T
530
- # # Zp0 = self.sol(t + h * C)[1].T - yp
531
- # Z0 = self.sol(t + h * C)[0].T - y
532
- # Yp0 = (1 / h) * A_inv @ Z0
533
532
scale = atol + np .abs (y ) * rtol
534
533
535
534
converged = False
@@ -660,30 +659,24 @@ def _step_impl(self):
660
659
return step_accepted , message
661
660
662
661
def _compute_dense_output (self ):
663
- # Q = np.dot(self.Z.T, self.P)
664
- # h = self.t - self.t_old
665
- # Yp = (self.A_inv / h) @ self.Z
666
- # Zp = Yp - self.yp_old
667
- # Qp = np.dot(Zp.T, self.P)
668
662
Z = self .Y - self .y_old
669
- Q = np .dot (Z .T , self .P )
663
+ ZP = np .dot (Z .T , self .P )
670
664
Zp = self .Yp - self .yp_old
671
- Qp = np .dot (Zp .T , self .P )
672
- return RadauDenseOutput (self .t_old , self .t , self .y_old , Q , self . yp_old , Qp )
665
+ ZpP = np .dot (Zp .T , self .P )
666
+ return RadauDenseOutput (self .t_old , self .t , self .y_old , ZP , ZpP )
673
667
674
668
def _dense_output_impl (self ):
675
669
return self .sol
676
670
677
671
678
672
class RadauDenseOutput (DAEDenseOutput ):
679
- def __init__ (self , t_old , t , y_old , Q , yp_old , Qp ):
673
+ def __init__ (self , t_old , t , y_old , ZP , ZpP ):
680
674
super ().__init__ (t_old , t )
681
675
self .h = t - t_old
682
- self .Q = Q
683
- self .Qp = Qp
684
- self .order = Q .shape [1 ] - 1
676
+ self .ZP = ZP
677
+ self .ZpP = ZpP
678
+ self .order = ZP .shape [1 ] - 1
685
679
self .y_old = y_old
686
- self .yp_old = yp_old
687
680
688
681
def _call_impl (self , t ):
689
682
x = (t - self .t_old ) / self .h
@@ -694,25 +687,10 @@ def _call_impl(self, t):
694
687
p = x ** c
695
688
dp = (c / self .h ) * (x ** (c - 1 ))
696
689
697
- # 1. compute derivative of interpolation polynomial for y
698
- y = np .dot (self .Q , p )
690
+ # compute derivative of interpolation polynomial for y
691
+ y = np .dot (self .ZP , p )
699
692
y += self .y_old [:, None ]
700
- yp = np .dot (self .Q , dp )
701
-
702
- # # 2. compute collocation polynomial for y and yp
703
- # y = np.dot(self.Q, p)
704
- # yp = np.dot(self.Qp, p)
705
- # y += self.y_old[:, None]
706
- # yp += self.yp_old[:, None]
707
-
708
- # # 3. compute both values by Horner's rule
709
- # y = np.zeros_like(y)
710
- # yp = np.zeros_like(y)
711
- # for i in range(self.order, -1, -1):
712
- # y = self.Q[:, i][:, None] + y * x[None, :]
713
- # yp = y + yp * x[None, :]
714
- # y = self.y_old[:, None] + y * x[None, :]
715
- # yp /= self.h
693
+ yp = np .dot (self .ZP , dp )
716
694
717
695
if t .ndim == 0 :
718
696
y = np .squeeze (y )
0 commit comments