Skip to content

Commit 71ba5e7

Browse files
committed
Cleanup dense output Radau.
1 parent c513dca commit 71ba5e7

File tree

1 file changed

+35
-57
lines changed

1 file changed

+35
-57
lines changed

scipy_dae/integrate/_dae/radau.py

Lines changed: 35 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,9 @@ def butcher_tableau(s):
3434

3535
# the coefficent matrix is computed according to Hoffmann Section 2 or
3636
# 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)
3838
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)
4040

4141
# since the method is stiffly accurate we can simply extract the
4242
# quadrature weights
@@ -66,26 +66,26 @@ def radau_constants(s):
6666
A_inv = np.linalg.inv(A)
6767

6868
# eigenvalues and corresponding eigenvectors of coefficient matrix
69-
lambdas, V = eig(A)
69+
lambdas, Q = eig(A)
7070

7171
# sort eigenvalues and permut eigenvectors accordingly
7272
idx = np.argsort(lambdas)[::-1]
7373
lambdas = lambdas[idx]
74-
V = V[:, idx]
74+
Q = Q[:, idx]
7575

7676
# 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
7878
for i in range(s):
79-
V[:, i] /= V[-1, i]
79+
Q[:, i] /= Q[-1, i]
8080

8181
# convert complex eigenvalues and eigenvectors to real eigenvalues
8282
# in a block diagonal form and the associated real eigenvectors
83-
Gammas, T = cdf2rdf(lambdas, V)
83+
Gammas, T = cdf2rdf(lambdas, Q)
8484
TI = np.linalg.inv(T)
8585

8686
# 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))
8989
assert np.allclose(T @ Gammas @ TI, A)
9090
assert np.allclose(TI @ A @ T, Gammas)
9191

@@ -99,12 +99,12 @@ def radau_constants(s):
9999

100100
# compute embedded method for error estimate
101101
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
103103

104104
# explicit embedded method as proposed in Hairer (8.16)
105105
rhs_explicit = 1 / np.arange(1, s + 1)
106106
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)
108108
v_explicit = b_hat_explicit - b
109109

110110
# implicit embedded method as proposed in de Swart (12.1)
@@ -113,26 +113,30 @@ def radau_constants(s):
113113
b_hat1_implicit = DAMPING_RATIO_ERROR_ESTIMATE * b_hats_implicit
114114
rhs_implicit[0] -= b_hat1_implicit
115115
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)
117117
v_implicit = b - b_hat_implicit
118118

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:])
121122

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)
125126

126127
# these linear combinations are used in the algorithm
127128
MU_REAL = gammas[0]
128129
MU_COMPLEX = alphas -1j * betas
129130

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+
)
131135

132136

133137
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):
136140
s, n = Yp0.shape
137141
ncs = s // 2
138142
tau = t + h * C
@@ -523,13 +527,8 @@ def _step_impl(self):
523527
if self.sol is None:
524528
Yp0 = np.tile(yp, s).reshape(s, -1)
525529
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
529531
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
533532
scale = atol + np.abs(y) * rtol
534533

535534
converged = False
@@ -660,30 +659,24 @@ def _step_impl(self):
660659
return step_accepted, message
661660

662661
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)
668662
Z = self.Y - self.y_old
669-
Q = np.dot(Z.T, self.P)
663+
ZP = np.dot(Z.T, self.P)
670664
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)
673667

674668
def _dense_output_impl(self):
675669
return self.sol
676670

677671

678672
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):
680674
super().__init__(t_old, t)
681675
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
685679
self.y_old = y_old
686-
self.yp_old = yp_old
687680

688681
def _call_impl(self, t):
689682
x = (t - self.t_old) / self.h
@@ -694,25 +687,10 @@ def _call_impl(self, t):
694687
p = x**c
695688
dp = (c / self.h) * (x**(c - 1))
696689

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)
699692
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)
716694

717695
if t.ndim == 0:
718696
y = np.squeeze(y)

0 commit comments

Comments
 (0)