Tarang.jl

Tarang.jl

A High-Performance Spectral PDE Solver for Julia

CPU | GPU | MPI Distributed | Symbolic Equations

Build Status Documentation License: MIT


Features

Spectral Methods

  • Fourier, Chebyshev, and Legendre bases
  • Spectral accuracy for smooth solutions
  • Automatic differentiation operators

GPU Acceleration

  • Native CUDA support with cuFFT
  • KernelAbstractions.jl backend
  • Automatic CPU/GPU dispatch

MPI Parallelization

  • PencilArrays pencil decomposition
  • Scalable to thousands of cores
  • Efficient distributed FFTs

Symbolic Equations

  • Natural mathematical syntax for PDEs
  • Automatic operator construction
  • Flexible boundary conditions

Quick Start

Installation

using Pkg
Pkg.add(url="https://github.com/subhk/Tarang.jl")

That is the whole installation. MPI, PencilArrays, PencilFFTs and KernelAbstractions are hard dependencies — they are installed with Tarang, and distributed runs work out of the box.

GPU support is the one opt-in: CUDA is a weak dependency loaded through a package extension, so add it only if you want it.

Pkg.add("CUDA")     # enables the GPU backend (TarangCUDAExt)

1D Diffusion

using Tarang

domain = PeriodicDomain(64)                     # 64-point periodic [0, 2π]
T = ScalarField(domain, "T")                    # Temperature field

problem = IVP([T])
add_parameters!(problem, kappa=0.01)
add_equation!(problem, "∂t(T) - kappa*Δ(T) = 0")

set!(T, x -> sin(x))                            # Initial condition
solver = InitialValueSolver(problem, RK222(); dt=0.01)
run!(solver; stop_time=1.0)                     # That's it!

The single mode sin(x) decays as exp(-κt); after t = 1 the solver gives max|T| = 0.99004983 against the exact 0.99004983 — a relative error of 4e-12.

2D Rayleigh-Benard Convection

A bounded (Chebyshev) direction needs the tau method: one tau variable per boundary condition, lifted into the equations. See The Tau Method for Boundary Conditions for the why.

using Tarang

Lx, Lz = 4.0, 1.0
Nx, Nz = 64, 32
Rayleigh, Prandtl = 2e4, 1.0

coords = CartesianCoordinates("x", "z")
dist   = Distributor(coords; dtype=Float64, device=CPU())
xbasis = RealFourier(coords["x"]; size=Nx, bounds=(0.0, Lx), dealias=3/2)
zbasis = ChebyshevT(coords["z"]; size=Nz, bounds=(0.0, Lz), dealias=3/2)
domain = Domain(dist, (xbasis, zbasis))

p = ScalarField(domain, "p")                   # Pressure
T = ScalarField(domain, "T")                   # Temperature
u = VectorField(domain, "u")                   # Velocity

# One tau per boundary condition, carrying the Fourier bases (one per x-mode).
tau_p  = ScalarField(dist, "tau_p",  (), Float64)          # pressure gauge
tau_T1 = ScalarField(dist, "tau_T1", (xbasis,), Float64)
tau_T2 = ScalarField(dist, "tau_T2", (xbasis,), Float64)
tau_u1 = VectorField(dist, coords, "tau_u1", (xbasis,), Float64)
tau_u2 = VectorField(dist, coords, "tau_u2", (xbasis,), Float64)

# First-order reduction: grad_X = ∇X + ẑ·lift(τ)
ex, ez     = unit_vector_fields(coords, dist)
lift_basis = derivative_basis(zbasis, 1)
τ_lift(A)  = lift(A, lift_basis, -1)
grad_u = grad(u) + ez * τ_lift(tau_u1)
grad_T = grad(T) + ez * τ_lift(tau_T1)

problem = IVP([p, T, u, tau_p, tau_T1, tau_T2, tau_u1, tau_u2])
add_parameters!(problem, nu=Prandtl, buoy=Rayleigh*Prandtl, ez=ez,
                grad_u=grad_u, grad_T=grad_T, τ_lift=τ_lift)

add_equation!(problem, "trace(grad_u) + tau_p = 0")                                  # continuity
add_equation!(problem, "∂t(T) - div(grad_T) + τ_lift(tau_T2) = -u⋅∇(T)")
add_equation!(problem, "∂t(u) - nu*div(grad_u) + ∇(p) - buoy*T*ez + τ_lift(tau_u2) = -u⋅∇(u)")

# Boundary conditions go through add_bc!, never add_equation!.
add_bc!(problem, "T(z=0) = 1")                 # hot bottom
add_bc!(problem, "T(z=$Lz) = 0")               # cold top
add_bc!(problem, "u(z=0) = 0")                 # no-slip
add_bc!(problem, "u(z=$Lz) = 0")
add_bc!(problem, "integ(p) = 0")               # pressure gauge

# Conduction profile 1 - z, plus wall-damped noise to seed convection.
x, z = local_grids(dist, xbasis, zbasis)
fill_random!(T, "g"; seed=42, distribution="normal", scale=1e-3)
get_grid_data(T) .*= z' .* (1.0 .- z')
get_grid_data(T) .+= 1.0 .- z'
ensure_layout!(T, :c)

