Helmholtz equation#

This notebook demonstrates a differentiable spectral solver for reaction–diffusion equations on the sphere, built on torch-harmonics. The solver supports two PDEs:

  • Allen–Cahn: \(\partial_t u = \epsilon\,\Delta u + u - u^3\)

  • Ginzburg–Landau: \(\partial_t u = \epsilon\,\Delta u + u - (1+i\beta)\,|u|^2 u\)

Time integration is performed in spectral space using third-order Adams–Bashforth, and the results are saved as an animated GIF.

Setup#

import torch
import matplotlib.pyplot as plt
import matplotlib.animation as animation

from math import ceil
from tqdm.auto import tqdm

from torch_harmonics.examples import SphereSolver

cmap='twilight_shifted'
/opt/anaconda/envs/torch-harmonics/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html
  from .autonotebook import tqdm as notebook_tqdm
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

Solver configuration#

The SphereSolver class provides the spectral infrastructure: it precomputes the SHT operators, quadrature weights, and the hyperdiffusion filter. We choose a resolution of 128 latitudes, a diffusion coefficient of 0.01, and integrate for 10 time units with \(\Delta t = 0.001\).

# initialize parameters:
nlat = 128
nlon = 2*nlat
lmax = ceil(nlon/4)
mmax = lmax

# timestepping
dt = 0.001
maxiter = int(10 / dt)

# initialize solver class
solver = SphereSolver(nlat, nlon, dt, lmax=lmax, mmax=mmax, coeff=0.01).to(device)

lons = solver.lons
lats = solver.lats

jj, ii = torch.triu_indices(lmax, mmax)

Initial condition#

We generate a random initial condition in spectral space using solver.randspec(). This produces a band-limited random field on the sphere:

uspec0 = solver.randspec()
fig = plt.figure(figsize=(8, 6), dpi=72)
solver.plot_specdata(uspec0, fig, cmap=cmap)
plt.show()
../_images/5ff6939b60be35e25df8123b876aee9fa1215b5823f5400b399ed188268e1da6.png

Time integration#

We integrate the PDE using third-order Adams–Bashforth. Every 20 steps, the current state is rendered into the animation. Since we only need the forward pass, the loop runs inside torch.no_grad() to save memory.

import os
os.makedirs("plots", exist_ok=True)

dudtspec = torch.zeros(3, solver.lmax, solver.mmax, dtype=torch.complex128, device=device)
nnew = 0
nnow = 1
nold = 2

uspec = uspec0.clone().to(device)
# pde = 'allen-cahn'
pde = 'ginzburg-landau'


# prepare figure for animation
fig = plt.figure(figsize=(8, 6), dpi=72)
moviewriter = animation.writers['pillow'](fps=20)
moviewriter.setup(fig, f'./plots/{pde}.gif', dpi=72)


pbar = tqdm(range(maxiter+1), desc="Helmholtz integration")
with torch.no_grad():
    for iter in pbar:
        t = iter*dt

        if iter % 20 == 0:
            pbar.set_postfix_str(f"t={t:.2f}")
            plt.clf()
            solver.plot_specdata(uspec, fig, cmap=cmap)
            plt.draw()
            moviewriter.grab_frame()

        dudtspec[nnew] = solver.dudtspec(uspec, pde=pde)
        
        # forward euler, then 2nd-order adams-bashforth time steps to start.
        if iter == 0:
            dudtspec[nnow] = dudtspec[nnew]
            dudtspec[nold] = dudtspec[nnew]
        elif iter == 1:
            dudtspec[nold] = dudtspec[nnew]

        uspec = uspec + solver.dt*( (23./12.) * dudtspec[nnew] - (16./12.) * dudtspec[nnow] + (5./12.) * dudtspec[nold] )

        # cycle through the indices
        nsav1 = nnew
        nsav2 = nnow
        nnew = nold
        nnow = nsav1
        nold = nsav2


moviewriter.finish()
Helmholtz integration: 100%|██████████| 10001/10001 [00:23<00:00, 417.34it/s, t=10.00]
../_images/8f409ce266a11276dd811900804efc60bb2da4e69ef43a6c0b5981f6989538cc.png