Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

The Two Faces of Polynomial Interpolants

Given n+1n+1 data points (xj,fj)(x_j, f_j) where xjx_j are Chebyshev points, the polynomial interpolant pn(x)p_n(x) is unique. But we can represent it in two equivalent ways:

Value Representation

Using Lagrange polynomials j(x)\ell_j(x):

pn(x)=j=0nfjj(x)pn(f0f1fn)p_n(x) = \sum_{j=0}^{n} f_j \ell_j(x) \quad \longleftrightarrow \quad \mathbf{p}_n \equiv \begin{pmatrix} f_0 \\ f_1 \\ \vdots \\ f_n \end{pmatrix}

The polynomial is uniquely determined by the sampled function values {fj}\{f_j\}.

Coefficient Representation

Using Chebyshev polynomials Tk(x)T_k(x):

pn(x)=k=0nckTk(x)pn(c0c1cn)p_n(x) = \sum_{k=0}^{n} c_k T_k(x) \quad \longleftrightarrow \quad \mathbf{p}_n \equiv \begin{pmatrix} c_0 \\ c_1 \\ \vdots \\ c_n \end{pmatrix}

The polynomial is uniquely determined by its Chebyshev coefficients {ck}\{c_k\}.

The Connection: A Linear Map

Evaluating the Chebyshev series at the data points gives:

fi=pn(xi)=k=0nckTk(xi)f_i = p_n(x_i) = \sum_{k=0}^{n} c_k T_k(x_i)

This is a matrix-vector product:

(f0f1fn)=(T0(x0)T1(x0)Tn(x0)T0(x1)T1(x1)Tn(x1)T0(xn)T1(xn)Tn(xn))(c0c1cn)\begin{pmatrix} f_0 \\ f_1 \\ \vdots \\ f_n \end{pmatrix} = \begin{pmatrix} T_0(x_0) & T_1(x_0) & \cdots & T_n(x_0) \\ T_0(x_1) & T_1(x_1) & \cdots & T_n(x_1) \\ \vdots & \vdots & \ddots & \vdots \\ T_0(x_n) & T_1(x_n) & \cdots & T_n(x_n) \end{pmatrix} \begin{pmatrix} c_0 \\ c_1 \\ \vdots \\ c_n \end{pmatrix}

Since Tk(xj)=Tk(cos(jπ/n))=cos(jkπ/n)T_k(x_j) = T_k(\cos(j\pi/n)) = \cos(jk\pi/n), this is exactly the Discrete Cosine Transform matrix!

Two Representations

Consider a polynomial p(x)p(x) of degree at most nn.

Value Space

Store the values at the n+1n+1 Chebyshev points:

f=(f0f1fn)=(p(x0)p(x1)p(xn))\mathbf{f} = \begin{pmatrix} f_0 \\ f_1 \\ \vdots \\ f_n \end{pmatrix} = \begin{pmatrix} p(x_0) \\ p(x_1) \\ \vdots \\ p(x_n) \end{pmatrix}

where xk=cos(kπ/n)x_k = \cos(k\pi/n) for k=0,1,,nk = 0, 1, \ldots, n.

Advantages:

Coefficient Space

Store the Chebyshev coefficients:

c=(c0c1cn)wherep(x)=k=0nckTk(x)\mathbf{c} = \begin{pmatrix} c_0 \\ c_1 \\ \vdots \\ c_n \end{pmatrix} \quad \text{where} \quad p(x) = \sum_{k=0}^{n} c_k T_k(x)

Advantages:

The Discrete Cosine Transform

The key insight: under x=cosθx = \cos\theta, Chebyshev polynomials become cosines:

Tk(cosθ)=cos(kθ)T_k(\cos\theta) = \cos(k\theta)

At Chebyshev points xj=cos(jπ/n)x_j = \cos(j\pi/n), we have θj=jπ/n\theta_j = j\pi/n, so:

Tk(xj)=cos(jkπn)T_k(x_j) = \cos\left(\frac{jk\pi}{n}\right)

This is exactly the DCT-I matrix!

Values to Coefficients

Given values f\mathbf{f}, the coefficients are:

ck=2nj=0nfjcos(jkπn)c_k = \frac{2}{n} \sum_{j=0}^{n}{}'' f_j \cos\left(\frac{jk\pi}{n}\right)

where \sum'' means the first and last terms are halved.

import scipy.fft as fft

