Heat Equation 1D (Fast Fourier Transform)¶
Background¶
Heat Equation 1D Fft
Educational script demonstrating heat equation 1d fft concepts.
Code¶
```python """ Heat Equation 1D Fft
Educational script demonstrating heat equation 1d fft concepts. """
============================================================================¶
heat_equation_1d_FFT.py¶
============================================================================¶
import heat_equation_1d as he1 import numpy as np import matplotlib.pyplot as plt from scipy import interpolate
def fft_method_simple(): """ Simple FFT method using the package's built-in functionality. """ print("=== FFT Spectral Method ===")
# Create solver instance
solver = he1.HeatEquation1D(L=1.0, T=0.1, Nx=100, Nt=1000, D=0.01)
# Set step function initial condition
solver.set_initial_condition("step", start=0.4, end=0.6, value=1.0)
# Get analytical solution using FFT spectral method
u_analytical_fft = solver.get_analytical_solution("fourier")
# Create the plot
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(solver.x, solver.u_initial, "--r", linewidth=2, label="Initial Temperature")
ax.plot(solver.x, u_analytical_fft, "-b", linewidth=2, label="FFT Spectral Solution")
ax.set_xlabel("Position (x)")
ax.set_ylabel("Temperature (u)")
ax.set_title("Final Temperature Distribution (FFT Spectral Method)")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
def custom_fft_implementation(): """ Custom FFT implementation showing the mathematical details. """ print("=== Custom FFT Implementation ===")
# Parameters
L = 1.0
T = 0.1
D = 0.01
Nx = 128 # Use power of 2 for efficient FFT
# Spatial grid
x = np.linspace(0, L, Nx)
dx = x[1] - x[0]
# Initial condition - step function
u_initial = np.where((x >= 0.4) & (x <= 0.6), 1.0, 0.0)
# Extended domain for periodicity (key for FFT)
L_ext = 3 * L # Extend domain to reduce boundary effects
Nx_ext = 2**int(np.ceil(np.log2(5 * Nx))) # FFT-friendly size
x_ext = np.linspace(-L_ext, L_ext, Nx_ext)
dx_ext = x_ext[1] - x_ext[0]
# Evaluate initial condition on extended domain
u_ext = np.zeros_like(x_ext)
mask = (x_ext >= 0.4) & (x_ext <= 0.6)
u_ext[mask] = 1.0
# FFT of initial condition
u_hat = np.fft.fft(u_ext)
# Frequency domain grid
freqs = np.fft.fftfreq(Nx_ext, d=dx_ext) # frequencies in cycles per unit length
# Apply heat equation evolution in frequency domain
# The heat equation in Fourier space: du_hat/dt = -4*pi^2*D*k^2*u_hat
# Solution: u_hat(k,t) = u_hat(k,0) * exp(-4*pi^2*D*k^2*t)
filter_decay = np.exp(-4 * np.pi**2 * D * freqs**2 * T)
u_hat_final = u_hat * filter_decay
# Inverse FFT to get solution
u_final_ext = np.fft.ifft(u_hat_final).real
# Interpolate back to original grid
interp = interpolate.interp1d(x_ext, u_final_ext, kind='cubic',
fill_value="extrapolate", bounds_error=False)
u_final = interp(x)
# Create the plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))
# Left plot: Solution
ax1.plot(x, u_initial, "--r", linewidth=2, label="Initial Temperature")
ax1.plot(x, u_final, "-b", linewidth=2, label="FFT Solution")
ax1.set_xlabel("Position (x)")
ax1.set_ylabel("Temperature (u)")
ax1.set_title("Custom FFT Implementation")
ax1.grid(True, alpha=0.3)
ax1.legend()
# Right plot: Frequency domain
ax2.semilogy(freqs[:Nx_ext//2], np.abs(u_hat[:Nx_ext//2]), "-g",
linewidth=2, label="Initial Spectrum")
ax2.semilogy(freqs[:Nx_ext//2], np.abs(u_hat_final[:Nx_ext//2]), "-b",
linewidth=2, label="Final Spectrum")
ax2.semilogy(freqs[:Nx_ext//2], filter_decay[:Nx_ext//2], "--r",
linewidth=2, label="Decay Filter")
ax2.set_xlabel("Frequency (cycles/unit)")
ax2.set_ylabel("Magnitude")
ax2.set_title("Frequency Domain Evolution")
ax2.grid(True, alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
return x, u_initial, u_final
def compare_all_methods(): """ Compare FFT spectral method with other analytical methods. """ print("=== Comparing All Analytical Methods ===")
# Create solver
solver = he1.HeatEquation1D(L=1.0, T=0.1, Nx=100, Nt=1000, D=0.01)
solver.set_initial_condition("step", start=0.4, end=0.6, value=1.0)
# Get solutions using all three analytical methods
u_eigenfunction = solver.get_analytical_solution("eigenfunction")
u_heat_kernel = solver.get_analytical_solution("heat_kernel")
u_fft = solver.get_analytical_solution("fourier")
# Create comparison plot
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))
# Left plot: All methods
ax1.plot(solver.x, solver.u_initial, "--k", linewidth=2, label="Initial", alpha=0.7)
ax1.plot(solver.x, u_eigenfunction, "-r", linewidth=2, label="Eigenfunction")
ax1.plot(solver.x, u_heat_kernel, "--b", linewidth=2, label="Heat Kernel")
ax1.plot(solver.x, u_fft, "-.g", linewidth=2, label="FFT Spectral")
ax1.set_xlabel("Position (x)")
ax1.set_ylabel("Temperature (u)")
ax1.set_title("Comparison of Analytical Methods")
ax1.grid(True, alpha=0.3)
ax1.legend()
# Right plot: Differences
diff_fft_eigen = np.abs(u_fft - u_eigenfunction)
diff_fft_kernel = np.abs(u_fft - u_heat_kernel)
ax2.semilogy(solver.x, diff_fft_eigen, "-r", linewidth=2,
label=f"FFT vs Eigenfunction (max: {np.max(diff_fft_eigen):.2e})")
ax2.semilogy(solver.x, diff_fft_kernel, "-b", linewidth=2,
label=f"FFT vs Heat Kernel (max: {np.max(diff_fft_kernel):.2e})")
ax2.set_xlabel("Position (x)")
ax2.set_ylabel("Absolute Difference")
ax2.set_title("Differences Between Methods")
ax2.grid(True, alpha=0.3)
ax2.legend()
plt.tight_layout()
plt.show()
print(f"Max difference FFT vs Eigenfunction: {np.max(diff_fft_eigen):.2e}")
print(f"Max difference FFT vs Heat Kernel: {np.max(diff_fft_kernel):.2e}")
def fft_time_evolution(): """ Show time evolution using FFT method. """ print("=== FFT Time Evolution ===")
# Parameters
L = 1.0
D = 0.01
Nx = 128
# Create spatial grid
x = np.linspace(0, L, Nx)
# Initial condition function
def step_initial_condition(x):
return np.where((x >= 0.4) & (x <= 0.6), 1.0, 0.0)
# Time points for evolution
times = [0.0, 0.02, 0.05, 0.08, 0.1]
fig, ax = plt.subplots(figsize=(12, 8))
colors = ['black', 'blue', 'green', 'orange', 'red']
for i, t in enumerate(times):
if t == 0:
u_t = step_initial_condition(x)
ax.plot(x, u_t, "--", color=colors[i], linewidth=2,
label=f"t = {t:.2f} (Initial)")
else:
# Use FFT spectral method for each time
u_t = he1.solve_analytical(x, t, step_initial_condition, D, L, method="fourier")
ax.plot(x, u_t, "-", color=colors[i], linewidth=2,
label=f"t = {t:.2f}")
ax.set_xlabel("Position (x)")
ax.set_ylabel("Temperature (u)")
ax.set_title("Heat Diffusion Evolution Using FFT Spectral Method")
ax.grid(True, alpha=0.3)
ax.legend()
plt.tight_layout()
plt.show()
def original_style_fft(): """ Create exact match to original style using FFT method. """ print("=== Original Style with FFT Method ===")
# Original lambda function
f = lambda x: np.where((x >= 0.4) & (x <= 0.6), 1.0, 0.0)
# Create solver instance
solver = he1.HeatEquation1D()
# Set custom initial condition using the lambda function
solver.set_initial_condition("custom", func=f)
# Get analytical solution using FFT spectral method
u_exact = solver.get_analytical_solution("fourier")
x = solver.x
u_initial = f(x)
u = u_exact
# Original plotting code with FFT
fig, ax = plt.subplots(figsize=(8, 4))
ax.plot(solver.x, f(solver.x), "--r", label="Initial Temperature")
ax.plot(solver.x, u_exact, label="Final Temperature")
ax.set_xlabel("Position (x)")
ax.set_ylabel("Temperature (u)")
ax.set_title("Final Temperature Distribution (FFT Spectral Method)")
ax.grid(True)
ax.legend()
plt.tight_layout()
plt.show()
def print_fft_theory(): """ Print the theory behind FFT spectral method. """ print("\n" + "="60) print("FFT SPECTRAL METHOD THEORY") print("="60) print(""" The FFT spectral method solves the heat equation in frequency domain:
-
Heat equation: du/dt = D * d2u/dx2
-
Fourier transform: u_hat(k,t) = FFT[u(x,t)]
-
In frequency domain: du_hat/dt = -4pi^2Dk^2u_hat
-
Solution: u_hat(k,t) = u_hat(k,0) * exp(-4pi^2Dk^2t)
-
Inverse FFT: u(x,t) = IFFT[u_hat(k,t)]
ADVANTAGES: - Spectral accuracy (exponential convergence for smooth solutions) - Natural handling of periodic boundary conditions - Efficient for smooth initial conditions
LIMITATIONS: - Assumes periodic boundary conditions - Less effective for discontinuous initial conditions - Requires extended domain to minimize boundary effects """) print("="*60)
if name == "main": """ Main execution - run all FFT examples with proper encoding. """ print("Starting FFT Heat Equation Solutions...") print("="*60)
try:
# Run all FFT examples
fft_method_simple()
custom_fft_implementation()
compare_all_methods()
fft_time_evolution()
original_style_fft()
print_fft_theory()
print("\nAll FFT examples completed successfully!")
except Exception as e:
print(f"Error occurred: {e}")
print("Please check your heat_equation_1d package installation.")
```
Exercises¶
Exercise 1. In the FFT spectral method, the heat equation becomes \(\hat{u}_t = -4\pi^2 D k^2 \hat{u}\) in frequency domain. Solve this ODE and explain why high-frequency components decay faster.
Solution to Exercise 1
The ODE \(\hat{u}_t = -4\pi^2 D k^2 \hat{u}\) has solution
The decay rate is \(4\pi^2 D k^2\), which grows quadratically with frequency \(k\). High-frequency components (large \(|k|\)) decay exponentially faster than low-frequency components. This is why the heat equation smooths out sharp features (high-frequency content) while preserving the overall shape (low-frequency content). After time \(t\), frequencies above \(k_c \approx 1/\sqrt{4\pi^2 D t}\) are effectively damped to zero.
Exercise 2. Explain why the FFT-based method uses an extended domain (\(L_{\text{ext}} = 3L\)) rather than the physical domain \([0, L]\) alone.
Solution to Exercise 2
The FFT assumes periodicity: the signal repeats outside the computational domain. If the initial condition is nonzero at the boundaries (or if the physical problem has Dirichlet conditions \(u = 0\)), the periodic extension creates artificial discontinuities at the boundaries. These discontinuities generate spurious high-frequency content (spectral leakage) that corrupts the solution.
Extending the domain to \([-3L, 3L]\) with zero padding ensures that the periodic copies are far from the physical region \([0, L]\). The solution on \([0, L]\) is then obtained by interpolation, free from boundary contamination.
Exercise 3. Why is the FFT size chosen as a power of 2 (e.g., \(N_{\text{ext}} = 2^{\lceil\log_2(5N_x)\rceil}\))? What is the computational complexity of the FFT?
Solution to Exercise 3
The Cooley-Tukey FFT algorithm achieves \(O(N \log N)\) complexity when \(N\) is a power of 2, by recursively decomposing the DFT into smaller DFTs. For non-power-of-2 sizes, the algorithm is less efficient or falls back to \(O(N^2)\).
For the extended domain with \(5N_x\) points, rounding up to the next power of 2 ensures optimal FFT performance. For example, with \(N_x = 100\), we need \(5 \times 100 = 500\) points, and the next power of 2 is \(512 = 2^9\).
Exercise 4. The code compares three analytical methods: eigenfunction expansion, heat kernel, and FFT spectral. Under what conditions would you expect the FFT method to outperform the eigenfunction expansion?
Solution to Exercise 4
The FFT method outperforms eigenfunction expansion when:
- Smooth initial conditions: FFT spectral methods achieve spectral (exponential) convergence for smooth functions, while eigenfunction expansion convergence depends on the decay of Fourier coefficients.
- Non-standard domains: FFT naturally handles periodic or extended domains, while eigenfunction expansion requires specific boundary conditions (Dirichlet) and known eigenfunctions.
- Computational efficiency: For \(N\) modes, eigenfunction expansion costs \(O(N \cdot N_x)\) while FFT costs \(O(N_x \log N_x)\), making FFT faster when many modes are needed.
Conversely, eigenfunction expansion is better for discontinuous initial conditions (where FFT suffers from Gibbs phenomena on the extended domain) and when only a few dominant modes are needed.