'''
multiprec_tables.py : Computation of quadrature and Runge-Kutta tables in multiprecision.
'''
import functools
import math
import mpmath
import numpy as np
from choreo.segm.cython.quad import QuadTable
from choreo.segm.cython.ODE import ExplicitSymplecticRKTable
from choreo.segm.cython.ODE import ImplicitRKTable
# Order is important.
all_GL_int = [
"Gauss" ,
"Radau_II" ,
"Radau_I" ,
"Lobatto_III" ,
]
def tridiag_eigenvalues(d, e):
"""
Adapted from mpmath
"""
n = len(d)
e[n-1] = 0
iterlim = 2 * mpmath.mp.dps
for l in range(n):
j = 0
while 1:
m = l
while 1:
# look for a small subdiagonal element
if m + 1 == n:
break
if abs(e[m]) <= mpmath.mp.eps * (abs(d[m]) + abs(d[m + 1])):
break
m = m + 1
if m == l:
break
if j >= iterlim:
raise RuntimeError("tridiag_eigen: no convergence to an eigenvalue after %d iterations" % iterlim)
j += 1
# form shift
p = d[l]
g = (d[l + 1] - p) / (2 * e[l])
r = mpmath.mp.hypot(g, 1)
if g < 0:
s = g - r
else:
s = g + r
g = d[m] - p + e[l] / s
s, c, p = 1, 1, 0
for i in range(m - 1, l - 1, -1):
f = s * e[i]
b = c * e[i]
if abs(f) > abs(g): # this here is a slight improvement also used in gaussq.f or acm algorithm 726.
c = g / f
r = mpmath.mp.hypot(c, 1)
e[i + 1] = f * r
s = 1 / r
c = c * s
else:
s = f / g
r = mpmath.mp.hypot(s, 1)
e[i + 1] = g * r
c = 1 / r
s = s * c
g = d[i + 1] - p
r = (d[i] - g) * s + 2 * c * b
p = s * r
d[i + 1] = g + p
g = c * r - b
d[l] = d[l] - p
e[l] = g
e[m] = 0
for ii in range(1, n):
# sort eigenvalues (bubble-sort)
i = ii - 1
k = i
p = d[i]
for j in range(ii, n):
if d[j] >= p:
continue
k = j
p = d[k]
if k == i:
continue
d[k] = d[i]
d[i] = p
# 3 terms definition of polynomial families
# P_n+1 = (X - a_n) P_n - b_n P_n-1
def GaussLegendre3Term(n):
a = mpmath.matrix(n,1)
b = mpmath.matrix(n,1)
b[0] = 2
for i in range(1,n):
i2 = i*i
b[i] = mpmath.fraction(i2,4*i2-1)
return a, b
def ShiftedGaussLegendre3Term(n):
a = mpmath.matrix(n,1)
b = mpmath.matrix(n,1)
for i in range(n):
a[i] = mpmath.fraction(1,2)
b[0] = 1
for i in range(1,n):
i2 = i*i
b[i] = mpmath.fraction(i2,4*(4*i2-1))
return a, b
def EvalAllFrom3Term(a,b,n,x):
# n >= 1
phi = mpmath.matrix(n+1,1)
phi[0] = mpmath.mpf(1)
phi[1] = x - a[0]
for i in range(1,n):
phi[i+1] = (x - a[i]) * phi[i] - b[i] * phi[i-1]
return phi
def GaussMatFrom3Term(a,b,n):
d = a.copy()
e = mpmath.matrix(n,1)
for i in range(n-1):
e[i] = mpmath.sqrt(b[i+1])
return d, e
def LobattoMatFrom3Term(a,b,n):
d = a.copy()
e = mpmath.matrix(n,1)
for i in range(n-2):
e[i] = mpmath.sqrt(b[i+1])
m = n-1
xm = mpmath.mpf(0)
phim = EvalAllFrom3Term(a,b,m,xm)
xp = mpmath.mpf(1)
phip = EvalAllFrom3Term(a,b,m,xp)
mat = mpmath.matrix(2)
mat[0,0] = phim[m]
mat[1,0] = phip[m]
mat[0,1] = phim[m-1]
mat[1,1] = phip[m-1]
rhs = mpmath.matrix(2,1)
rhs[0] = xm * phim[m]
rhs[1] = xp * phip[m]
(alpha, beta) = (mat ** -1) * rhs
d[n-1] = alpha
e[n-2] = mpmath.sqrt(beta)
return d, e
def RadauMatFrom3Term(a,b,n,x):
d = a.copy()
e = mpmath.matrix(n,1)
for i in range(n-1):
e[i] = mpmath.sqrt(b[i+1])
m = n-1
phim = EvalAllFrom3Term(a,b,m,x)
alpha = x - b[m] * phim[m-1] / phim[m]
d[n-1] = alpha
return d, e
@functools.cache
def ComputeQuadNodes(n, method = "Gauss"):
if method in all_GL_int:
a, b = ShiftedGaussLegendre3Term(n)
if method == "Gauss":
z, e = GaussMatFrom3Term(a,b,n)
elif method == "Radau_I":
x = mpmath.mpf(0)
z, e = RadauMatFrom3Term(a,b,n,x)
elif method == "Radau_II":
x = mpmath.mpf(1)
z, e = RadauMatFrom3Term(a,b,n,x)
elif method == "Lobatto_III":
z, e = LobattoMatFrom3Term(a,b,n)
tridiag_eigenvalues(z, e)
elif method == "Cheb_I":
z = mpmath.matrix(n,1)
alpha = mpmath.mp.pi / (2*n)
for i in range(n):
z[i] = (1 - mpmath.cos(alpha * (2*i+1)))/2
elif method == "Cheb_II":
z = mpmath.matrix(n,1)
alpha = mpmath.mp.pi / (n+1)
for i in range(n):
z[i] = (1 + mpmath.cos(alpha * (n-i)))/2
elif method == "ClenshawCurtis":
z = mpmath.matrix(n,1)
alpha = mpmath.mp.pi / (n-1)
for i in range(n):
z[i] = (1 + mpmath.cos(alpha * (n-1-i)))/2
else:
raise ValueError(f"Unknown method {method}")
return z
def ComputeLagrangeWeights(n, xi):
"""
Computes Lagrange weights for barycentric Lagrange interpolation
"""
wmat = mpmath.matrix(n)
wmat[0,0] = 1
for i in range(1,n):
for j in range(i):
wmat[j,i] = (xi[j] - xi[i]) * wmat[j,i-1]
wmat[i,i] = 1
for j in range(i):
wmat[i,i] *= (xi[i] - xi[j])
wi = mpmath.matrix(n,1)
for i in range(n):
wi[i] = 1 / wmat[i,n-1]
return wi
def ComputeAvec(n, xi):
amat = mpmath.matrix(n+1)
avec = mpmath.matrix(n,1)
amat[0,0] = -xi[0]
amat[1,0] = 1
for j in range(1,n):
amat[0,j] = -xi[j] * amat[0,j-1]
for i in range(1,j+1):
amat[i,j] = amat[i-1,j-1]-xi[j]*amat[i,j-1]
amat[j+1,j] = amat[j,j-1]
for j in range(n):
avec[j] = amat[n-j,n-1]
return avec
def ComputeVandermondeInverseParker(n, xi, wi=None):
if wi is None:
wi = ComputeLagrangeWeights(n, xi)
l = n-1
avec = ComputeAvec(n, xi)
qmat = mpmath.matrix(n)
for j in range(n):
qmat[l,j] += 1
for j in range(n):
for i in range(1,n):
qmat[l-i,j] += xi[j] * qmat[l-i+1,j] + avec[i]
for i in range(n):
for j in range(n):
qmat[l-i,j] *= wi[j]
return qmat
def BuildButcherCMat(z,n):
mat = mpmath.matrix(n)
for j in range(n):
for i in range(n):
mat[j,i] = z[j]**i
return mat
def Build_integration_RHS(z,n):
rhs = mpmath.matrix(1,n)
for j in range(n):
rhs[j] = 1
rhs[j] /= j+1
return rhs
def BuildButcherCRHS(y,z,n,m):
rhs = mpmath.matrix(n,m)
for j in range(n):
for i in range(m):
rhs[j,i] = (z[j]**(i+1) - y[j]**(i+1))/(i+1)
return rhs
def ComputeButcher_collocation(z, vdm_inv, n):
y = mpmath.matrix(n,1)
for i in range(n):
y[i] = 0
rhs = BuildButcherCRHS(y,z,n,n)
Butcher_a = rhs * vdm_inv
zp = mpmath.matrix(n,1)
for i in range(n):
y[i] = 1
zp[i] = 1+z[i]
rhs = BuildButcherCRHS(y,zp,n,n)
Butcher_beta = rhs * vdm_inv
for i in range(n):
y[i] = -1 + z[i]
zp[i] = 0
rhs = BuildButcherCRHS(y,zp,n,n)
Butcher_gamma = rhs * vdm_inv
return Butcher_a, Butcher_beta, Butcher_gamma
def ComputeButcher_sub_collocation(z, n):
parker_inv = ComputeVandermondeInverseParker(n-1, z)
y = mpmath.matrix(n,1)
for i in range(n):
y[i] = 0
rhs_plus = BuildButcherCRHS(y,z,n,n)
rhs = rhs_plus[1:n,0:(n-1)]
Butcher_a_sub = rhs * parker_inv
Butcher_a = mpmath.matrix(n)
for i in range(n-1):
for j in range(n-1):
Butcher_a[i+1,j] = Butcher_a_sub[i,j]
return Butcher_a
def SymmetricAdjointQuadrature(w,z,n):
w_ad = mpmath.matrix(n,1)
z_ad = mpmath.matrix(n,1)
for i in range(n):
z_ad[i] = 1 - z[n-1-i]
w_ad[i] = w[n-1-i]
return w_ad, z_ad
def SymmetricAdjointButcher(Butcher_a, Butcher_b, Butcher_c, Butcher_beta, Butcher_gamma, n):
Butcher_b_ad, Butcher_c_ad = SymmetricAdjointQuadrature(Butcher_b,Butcher_c,n)
Butcher_a_ad = mpmath.matrix(n)
Butcher_beta_ad = mpmath.matrix(n)
Butcher_gamma_ad = mpmath.matrix(n)
for i in range(n):
for j in range(n):
Butcher_a_ad[i,j] = Butcher_b[n-1-j] - Butcher_a[n-1-i,n-1-j]
Butcher_beta_ad[i,j] = Butcher_gamma[n-1-i,n-1-j]
Butcher_gamma_ad[i,j] = Butcher_beta[n-1-i,n-1-j]
return Butcher_a_ad, Butcher_b_ad, Butcher_c_ad, Butcher_beta_ad, Butcher_gamma_ad
def SymplecticAdjointButcher(Butcher_a, Butcher_b, n):
Butcher_a_ad = mpmath.matrix(n)
for i in range(n):
for j in range(n):
Butcher_a_ad[i,j] = Butcher_b[j] * (1 - Butcher_a[j,i] / Butcher_b[i])
return Butcher_a_ad
def Get_quad_method_from_RK_method(method="Gauss"):
for quad_method in all_GL_int:
if quad_method in method:
return quad_method
else:
return method
@functools.cache
def ComputeNamedGaussButcherTables(n, dps=60, method="Gauss"):
mpmath.mp.dps = dps
quad_method = Get_quad_method_from_RK_method(method)
w, z, wlag, vdm_inv = ComputeQuadratureTables(n, dps, quad_method)
Butcher_a, Butcher_beta , Butcher_gamma = ComputeButcher_collocation(z, vdm_inv, n)
if method in ["Lobatto_IIIC", "Lobatto_IIIC*", "Lobatto_IIID"]:
Butcher_a = ComputeButcher_sub_collocation(z,n)
known_method = False
if method in ["Gauss", "Lobatto_IIIA", "Radau_IIA", "Lobatto_IIIC*", "Cheb_I", "Cheb_II", "ClenshawCurtis"]:
# No transformation is required
known_method = True
if method in ["Lobatto_IIIB", "Radau_IA", "Lobatto_IIIC"]:
known_method = True
# Symplectic adjoint
Butcher_a = SymplecticAdjointButcher(Butcher_a, w, n)
if method in ["Radau_IB", "Radau_IIB", "Lobatto_IIIS", "Lobatto_IIID" ]:
known_method = True
# Symplectic adjoint average
Butcher_a_ad = SymplecticAdjointButcher(Butcher_a, w, n)
Butcher_a = (Butcher_a_ad + Butcher_a) / 2
if not(known_method):
raise ValueError(f'Unknown method {method}')
return Butcher_a, w, z, Butcher_beta, Butcher_gamma
def GetConvergenceRate(method, n):
if "Gauss" in method:
if n < 1:
raise ValueError(f"Incorrect value for n {n}")
th_cvg_rate = 2*n
elif "Radau" in method:
if n < 2:
raise ValueError(f"Incorrect value for n {n}")
th_cvg_rate = 2*n-1
elif "Lobatto" in method:
if n < 2:
raise ValueError(f"Incorrect value for n {n}")
th_cvg_rate = 2*n-2
elif "Cheb" in method:
th_cvg_rate = n + (n % 2)
elif method == "ClenshawCurtis":
th_cvg_rate = n + (n % 2)
else:
raise ValueError(f"Unknown method {method}")
return th_cvg_rate
@functools.cache
def ComputeQuadratureTables(n, dps=30, method="Gauss"):
mpmath.mp.dps = dps
z = ComputeQuadNodes(n, method=method)
return ComputeQuadratureTablesFromNodes(z, dps=30)
def ComputeQuadratureTablesFromNodes(nodes, dps=30):
mpmath.mp.dps = dps
n = len(nodes)
if isinstance(nodes, mpmath.matrix):
z = nodes
else:
# Not sure how to cast properly
z = mpmath.matrix(n,1)
for i in range(n):
z[i] = nodes[i]
wlag = ComputeLagrangeWeights(n, z)
vdm_inv = ComputeVandermondeInverseParker(n, z, wlag)
rhs = Build_integration_RHS(z, n)
w = rhs * vdm_inv
return w, z, wlag, vdm_inv
[docs]
def ComputeQuadrature(n=10, dps=30, method="Gauss", nodes=None):
"""Computes a :class:`choreo.segm.quad.QuadTable`
The computation is performed at a user-defined precision using `mpmath <https://mpmath.org/doc/current>`_ to ensure that the result does not suffer from precision loss, even at relatively high orders.
Available choices for ``method`` are:
* ``"Gauss"``
* ``"Radau_I"`` and ``"Radau_II"``
* ``"Lobatto_III"``
* ``"Cheb_I"`` and ``"Cheb_II"``
* ``"ClenshawCurtis"``
Alternatively, the user can supply an array of node values between ``0.`` and ``1.``. The result is then the associated collocation method.
Example
-------
>>> import choreo
>>> choreo.segm.multiprec_tables.ComputeQuadrature(n=2, dps=60, method="Lobatto_III")
QuadTable object with 2 nodes
Nodes: [7.7787691e-62 1.0000000e+00]
Weights: [0.5 0.5]
Barycentric Lagrange interpolation weights: [-1. 1.]
>>>
>>> choreo.segm.multiprec_tables.ComputeQuadrature(nodes=[0., 0.25, 0.5, 0.75, 1.])
QuadTable object with 5 nodes
Nodes: [0. 0.25 0.5 0.75 1. ]
Weights: [0.07777778 0.35555556 0.13333333 0.35555556 0.07777778]
Barycentric Lagrange interpolation weights: [ 10.66666667 -42.66666667 64. -42.66666667 10.66666667]
Parameters
----------
n : :class:`python:int`, optional
Order of the method. By default ``10``.
dps : :class:`python:int`, optional
Context precision in `mpmath <https://mpmath.org/doc/current>`_. See :doc:`mpmath:contexts` for more info. By default ``30``.
method : :class:`python:str`, optional
Name of the method, by default ``"Gauss"``.
nodes: :class:`numpy:numpy.ndarray` | :class:`mpmath:mpmath.matrix` | :data:`python:None`, optional
Array of integration node values. By default, :data:`python:None`.
Returns
-------
:class:`choreo.segm.quad.QuadTable`
The resulting nodes and weights of the quadrature.
"""
if nodes is None:
th_cvg_rate = GetConvergenceRate(method, n)
w, z, wlag, vdm_inv = ComputeQuadratureTables(n, dps=dps, method=method)
else:
n = len(nodes)
th_cvg_rate = n
w, z, wlag, vdm_inv = ComputeQuadratureTablesFromNodes(nodes, dps=dps)
w_np = np.array(w.tolist(),dtype=np.float64).reshape(n)
z_np = np.array(z.tolist(),dtype=np.float64).reshape(n)
w_lag_np = np.array(wlag.tolist(),dtype=np.float64).reshape(n)
return QuadTable(
w = w_np ,
x = z_np ,
wlag = w_lag_np ,
th_cvg_rate = th_cvg_rate ,
)
[docs]
def ComputeImplicitRKTable(n=10, dps=60, method="Gauss", nodes=None):
"""Computes a :class:`choreo.segm.ODE.ImplicitRKTable`
The computation is performed at a user-defined precision using `mpmath <https://mpmath.org/doc/current>`_ to ensure that the result does not suffer from precision loss, even at relatively high orders.
Available choices for ``method`` are:
* ``"Gauss"``
* ``"Radau_IA"``, ``"Radau_IIA"``, ``"Radau_IB"`` and ``"Radau_IIB"``
* ``"Lobatto_IIIA"``, ``"Lobatto_IIIB"``, ``"Lobatto_IIIC"``, ``"Lobatto_IIIC*"``, ``"Lobatto_IIID"`` and ``"Lobatto_IIIS"``
* ``"Cheb_I"`` and ``"Cheb_II"``
* ``"ClenshawCurtis"``
Alternatively, the user can supply an array of node values between ``0.`` and ``1.``. The result is then the associated collocation method. Cf Theorem 7.7 of :footcite:`hairer1987solvingODEI` for more details.
:cited:
.. footbibliography::
Example
-------
>>> import choreo
>>> Gauss = choreo.segm.multiprec_tables.ComputeImplicitRKTable(n=2, dps=60, method="Gauss")
>>> Gauss
ImplicitRKTable object of order 2
>>> Gauss.a_table
array([[ 0.25 , -0.03867513],
[ 0.53867513, 0.25 ]])
>>> Gauss.b_table
array([0.5, 0.5])
>>> Gauss.c_table
array([0.21132487, 0.78867513])
Parameters
----------
n : :class:`python:int`, optional
Order of the method. By default ``10``.
dps : :class:`python:int`, optional
Context precision in `mpmath <https://mpmath.org/doc/current>`_. See :doc:`mpmath:contexts` for more info. By default ``30``.
method : :class:`python:str`, optional
Name of the method, by default ``"Gauss"``.
nodes: :class:`numpy:numpy.ndarray` | :class:`mpmath:mpmath.matrix` | :data:`python:None`, optional
Array of integration node values. By default, :data:`python:None`.
Returns
-------
:class:`choreo.segm.quad.QuadTable`
The resulting nodes and weights of the quadrature.
"""
if nodes is None:
th_cvg_rate = GetConvergenceRate(method, n)
quad_method = Get_quad_method_from_RK_method(method)
quad_table = ComputeQuadrature(n, dps=dps, method=quad_method)
Butcher_a, Butcher_b, Butcher_c, Butcher_beta, Butcher_gamma = ComputeNamedGaussButcherTables(n, dps=dps, method=method)
else:
n = len(nodes)
th_cvg_rate = n
w, z, wlag, vdm_inv = ComputeQuadratureTablesFromNodes(nodes, dps=dps)
w_np = np.array(w.tolist(),dtype=np.float64).reshape(n)
z_np = np.array(z.tolist(),dtype=np.float64).reshape(n)
w_lag_np = np.array(wlag.tolist(),dtype=np.float64).reshape(n)
quad_table = QuadTable(
w = w_np ,
x = z_np ,
wlag = w_lag_np ,
th_cvg_rate = th_cvg_rate ,
)
Butcher_a, Butcher_beta , Butcher_gamma = ComputeButcher_collocation(z, vdm_inv, n)
Butcher_a_np = np.array(Butcher_a.tolist(),dtype=np.float64)
Butcher_beta_np = np.array(Butcher_beta.tolist(),dtype=np.float64)
Butcher_gamma_np = np.array(Butcher_gamma.tolist(),dtype=np.float64)
return ImplicitRKTable(
a_table = Butcher_a_np ,
quad_table = quad_table ,
beta_table = Butcher_beta_np ,
gamma_table = Butcher_gamma_np ,
th_cvg_rate = th_cvg_rate ,
)
def ComputeImplicitSymplecticRKTablePair_Gauss(n=10, dps=60, method="Gauss", nodes=None):
if nodes is None:
th_cvg_rate = GetConvergenceRate(method, n)
quad_method = Get_quad_method_from_RK_method(method)
quad_table = ComputeQuadrature(n, dps=dps, method=quad_method)
Butcher_a, Butcher_b, Butcher_c, Butcher_beta, Butcher_gamma = ComputeNamedGaussButcherTables(n, dps=dps, method=method)
else:
n = len(nodes)
th_cvg_rate = n
w, z, wlag, vdm_inv = ComputeQuadratureTablesFromNodes(nodes, dps=dps)
w_np = np.array(w.tolist(),dtype=np.float64).reshape(n)
z_np = np.array(z.tolist(),dtype=np.float64).reshape(n)
w_lag_np = np.array(wlag.tolist(),dtype=np.float64).reshape(n)
quad_table = QuadTable(
w = w_np ,
x = z_np ,
wlag = w_lag_np ,
th_cvg_rate = th_cvg_rate ,
)
Butcher_a, Butcher_beta , Butcher_gamma = ComputeButcher_collocation(z, vdm_inv, n)
Butcher_a_ad = SymplecticAdjointButcher(Butcher_a, Butcher_b, n)
Butcher_a_np = np.array(Butcher_a.tolist(),dtype=np.float64)
Butcher_beta_np = np.array(Butcher_beta.tolist(),dtype=np.float64)
Butcher_gamma_np = np.array(Butcher_gamma.tolist(),dtype=np.float64)
Butcher_a_ad_np = np.array(Butcher_a_ad.tolist(),dtype=np.float64)
rk = ImplicitRKTable(
a_table = Butcher_a_np ,
quad_table = quad_table ,
beta_table = Butcher_beta_np ,
gamma_table = Butcher_gamma_np ,
th_cvg_rate = th_cvg_rate ,
)
rk_ad = ImplicitRKTable(
a_table = Butcher_a_ad_np ,
quad_table = quad_table ,
beta_table = Butcher_beta_np ,
gamma_table = Butcher_gamma_np ,
th_cvg_rate = th_cvg_rate ,
)
return rk, rk_ad
def Yoshida_w_to_cd(w_in, th_cvg_rate):
'''
input : vector w as in Construction of higher order symplectic integrators in PHYSICS LETTERS A by Haruo Yoshida 1990.
w[1:m+1] (m elements) is provided. w0 is implicit.
'''
m = w_in.shape[0]
wo = 1-2*math.fsum(w_in)
w = np.zeros((m+1),dtype=np.float64)
w[0] = wo
for i in range(m):
w[i+1] = w_in[i]
n = 2*m + 2
c_table = np.zeros((n),dtype=np.float64)
d_table = np.zeros((n),dtype=np.float64)
for i in range(m):
val = w[m-i]
d_table[i] = val
d_table[2*m-i] = val
d_table[m] = w[0]
c_table[0] = w[m] / 2
c_table[2*m+1] = w[m] / 2
for i in range(m):
val = (w[m-i]+w[m-1-i]) / 2
c_table[i+1] = val
c_table[2*m-i] = val
return ExplicitSymplecticRKTable(
c_table ,
d_table ,
th_cvg_rate ,
)
def Yoshida_w_to_cd_reduced(w, th_cvg_rate):
'''
Variation on Yosida's method
input : vector w as in Construction of higher order symplectic integrators in PHYSICS LETTERS A by Haruo Yoshida 1990.
w[1:m+1] (m elements) is provided.
'''
m = w.shape[0]
n = 2*m
c_table = np.zeros((n),dtype=np.float64)
d_table = np.zeros((n),dtype=np.float64)
for i in range(m):
val = w[m-1-i]
d_table[i] = val
d_table[n-2-i] = val
c_table[0] = w[m-1] / 2
c_table[n-1] = w[m-1] / 2
for i in range(m-1):
val = (w[m-1-i]+w[m-2-i]) / 2
c_table[i+1] = val
c_table[n-2-i] = val
return ExplicitSymplecticRKTable(
c_table ,
d_table ,
th_cvg_rate ,
)