Padé Approximants¶
A Padé approximant is a rational functions $A(x) = P(x)/Q(x)$ where $P(x)$ and $Q(x)$ are polinomials. The provide an alternative to Taylor series, often with better behaviour. Here we consider using them to model functions with well-defined behaviour both at small and large parameter values.
Our running example will be the equation of state for a Fermi gas with S-wave scattering length $a$. We shall consider the region from weakly interacting – the BCS region – to the strongly interacting unitary Fermi gas (UFG). The equation of state will be expressed in terms of a dimensionless parameter $x = k_F a$ where $k_F$ is the Fermi momentum and $a$ is the (negative) scattering length. The equation of state will be normalized by the energy density of a non-interacting gas. We then have the following limits:
\begin{align} \frac{\mathcal{E}}{\mathcal{E}_{FG}} &= 1 + \frac{10}{9\pi}x + \frac{4(11-2\ln 2)}{21\pi^2} x^2 + \order(x^3), \tag{when $x$ is small}\\ &= \xi - \frac{\zeta}{x} + \order(x^{-2}).\tag{when $x$ is large} \end{align}Our goal is to find a Padé approximant reproducing both of these behaviours. Let's simplify this to:
\begin{align} \frac{\mathcal{E}}{\mathcal{E}_{FG}} &= \sum_{n=0}^{N_a} a_n x^n,\tag{when $x$ is small}\\ &= \sum_{n=0}^{N_b} b_n x^{-n},\tag{when $x$ is large} \end{align}from IPython.display import display
%matplotlib inline
import matplotlib.pyplot as plt
import numpy as np
a = [1.0, 10./9./np.pi, 4*(11 - 2*np.log(2))/12/np.pi**2]
b = [0.370, -0.9, 0.05]
x = np.linspace(-5, -0.01, 100)
plt.plot(x, np.polyval(a[::-1], x), label='BCS')
plt.plot(x, np.polyval(b[::-1], 1./x), label='UFG')
plt.ylim(0.37, 1)
plt.xlabel(r"$k_F a$")
plt.ylabel(r"$E/E_{FG}$")
plt.legend(loc='best')
Our goal is to find a nice smooth interpolating function that works over this range of $k_F a$. We have 6 parameters to fit here, and constants at both asymptotes which means that $P(x)$ and $Q(x)$ must have the same order $N$. Removing the overall normalization which will cancell, we have $2N - 1$ degrees of freedom, so we need $N=3$ and will have one free parameter to play with.
%install_ext http://bitbucket.org/mforbes/notes_ipython/raw/tip/notebook_utils/mmf_ext.py
%load_ext mmf_ext
import sympy
from sympy import S, var
sympy.init_printing(use_latex=False)
var('x')
var('a_0, a_1, a_2, a_3')
var('b_0, b_1, b_2')
var('p_0, p_1, p_2, p_3')
var('q_1, q_2, q_3')
BCS = a_0 + a_1*x + a_2*x**2 + a_3*x**3
UFG = b_0 + b_1/x + b_2/x**2
P = p_0 + p_1*x + p_2*x**2 + p_3* x**3
Q = 1 + q_1*x + q_2*x**2 + q_3* x**3
A = P/Q
%math A(x) = A
x, y = sympy.symbols('x, y')
N = 4
M = N
_as = sympy.symbols(map("a_{0}".format, range(M)))
_bs = sympy.symbols(map("b_{0}".format, range(2*N-M-1)))
_ps = sympy.symbols(map("p_{0}".format, range(N)))
_qs = sympy.symbols(map("q_{0}".format, range(N)))
_qs[0] = 1
A = np.polyval(_as[::-1], x)
B = np.polyval(_bs[::-1], 1/x)
P = np.polyval(_ps[::-1], x)
Q = np.polyval(_qs[::-1], x)
P/Q
Here is a direct solution using series and letting sympy do some hard work (the equations for $p_i$ and $q_i$ are non-linear in this approach.)
eqsA = sympy.poly((P/Q-A).series(x, n=len(_as)).removeO(), x).coeffs()
sol = sympy.solve(eqsA, _ps, dict=True)[0]; display(sol)
(P/Q).subs(sol).series(x, n=len(_as)).removeO().simplify()
Now we do it explicitly by collecting the coefficients of the polynomial to obtain a set of linear equations. Note that the results are the same.
eqsA = sympy.poly((P - A*Q), x).coeffs()[::-1][:len(_as)]
sol = sympy.solve(eqsA, _ps, dict=True)[0]; display(sol)
(P/Q).subs(sol).series(x, n=len(_as)).removeO().simplify()
Now for the denominator:
eqsB = sympy.poly((P/Q-B).subs(x, 1/y).series(y, n=len(_bs)).removeO(), y).coeffs()
sol = sympy.solve(eqsB, _qs[1:], dict=True)[0]; display(sol)
(P/Q).subs(sol).subs(x, 1/y).series(y, n=len(_bs)).removeO().simplify()
And the smart way:
eqsB = sympy.poly(((P-B*Q)*x**(len(_bs)-1)).expand(), x).coeffs()[:len(_bs)]
sol = sympy.solve(eqsB, _qs[1:], dict=True)[0]; display(sol)
(P/Q).subs(sol).subs(x, 1/y).series(y, n=len(_bs)).simplify()
This SEEMS TO FAIL due to Issue #9173. Be sure to simplify first before calling `series
or limit
!
(P/Q).subs(sol).subs(x, 1/y).simplify().series(y, n=len(_bs)).simplify()
Here is the complete solution:
eqsA = sympy.poly((P - A*Q), x).coeffs()[::-1][:len(_as)]
eqsB = sympy.poly(((P-B*Q).subs(x, 1/y)*y**(N-1)).expand(), y).coeffs()[::-1][:len(_bs)]
sols = sympy.solve(eqsA + eqsB, _ps + _qs[1:])
as_ = np.random.rand(len(_as))
bs_ = np.random.rand(len(_bs))
vals = dict(zip(_as, as_)+zip(_bs, bs_))
vals.update({_s: sols[_s].subs(vals) for _s in sols})
display((P/Q - A).subs(vals).simplify().series(x, n=len(_as)).simplify(),
(P/Q-B).subs(vals).subs(x, 1/y).simplify().series(y, n=2+0*len(_bs)).simplify())
The equations for the coefficients are easily found by multiplying the approrpiate polynomials and collecting terms. For the BCS limit this is simply $P(x) = Q(x)f(x)$ and then we collect the lowest powers in $x$. For the UFG limit, we do the same, but multiply by the highest inverse power and then collect the highest powers. Thus:
eqs = (sympy.Poly(P - Q*BCS, x).coeffs()[::-1][:3]
+ sympy.Poly((P - UFG*Q)*x**2, x).coeffs()[:3])
sols = sympy.solve(eqs, [p_0, p_1, p_2, q_1, q_2, q_3])
A_p3 = A.subs(sols).simplify()
display(A.series(x, n=3),
A.subs(x, 1/x).series(x, n=3).subs(x, 1/x))
sols_q = sympy.solve(sympy.Poly((P - UFG*Q)*x**2, x).coeffs()[:3], [q_1, q_2, q_3])
A.subs(sols_q).subs(x, 1/x).series(x, n=3).subs(x, 1/x).simplify().zeries
Now let's try some values. The parameter $p_3$ is still undetermined, but we must be careful to ensure that there are no poles in the region of interest $x<0$. This is ensured by choosing $p_3\lessapprox -0.04$:
a = [1.0, 10./9./np.pi, 4*(11 - 2*np.log(2))/12/np.pi**2]
b = [0.370, -0.9, 0.05]
subs = dict(zip([a_0, a_1, a_2], a) + zip([b_0, b_1, b_2], b))
subs[p_3] = -1
np.roots(sympy.Poly(Q.subs(sols).subs(subs)).coeffs())
interp = sympy.lambdify(
[x, p_3], A_p3.subs(subs), np)
_x = np.linspace(-10, -0.001, 1000)
plt.plot(_x, np.polyval(a[::-1], _x), label='BCS')
plt.plot(_x, np.polyval(b[::-1], 1./_x), label='UFG')
for p3 in np.linspace(-1000, -0.1,20):
plt.plot(_x, interp(_x, p3))
plt.ylim(0.37, 1)
plt.xlabel(r"$k_F a$")
plt.ylabel(r"$E/E_{FG}$")
plt.legend(loc='best')
Note that the interpolation is largely insensitive to the value of $p_3$ since the poles are all at positive values of $x$.