import FIAT
import numpy
from numpy import vander, zeros
from numpy.linalg import solve
[docs]
class ButcherTableau(object):
"""Top-level class representing a Butcher tableau encoding
a Runge-Kutta method. It has members
:arg A: a 2d array containing the Butcher matrix
:arg b: a 1d array giving weights assigned to each stage when
computing the solution at time n+1.
:arg btilde: If present, a 1d array giving weights for an embedded
lower-order method (used in estimating temporal
truncation error.)
:arg c: a 1d array containing weights at which time-dependent
terms are evaluated.
:arg order: the (integer) formal order of accuracy of the method
:arg embedded_order: If present, the (integer) formal order of
accuracy of the embedded method
:arg gamma0: If present, the weight on the explicit term in the
embedded lower-order method
"""
def __init__(self, A, b, btilde, c, order, embedded_order, gamma0):
self.A = A
self.b = b
self.btilde = btilde
self.c = c
self.order = order
self.embedded_order = embedded_order
self.gamma0 = gamma0
@property
def num_stages(self):
"""Return the number of stages the method has."""
return len(self.b)
@property
def is_stiffly_accurate(self):
"""Determines whether the method is stiffly accurate."""
res = zeros(self.num_stages)
res[-1] = 1.0
try:
return numpy.allclose(res, solve(self.A.T, self.b))
except numpy.linalg.LinAlgError:
return False
@property
def is_explicit(self):
A = self.A
ns = self.num_stages
for i in range(ns):
for j in range(i, ns):
if abs(A[i, j]) > 1.e-15:
return False
return True
@property
def is_diagonally_implicit(self):
A = self.A
ns = self.num_stages
for i in range(ns):
for j in range(i+1, ns):
if abs(A[i, j]) > 1.e-15:
return False
return True
@property
def is_implicit(self):
return not self.is_explicit
@property
def is_fully_implicit(self):
return self.is_implicit and not self.is_diagonally_implicit
def __str__(self):
return str(self.__class__).split(".")[-1][:-2]+"()"
[docs]
class CollocationButcherTableau(ButcherTableau):
"""When an RK method is based on collocation with point sets present
in FIAT, we have a general formula for producing the Butcher tableau.
:arg L: a one-dimensional class :class:`FIAT.FiniteElement`
of Lagrange type -- the degrees of freedom must all be point
evaluation.
:arg order: the order of the resulting RK method.
"""
def __init__(self, L, order):
assert L.ref_el == FIAT.ufc_simplex(1)
points = []
for ell in L.dual.nodes:
assert isinstance(ell, FIAT.functional.PointEvaluation)
# Assert singleton point for each node.
pt, = ell.get_point_dict().keys()
points.append(pt[0])
c = numpy.asarray(points)
# GLL DOFs are ordered by increasing entity dimension!
perm = numpy.argsort(c)
c = c[perm]
num_stages = len(c)
Q = FIAT.make_quadrature(L.ref_el, 2*num_stages)
qpts = Q.get_points()
qwts = Q.get_weights()
Lvals = L.tabulate(0, qpts)[0, ][perm]
# integrates them all!
b = Lvals @ qwts
# now for A, which means we have to adjust the interval
A = numpy.zeros((num_stages, num_stages))
for i in range(num_stages):
qpts_i = qpts * c[i]
qwts_i = qwts * c[i]
Lvals_i = L.tabulate(0, qpts_i)[0, ][perm]
A[i, :] = Lvals_i @ qwts_i
Aexplicit = numpy.zeros((num_stages, num_stages))
for i in range(num_stages):
qpts_i = 1 + qpts * c[i]
qwts_i = qwts * c[i]
Lvals_i = L.tabulate(0, qpts_i)[0, ][perm]
Aexplicit[i, :] = Lvals_i @ qwts_i
self.Aexplicit = Aexplicit
V = vander(c, increasing=True)
rhs = numpy.array([1.0/(s+1) for s in range(num_stages-1)] + [0])
btilde = solve(V.T, rhs)
gamma0 = 0
embedded_order = num_stages-1
super(CollocationButcherTableau, self).__init__(A, b, btilde, c, order, embedded_order, gamma0)
[docs]
class GaussLegendre(CollocationButcherTableau):
"""Collocation method based on the Gauss-Legendre points.
The order of accuracy is 2 * `num_stages`.
GL methods are A-stable, B-stable, and symplectic.
:arg num_stages: The number of stages (1 or greater)
"""
def __init__(self, num_stages):
assert num_stages > 0
U = FIAT.ufc_simplex(1)
L = FIAT.GaussLegendre(U, num_stages - 1)
super(GaussLegendre, self).__init__(L, 2 * num_stages)
def __str__(self):
return "GaussLegendre(%d)" % self.num_stages
[docs]
class LobattoIIIA(CollocationButcherTableau):
"""Collocation method based on the Gauss-Lobatto points.
The order of accuracy is 2 * `num_stages` - 2.
LobattoIIIA methods are A-stable but not B- or L-stable.
:arg num_stages: The number of stages (2 or greater)
"""
def __init__(self, num_stages):
assert num_stages > 1
U = FIAT.ufc_simplex(1)
L = FIAT.GaussLobattoLegendre(U, num_stages - 1)
super(LobattoIIIA, self).__init__(L, 2 * num_stages - 2)
def __str__(self):
return "LobattoIIIA(%d)" % self.num_stages
[docs]
class RadauIIA(CollocationButcherTableau):
"""Collocation method based on the Gauss-Radau points.
The order of accuracy is 2 * `num_stages` - 1.
RadauIIA methods are algebraically (hence B-) stable.
:arg num_stages: The number of stages (2 or greater)
:arg variant: Indicate whether to use the Radau5 style of
embedded scheme (with `variant = "embed_Radau5"`) or
the simple collocation type (with `variant = "embed_colloc"`)
"""
def __init__(self, num_stages, variant="embed_Radau5"):
assert num_stages >= 1
U = FIAT.ufc_simplex(1)
L = FIAT.GaussRadau(U, num_stages - 1)
super(RadauIIA, self).__init__(L, 2 * num_stages - 1)
if num_stages == 3 and variant == "embed_Radau5":
self.embedded_order = 3
self.btilde = numpy.array([4763/13500-numpy.sqrt(503/3071), 4763/13500+numpy.sqrt(503/3071), 263/13500], dtype='float')
self.gamma0 = 1237.0/4500
def __str__(self):
return "RadauIIA(%d)" % self.num_stages
[docs]
class BackwardEuler(RadauIIA):
"""The rock-solid first-order implicit method."""
def __init__(self):
super(BackwardEuler, self).__init__(1)
def __str__(self):
return ButcherTableau.__str__(self)
[docs]
class LobattoIIIC(ButcherTableau):
"""Discontinuous collocation method based on the Lobatto points.
The order of accuracy is 2 * `num_stages` - 2.
LobattoIIIC methods are A-, L-, algebraically, and B- stable.
:arg num_stages: The number of stages (2 or greater)
"""
def __init__(self, num_stages):
assert num_stages > 1
# mooch the b and c from IIIA
IIIA = LobattoIIIA(num_stages)
b = IIIA.b
btilde = IIIA.btilde
c = IIIA.c
embedded_order = IIIA.embedded_order
gamma0 = IIIA.gamma0
A = numpy.zeros((num_stages, num_stages))
for i in range(num_stages):
A[i, 0] = b[0]
for j in range(num_stages):
A[-1, j] = b[j]
mat = numpy.vander(c[1:], increasing=True).T
for i in range(num_stages-1):
rhs = numpy.array([(c[i]**(k+1))/(k+1) - b[0] * c[0]**k
for k in range(num_stages-1)])
A[i, 1:] = numpy.linalg.solve(mat, rhs)
super(LobattoIIIC, self).__init__(A, b, btilde, c, 2 * num_stages - 2, embedded_order, gamma0)
def __str__(self):
return "LobattoIIIC(%d)" % self.num_stages
[docs]
class PareschiRusso(ButcherTableau):
"""Second order, diagonally implicit, 2-stage.
A-stable if x >= 1/4 and L-stable iff x = 1 plus/minus 1/sqrt(2)."""
def __init__(self, x):
self.x = x
A = numpy.array([[x, 0.0], [1-2*x, x]])
b = numpy.array([0.5, 0.5])
c = numpy.array([x, 1-x])
super(PareschiRusso, self).__init__(A, b, None, c, 2, None, None)
def __str__(self):
return "PareschiRusso(%f)" % self.x
[docs]
class QinZhang(PareschiRusso):
"Symplectic Pareschi-Russo DIRK"
def __init__(self):
super(QinZhang, self).__init__(0.25)
def __str__(self):
return "QinZhang()"
[docs]
class Alexander(ButcherTableau):
""" Third-order, diagonally implicit, 3-stage, L-stable scheme from
Diagonally Implicit Runge-Kutta Methods for Stiff O.D.E.'s,
R. Alexander, SINUM 14(6): 1006-1021, 1977."""
def __init__(self):
# Root of x^3 - 3x^2 +3x/2-1/6 between 1/6 and 1/2
x = 0.43586652150845899942
y = -1.5*x*x + 4*x - 0.25
z = 1.5*x*x-5*x+1.25
A = numpy.array([[x, 0.0, 0.0], [(1-x)/2.0, x, 0.0], [y, z, x]])
b = numpy.array([y, z, x])
c = numpy.array([x, (1+x)/2.0, 1])
super(Alexander, self).__init__(A, b, None, c, 3, None, None)
def __str__(self):
return "Alexander()"