def vals2coeffs(values):
    """Convert values at Chebyshev points to coefficients."""
    n = len(values) - 1
    if n == 0:
        return values.copy()

    # Use DCT-I
    coeffs = fft.dct(values[::-1], type=1, norm='forward')

    # Scale interior coefficients
    coeffs[1:n] *= 2.0
    return coeffs

Coefficients to Values

Given coefficients c\mathbf{c}, the values are:

fj=k=0nckcos(jkπn)f_j = \sum_{k=0}^{n} c_k \cos\left(\frac{jk\pi}{n}\right)
def coeffs2vals(coeffs):
    """Convert coefficients to values at Chebyshev points."""
    n = len(coeffs) - 1
    if n == 0:
        return coeffs.copy()

    # Undo scaling
    coeffs_scaled = coeffs.copy()
    coeffs_scaled[1:n] /= 2.0

    # Use inverse DCT-I
    values = fft.idct(coeffs_scaled, type=1, norm='forward')
    return values[::-1]

Example: f(x)=x3f(x) = x^3

n = 4
x = np.cos(np.pi * np.arange(n) / (n-1))  # Chebyshev points
f = x**3                                    # Values

c = vals2coeffs(f)
# c = [0, 0.75, 0, 0.25]

The polynomial x3x^3 has the exact Chebyshev expansion:

x3=34T1(x)+14T3(x)x^3 = \frac{3}{4}T_1(x) + \frac{1}{4}T_3(x)
The Linear System

Let’s derive this by setting up and solving the linear system explicitly.

Step 1: Chebyshev points (n=4n = 4, so θj=jπ/3\theta_j = j\pi/3):

jjθj\theta_jxj=cosθjx_j = \cos\theta_jfj=xj3f_j = x_j^3
0011
1π/3\pi/31/21/21/81/8
22π/32\pi/31/2-1/21/8-1/8
3π\pi-1-1

Step 2: Build the Chebyshev matrix Tjk=Tk(xj)T_{jk} = T_k(x_j):

Using T0=1T_0 = 1, T1=xT_1 = x, T2=2x21T_2 = 2x^2 - 1, T3=4x33xT_3 = 4x^3 - 3x:

T=(T0(1)T1(1)T2(1)T3(1)T0(12)T1(12)T2(12)T3(12)T0(12)T1(12)T2(12)T3(12)T0(1)T1(1)T2(1)T3(1))=(11111121211121211111)T = \begin{pmatrix} T_0(1) & T_1(1) & T_2(1) & T_3(1) \\ T_0(\tfrac{1}{2}) & T_1(\tfrac{1}{2}) & T_2(\tfrac{1}{2}) & T_3(\tfrac{1}{2}) \\ T_0(-\tfrac{1}{2}) & T_1(-\tfrac{1}{2}) & T_2(-\tfrac{1}{2}) & T_3(-\tfrac{1}{2}) \\ T_0(-1) & T_1(-1) & T_2(-1) & T_3(-1) \end{pmatrix} = \begin{pmatrix} 1 & 1 & 1 & 1 \\ 1 & \tfrac{1}{2} & -\tfrac{1}{2} & -1 \\ 1 & -\tfrac{1}{2} & -\tfrac{1}{2} & 1 \\ 1 & -1 & 1 & -1 \end{pmatrix}

Step 3: Set up the system f=Tc\mathbf{f} = T\mathbf{c}:

(11/81/81)=(11111121211121211111)(c0c1c2c3)\begin{pmatrix} 1 \\ 1/8 \\ -1/8 \\ -1 \end{pmatrix} = \begin{pmatrix} 1 & 1 & 1 & 1 \\ 1 & \tfrac{1}{2} & -\tfrac{1}{2} & -1 \\ 1 & -\tfrac{1}{2} & -\tfrac{1}{2} & 1 \\ 1 & -1 & 1 & -1 \end{pmatrix} \begin{pmatrix} c_0 \\ c_1 \\ c_2 \\ c_3 \end{pmatrix}

Step 4: Solve (or use the DCT, which exploits the structure of TT):

c=T1f=(03/401/4)\mathbf{c} = T^{-1}\mathbf{f} = \begin{pmatrix} 0 \\ 3/4 \\ 0 \\ 1/4 \end{pmatrix}