solver = InitialValueSolver(problem, RK222(); dt=1e-4)
diagnose(solver)                               # Print solver summary
run!(solver; stop_time=0.1, log_interval=100)

Time is measured in thermal diffusion times, so buoy = Ra·Pr and nu = Pr. Over those first 1000 steps the seeded noise grows into rolls (max|u_z| reaches 3.76) while the walls stay pinned to machine precision: max|T(z=0) − 1| = 3.6e-15, max|T(z=Lz)| = 8.1e-15.

A fixed `dt` will not survive a stiff Rayleigh number

This is a Quick Start, not a production run. Push Rayleigh to 2e6 with the same fixed dt=1e-4 and the velocities outrun the CFL limit — the run goes to NaN within ~100 steps, at 64×32 and at 128×64. Real runs let a CFL controller choose dt (run!(solver; cfl=cfl, ...)); see examples/ivp/rayleigh_benard_2d.jl for the full 256×64, Ra = 2e6 version.

Boundary-condition strings are parsed by Tarang, not by Julia

add_bc!(problem, "T(z=Lz) = 0") does not work: the parser resolves names against the problem's variables and add_parameters! entries, never your script's globals. A bare global warns (Unknown variable: Lz) and then fails the matrix build outright. Use a literal ("T(z=1) = 0") or interpolate the value in ("T(z=$Lz) = 0") — both enforce the boundary exactly.

This example is serial

Two things here are serial-only. Under MPI the Chebyshev axis must come first (Domain(dist, (zbasis, xbasis))) because a decomposed axis cannot hold a Chebyshev transform — and per-mode tau fields ((xbasis,)) need the Fourier axis first, so distributed runs use bare () taus. -u⋅∇(T) also puts a Chebyshev derivative on the explicit side, which the distributed solver rejects. Pure-Fourier problems parallelize with no changes at all; see Running with MPI.

Pro Tip

Use diagnose(solver) at any time to inspect field layout, compiled RHS status, dealiasing mode, and memory usage.


GPU Example

using Tarang, CUDA

# Just add device=GPU() — everything else stays the same
domain = PeriodicDomain(512, 512; device=GPU(), dtype=Float32)
field = ScalarField(domain, "u")
forward_transform!(field)   # Uses cuFFT automatically

device is the only line that changes: the same script with device=CPU() runs on the CPU, and dtype=Float32 gives ComplexF32 coefficients either way. GPU() needs CUDA.jl loaded — without it the constructor throws rather than silently falling back.


Scientific Applications

Fluid Dynamics

Navier-Stokes, Rayleigh-Benard convection, channel flow, jets

Turbulence

LES models (Smagorinsky, AMD), stochastic forcing, GQL approximation

Magnetohydrodynamics

MHD with magnetic fields, dynamo problems, magnetic dissipation

Geophysical Flows

Rotating shallow water, stratified turbulence, surface dynamics


Problem Types

TypeDescriptionExample
IVPInitial Value ProblemsTime-dependent Navier-Stokes
LBVPLinear Boundary Value ProblemsPoisson equation
NLBVPNonlinear Boundary Value ProblemsSteady nonlinear systems
EVPEigenvalue ProblemsLinear stability analysis

Spectral Bases

BasisDomainUsage
RealFourierPeriodicHorizontal directions, real-valued fields
ComplexFourierPeriodicComplex-valued fields
ChebyshevTBoundedWall-bounded domains, boundary conditions
LegendreBoundedAlternative to Chebyshev

Documentation

Installation

Setup and configuration

Tutorials

Step-by-step guides

GPU Guide

CUDA acceleration

API Reference

Complete documentation


Installation Options

SetupCommandUse Case
DefaultPkg.add(url="...")Single CPU and MPI — nothing else to install
GPUPkg.add("CUDA")NVIDIA GPU acceleration (loads TarangCUDAExt)
Cluster MPIMPIPreferences.use_system_binary()Bind MPI.jl to the cluster's own MPI

MPI, MPIPreferences, PencilArrays, PencilFFTs and KernelAbstractions are [deps] of Tarang, so a plain install is already MPI-capable: mpiexec -n 4 julia --project=. run.jl works with the MPI binary that MPI.jl ships. Only CUDA is a [weakdeps] package extension.

Requirements
  • Julia 1.10 or later
  • For GPU: NVIDIA GPU with CUDA support
  • For MPI on a cluster: call MPIPreferences.use_system_binary() once so MPI.jl uses the site's MPI (and its launcher) instead of the bundled one — see Running with MPI.

Contributing

We welcome contributions! See our GitHub repository for:

  • Bug reports and feature requests
  • Documentation improvements
  • Pull requests

Citation

If you use Tarang.jl in your research, please cite:

@software{tarang_jl,
  author = {Kar, Subhajit},
  title  = {Tarang.jl: A Spectral PDE Solver for Julia},
  url    = {https://github.com/subhk/Tarang.jl},
  year   = {2024}
}

License

Tarang.jl is released under the MIT License.

Made for the scientific computing community