This is a demo following the original paper of Battles & Trefethen.
%matplotlib inline
%config InlineBackend.figure_formats = {'svg', 'png'}
import numpy as np
import matplotlib.pyplot as plt
In principle, it suffices to use the class Chebfun
, but we'll import all the objects in chebfun
for convenience.
from pychebfun import Chebfun
import pychebfun
x = Chebfun.identity()
This is only for this IPython notebook
from IPython.core.display import HTML
We define a convenience plotting function
def cplot(fun):
pychebfun.plot(fun, with_interpolation_points=True)
A typical way of initialising a Chebfun is to use the function chebfun
. It will work for any function.
cube = pychebfun.chebfun(lambda x:x**3)
A more pythonic way to achieve the same result would be using Chebfun.from_function
, it will also work for any function.
cube = Chebfun.from_function(lambda x:x**3)
Another possibility is to use the variable x
defined by x = Chebfun.identity()
:
x = Chebfun.identity()
Note however, that this will only work if the function of x has been registered in chebfun. Here it works because chebfun knows how to compute arbitray powers:
cube = x**3
Other examples could include:
print(np.cos(x))
print(np.tan(x))
print(np.exp(np.sin(x)))
<Chebfun(13)> <Chebfun(32)> <Chebfun(20)>
The size of f
is the number of interpolation point.
cube.size()
4
Displaying a Chebfun gives the interpolation values at the Chebyshev interpolation points:
print(repr(cube))
Chebfun domain length endpoint values [ -1.0, 1.0] 4 -1.00 1.00 vscale = 1.00e+00
Chebfun.from_function(lambda x: x**7 - 3*x + 5).size()
8
f_sin5 = Chebfun.from_function(lambda x: np.sin(5*np.pi*x))
f_sin5.size()
42
For some functions, convergence is not achievable, and one must set a limit to the dichotomy algorithm.
f_abs = Chebfun.from_function(abs, N=1000)
Chebfun
objects behave as ordinary function, which can be evaluated at any point.
cube(5)
array(125.)
cube(-.5)
array(-0.125)
It is also possible (and more efficient) to evaluate a chebfun at several point at once. For instance, we evaluate the Chebfun for sin(5x) at several points at once:
f_sin5(np.linspace(0, .2, 5))
array([ 3.17383112e-16, 7.07106781e-01, 1.00000000e+00, 7.07106781e-01, -1.26628627e-15])
One can for instance add two chebfuns:
chebsum = cube+f_sin5
print(chebsum.size())
cplot(chebsum)
42
Or multiply them:
chebmult = cube*f_sin5
print(chebmult.size())
cplot(chebmult)
43
f_sin = Chebfun.from_function(lambda x:np.sin(x))
print(f_sin.size())
print((f_sin*f_sin).size())
14 17
It is also possible to add and multiply chebfuns with scalars:
plt.subplot(121)
cplot(3*f_sin)
plt.subplot(122)
cplot(3+f_sin)
f_cubic = Chebfun.from_function(lambda x: x**3 - x)
cplot(f_cubic)
cplot(f_sin5)
In the paper, they suggest to create two Chebfuns as follows:
np.sin(16*x), np.sin(18*x)
(Chebfun domain length endpoint values [ -1.0, 1.0] 42 0.29 -0.29 vscale = 1.00e+00, Chebfun domain length endpoint values [ -1.0, 1.0] 44 0.75 -0.75 vscale = 1.00e+00)
It is certainly possible, but we can't create a 2D Chebfun from them, at the moment. Instead we have to do it like this:
def vector_func(x):
return np.vstack([np.sin(16*x), np.sin(18*x)]).T
cheb_vec = Chebfun.from_function(vector_func)
cplot(cheb_vec)
Again, we explore the two ways to create a chebfun. First, the general way, which works for any smooth functions:
f_expsin = Chebfun.from_function(lambda x: np.exp(np.sin(x)))
Then, the “operator overloading” way.
We take advantage of the fact that exp
and sin
are defined on chebfuns.
Not all functions are, though.
This is very similar to ufuncs in numpy.
g_expsin = np.exp(np.sin(x))
Of course, both chebfuns are equivalent, as we demonstrate here.
print(f_expsin.size())
print(g_expsin.size())
20 20
plt.subplot(121)
cplot(f_expsin)
plt.subplot(122)
cplot(g_expsin)
(f_expsin - g_expsin).norm()
6.280369834735101e-16
By limiting the accuracy of the chebfun, one observes the celebrated Gibbs phenomenon.
f_sign = Chebfun.from_function(lambda x: np.sign(x), N=25)
cplot(f_sign)
Pychebfun implements the method compare
which plots a graph of the error.
def absfive(x):
return np.abs(x)**5
#errors = array([(Chebfun(absfive, N) - exact).norm() for N in range(10, 20)])
#loglog(errors)
pychebfun.compare(Chebfun.from_function(absfive), absfive)
<Axes: >
A chebfun passing through random values at 50 Chebyshev points.
The method to initialise a Chebfun from data at Chebyshev points is from_data
:
rng = np.random.default_rng(0)
rand_chebfun = Chebfun.from_data(rng.random(51))
cplot(rand_chebfun)
rand_chebfun.size()
51
f_walk = Chebfun.from_data(rng.normal(size=(100,2)))
cplot(f_walk)
Clearly, as chebfuns are based on polynomial interpolation, there is no guarantee that extrapolation outside the interval [−1,1] will give any sensible result at all. This is an illustration of that phenomenon.
xx = np.linspace(-1.4, 1.4, 100)
error = np.log10(np.abs(f_sin5(xx)-np.sin(5*np.pi*xx))+1e-16)
plt.subplot(211)
plt.plot(xx, f_sin5(xx), marker='.')
plt.ylabel('interpolant')
plt.subplot(212)
plt.plot(xx,error, marker='.')
plt.ylabel('log_10(error)')
Text(0, 0.5, 'log_10(error)')
The Chebyshev coefficients of a chebfun are quickly computed using a fast Fourier transform.
The corresponding method is coefficients
Chebfun.from_function(np.exp(x)).coefficients()
array([1.26606588e+00, 1.13031821e+00, 2.71495340e-01, 4.43368498e-02, 5.47424044e-03, 5.42926312e-04, 4.49773230e-05, 3.19843646e-06, 1.99212481e-07, 1.10367720e-08, 5.50589728e-10, 2.49795910e-11, 1.03889547e-12, 3.97630646e-14])
Chebfun.from_coeff(np.array([3,2,1.]))
Chebfun domain length endpoint values [ -1.0, 1.0] 3 2.00 6.00 vscale = 1.00e+00
One way to create the basis Chebyshev polynomial of order 10.
T_10 = Chebfun.from_coeff(np.array([0.]*10+[1]))
cplot(T_10)
Incidentally, the same result is achieved using Chebfun.basis
:
cplot(Chebfun.basis(10))
As an illustration of how fast the fast Fourier transform is, we create a chebfun with 100000 points.
r = rng.normal(size=100000)
f_randn = Chebfun.from_coeff(r)
np.sqrt(np.sum(np.square(r)))
316.21969834936846
f_randn.sum()
#f_randn.norm() # doesn't work yet
0.35631543750789174
T_20 = Chebfun.basis(20)
T_20.norm()
1.4142135623730951
The method sum
computes the integral of the chebfun using the Chebyshev expansion and integral of the Chebyshev basis polynomial (it is known as the Clenshaw–Curtis quadrature)
k_odd = 5
k_even = 2
HTML(r'For odd $k$ we check that $\int T_k = %s$ ($k=%s$), otherwise, $\int T_k = \frac{2}{1-k^2} = %s \approx %s$ for $k = %s$.' %
(Chebfun.basis(k_odd).sum(),
k_odd,
2./(1-k_even**2),
Chebfun.basis(k_even).sum(),
k_even))
Computing some integrals.
f1 = np.sin(np.pi*x)**2
print(f1.size(), f1.sum())
HTML(r'$\int \sin(\pi x)^2 dx = %s$' % f1.sum())
25 0.9999999999999993
f2 = Chebfun.from_function(lambda x: 1/(5+3*np.cos(np.pi*x)))
print(f2.size(), f2.sum())
51 0.5
f3 = Chebfun.from_function(lambda x: np.abs(x)**9 * np.log(np.abs(x)+1e-100))
print(f3.size(), f3.sum())
91 -0.019999999999999997
HTML(r'Computing the norm of $x^2$ gives: %s $\approx \sqrt{\int_{-1}^1 x^4 dx} = \sqrt{\frac{2}{5}} = %s$' % ((x**2).norm(), np.sqrt(2./5)))
The dot
method computes the Hilbert scalar product, so f.dot(g)
corresponds to
∫1−1f(x)g(x)dx
x.dot(x)
0.6666666666666667
The integrate
method computes a primitive F of a chebfun f, which is zero at zero i.e.,
F(x)=∫x0f(y)dy
f = Chebfun.from_function(lambda t: 2/np.sqrt(np.pi) * np.exp(-t**2))
erf2_ = f.integrate()
erf2 = erf2_ - erf2_(0)
from scipy.special import erf
randx = rng.random()
print(erf2(randx) - erf(randx))
-4.218847493575595e-15
This allows to define continuous versions of the prod
and cumprod
functions for chebfuns:
def prod(f):
return np.exp(np.log(f).sum())
def cumprod(f):
return np.exp(np.log(f).integrate())
prod(np.exp(x))
0.9999999999999991
prod(np.exp(np.exp(x)))
10.489789833690239
cplot(cumprod(np.exp(x)))
One can also differentiate chebfuns to arbitrary orders with the method differentiate
f_sin5 = np.sin(5*x)
fd_sin5 = f_sin5.differentiate(4)
print(fd_sin5.norm()/f_sin5.norm())
625.0000000268575
f = np.sin(np.exp(x**2)).differentiate()
print(f(1))
-4.956699465919201
g = Chebfun.from_function(lambda x: 1/(2 + x**2))
h = g.differentiate()
print(h(1))
-0.22222222222884166
#f.differentiate().integrate() == f - f(-1)
#f.integrate().differentiate() == f
As chebfun are polynomial it is possible to find all the roots in the interval [−1,1] using the method roots
:
print((x - np.cos(x)).roots())
[0.73908513]
print((x - np.cos(4*x)).roots())
[ 0.31308831 -0.89882622 -0.53333306]
Zeros of the Bessel function J0 on the interval [0,20]:
import scipy.special
def J_0(x):
return scipy.special.jn(0,x)
#f = chebfun(lambda x: J_0(10*(1+x)))
f = Chebfun.from_function(J_0, domain=[0,20])
cplot(f)
roots = f.roots()
print('roots:', roots)
plt.plot(roots, np.zeros_like(roots), color='red', marker='.', linestyle='', markersize=10)
plt.grid()
roots: [ 2.40482556 5.52007811 8.65372791 18.07106397 11.79153444 14.93091771]
The methods min
and max
are not implemented but it is not difficult to do so, by finding the roots of the derivative.
This may come in a future version of pychebfun
.
f = x - x**2
#print('min', f.min())
#print('max', f.max())
#print('argmax', f.argmax())
#print('norm inf', f.norm('inf'))
#print('norm 1', f.norm(1))
Total variation
def total_variation(f):
return f.differentiate().norm(1)
#total_variation(x)
#total_variation(sin(5*pi*x))
Here is an example of computation of extrema of a function.
f = np.sin(6*x) + np.sin(30*np.exp(x))
r = f.roots()
fd = f.differentiate()
fdd = fd.differentiate()
e = fd.roots()
ma = e[fdd(e) <= 0]
mi = e[fdd(e) > 0]
def plot_all():
pychebfun.plot(f, with_interpolation_points=False)
plt.plot(r, np.zeros_like(r), linestyle='', marker='o', color='gray', markersize=8)
plt.plot(ma, f(ma), linestyle='', marker='o', color='green', markersize=8)
plt.plot(mi, f(mi), linestyle='', marker='o', color='red', markersize=8)
plt.grid()
plot_all()
We apply quadrature to the function f(x)=tan(x+14)+cos(10x2+exp(exp(x)))
def s(x):
return np.tan(x+1./4) + np.cos(10*x**2 + np.exp(np.exp(x)))
tancos = Chebfun.from_function(s)
cplot(tancos)
plt.title(str(tancos))
Text(0.5, 1.0, '<Chebfun(73)>')
Using the built-in quad
quadrature integrator of scipy.integrate
:
import scipy.integrate
scipy.integrate.quad(s, -1, 1, epsabs=1e-14)[0]
0.29547767624377147
Using Chebyshev integration, we obtain exactly the same value:
tancos.sum()
0.2954776762437714
One solves the ODE u′(x)=e−2.75xu(x)u(−1)=0
The problem is rewritten first as an integral problem, namely u(x)=T(u)
uold = Chebfun(0.)
du = 1.
for i in range(100):
integrand = np.exp(-2.75*x*uold)
uint = integrand.integrate()
u = uint - uint(-1)
du = u-uold
uold = u
if du.norm() < 1e-13:
break
else:
print('no convergence')
print('Converged in {} steps'.format(i))
print('u(1) = {:.14f}'.format(float(u(1))))
pychebfun.plot(u)
plt.grid()
plt.title(str(u))
Converged in 32 steps u(1) = 5.07818302385441
Text(0.5, 1.0, '<Chebfun(149)>')
One solves the boundary problem u″(x)=e4x
f = np.exp(4*x)
u_ = f.integrate().integrate()
u = u_ - (u_(1)*(x+1) + u_(-1)*(1-x))/2.
cplot(u)
plt.grid()