Verification: Tc=fT\mathbf{c} = \mathbf{f}?

  • Row 0: 0(1)+34(1)+0(1)+14(1)=10(1) + \tfrac{3}{4}(1) + 0(1) + \tfrac{1}{4}(1) = 1

  • Row 1: 0(1)+34(12)+0(12)+14(1)=3814=180(1) + \tfrac{3}{4}(\tfrac{1}{2}) + 0(-\tfrac{1}{2}) + \tfrac{1}{4}(-1) = \tfrac{3}{8} - \tfrac{1}{4} = \tfrac{1}{8}

  • Row 2: 0(1)+34(12)+0(12)+14(1)=38+14=180(1) + \tfrac{3}{4}(-\tfrac{1}{2}) + 0(-\tfrac{1}{2}) + \tfrac{1}{4}(1) = -\tfrac{3}{8} + \tfrac{1}{4} = -\tfrac{1}{8}

  • Row 3: 0(1)+34(1)+0(1)+14(1)=10(1) + \tfrac{3}{4}(-1) + 0(1) + \tfrac{1}{4}(-1) = -1

Why the DCT is faster

The matrix TT has special structure: Tjk=cos(jkπ/(n1))T_{jk} = \cos(jk\pi/(n-1)). This is exactly the DCT-I matrix, and the FFT algorithm exploits this structure to compute T1fT^{-1}\mathbf{f} in O(nlogn)O(n \log n) instead of the O(n3)O(n^3) required for general matrix inversion.

Algebraic verification

Using T1(x)=xT_1(x) = x and T3(x)=4x33xT_3(x) = 4x^3 - 3x, we can verify directly:

34T1(x)+14T3(x)=34x+14(4x33x)=34x+x334x=x3\frac{3}{4}T_1(x) + \frac{1}{4}T_3(x) = \frac{3}{4}x + \frac{1}{4}(4x^3 - 3x) = \frac{3}{4}x + x^3 - \frac{3}{4}x = x^3 \quad \checkmark

Example: f(x)=x5f(x) = x^5

Similarly:

x5=1016T1(x)+516T3(x)+116T5(x)x^5 = \frac{10}{16}T_1(x) + \frac{5}{16}T_3(x) + \frac{1}{16}T_5(x)

The pattern: monomials have sparse Chebyshev representations (only odd or even terms appear).

The Clenshaw Algorithm

To evaluate p(x)=k=0nckTk(x)p(x) = \sum_{k=0}^{n} c_k T_k(x) without computing each TkT_k, use Clenshaw’s algorithm:

def clenshaw(x, coeffs):
    """Evaluate Chebyshev series at x using Clenshaw algorithm."""
    n = len(coeffs) - 1
    if n == 0:
        return coeffs[0] * np.ones_like(x)

    b_k1 = np.zeros_like(x)
    b_k2 = np.zeros_like(x)

    for k in range(n, 1, -1):
        b_k2, b_k1 = b_k1, coeffs[k] + 2*x*b_k1 - b_k2

    return coeffs[0] + x*b_k1 - b_k2

This is analogous to Horner’s method for monomial expansions.

Why Two Representations?

Different operations are natural in each space:

OperationValue SpaceCoefficient Space
Sample ffDirect: f(xk)f(x_k)Need inverse DCT
DifferentiateMatrix multiply: DfD\mathbf{f}Recurrence relation
IntegrateNeed DCT firstDirect formula
Multiply fgf \cdot gComponentwiseConvolution (expensive)
Assess smoothnessNot easyCoefficient decay
TruncateNot easyDrop small coefficients

The Freedom to Choose

The key insight is that translation between representations is cheap (O(nlogn)O(n \log n)), so we can work in whichever space is most convenient for each operation.

Example: Computing 11f(x)2dx\int_{-1}^{1} f(x)^2 \, dx

Given the interpolant pn(x)p_n(x) for f(x)f(x):

  1. Square in value space: (f02,f12,,fn2)(f_0^2, f_1^2, \ldots, f_n^2) — just componentwise!

  2. Transform to coefficient space: Use DCT

  3. Integrate in coefficient space: Use the closed-form Chebyshev integral formula

This hybrid approach is often optimal.

Extension to Infinite Dimensions

For a general Lipschitz continuous function f(x)f(x), the Chebyshev series is infinite:

f(x)=k=0ckTk(x),f(c0,c1,c2,)Tf(x) = \sum_{k=0}^{\infty} c_k T_k(x), \quad f \equiv (c_0, c_1, c_2, \ldots)^T

This “infinite vector” is called a quasivector in the literature. While working with infinitely many coefficients requires care (functional analysis!), the practical reality is that:

The Chebfun Philosophy

Modern software like Chebfun (MATLAB) and ApproxFun (Julia) represent functions as their Chebyshev coefficients, automatically:

  1. Sampling the function at Chebyshev points

  2. Computing coefficients via DCT

  3. Adaptively choosing nn until coefficients decay to machine precision

This gives “numerical functions” that can be manipulated like symbolic functions but with guaranteed accuracy.

# Conceptual chebfun-style workflow
def chebfun(f, tol=1e-14):
    """Create adaptive Chebyshev approximation."""
    for n in [16, 32, 64, 128, 256, 512, 1024]:
        x = chebpts(n)
        vals = f(x)
        coeffs = vals2coeffs(vals)

        # Check if coefficients have decayed
        if np.max(np.abs(coeffs[-3:])) < tol * np.max(np.abs(coeffs)):
            return coeffs  # Converged!

    raise ValueError("Function too complex or non-smooth")

Complexity Summary

OperationCost
Values → CoefficientsO(nlogn)O(n \log n) via DCT
Coefficients → ValuesO(nlogn)O(n \log n) via inverse DCT
Evaluate at one pointO(n)O(n) via Clenshaw or barycentric
Differentiate (value space)O(n2)O(n^2) matrix-vector product
Integrate (coefficient space)O(n)O(n)

The DCT/FFT connection makes transforming between representations cheap, so we can use whichever is more convenient for each operation.

Aliasing: The Sampling Pitfall

When we sample a function at NN discrete points, we cannot distinguish high-frequency components from low-frequency ones. This is aliasing—the bane of spectral methods.

The Nyquist Limit

For polynomial interpolation at N+1N+1 points: we can represent polynomials of degree at most NN. Anything higher gets aliased to lower frequencies.

Aliasing in Action

Consider sampling cos(10θ)\cos(10\theta) at N=8N = 8 points (θj=jπ/8\theta_j = j\pi/8):

N = 8
theta = np.pi * np.arange(N) / N
f_high = np.cos(10 * theta)  # Frequency 10 > N/2 = 4

# These samples are IDENTICAL to cos(6*theta)!
f_low = np.cos(6 * theta)
np.allclose(f_high, f_low)  # True!

The frequency-10 wave “folds back” and appears as frequency 6. We can’t tell them apart from samples alone.

Why Aliasing Matters

Aliasing causes silent errors:

  1. No warning: The computed coefficients look reasonable

  2. Wrong answer: High-frequency content corrupts low-frequency coefficients

  3. Hard to detect: Everything “works” until you compare with a finer grid

The 2/3 Rule for Nonlinear Terms

When computing products like uvu \cdot v in spectral methods, aliasing becomes critical.

The problem: If uu and vv each have frequencies up to N/2N/2, then uvu \cdot v has frequencies up to NN—exceeding our resolution!

def dealias_product(u_hat, v_hat):
    """Compute u*v with 2/3 dealiasing."""
    N = len(u_hat)
    M = 3 * N // 2  # Padding factor

    # Zero-pad to finer grid
    u_pad = np.zeros(M, dtype=complex)
    v_pad = np.zeros(M, dtype=complex)
    u_pad[:N//2] = u_hat[:N//2]
    u_pad[-(N//2):] = u_hat[-(N//2):]
    v_pad[:N//2] = v_hat[:N//2]
    v_pad[-(N//2):] = v_hat[-(N//2):]

    # Multiply in physical space
    uv = np.fft.ifft(u_pad) * np.fft.ifft(v_pad)

    # Transform back and truncate
    uv_hat = np.fft.fft(uv)
    result = np.zeros(N, dtype=complex)
    result[:N//2] = uv_hat[:N//2]
    result[-(N//2):] = uv_hat[-(N//2):]
    return result * (M / N)  # Normalization

Practical Guidelines

SituationRecommendation
Linear problemsAliasing less critical—errors stay bounded
Nonlinear productsAlways dealias (2/3 rule or zero-padding)
Function evaluationCheck convergence by comparing NN and 2N2N
Unknown smoothnessMonitor coefficient decay

Detecting Aliasing

How to know if your solution is aliased:

  1. Coefficient plateau: If ck|c_k| doesn’t decay to machine precision, you may need more points

  2. Resolution test: Double NN and compare—if answer changes significantly, you were underresolved

  3. Energy in high modes: If significant energy near k=N/2k = N/2, aliasing is likely

def check_resolution(coeffs, tol=1e-10):
    """Warn if coefficients suggest underresolution."""
    if np.max(np.abs(coeffs[-5:])) > tol * np.max(np.abs(coeffs)):
        print("Warning: coefficients not fully resolved—aliasing possible!")

The Moral

“Aliasing is the price of discretization.”

When sampling continuous functions at discrete points, information is lost. Nyquist tells us exactly how much bandwidth we can capture. For spectral methods to work reliably: