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

Problem API and execution support

ProblemSolverPurpose
InitialValueProblemInitialValueSolverTime evolution
LinearBoundaryValueProblemBoundaryValueSolverSteady linear equations
NonlinearBoundaryValueProblemBoundaryValueSolverSteady nonlinear equations
EigenvalueProblemEigenvalueSolverEigenvalues and modes

The abbreviated IVP, LBVP, NLBVP, and EVP aliases have been removed. Use the corresponding full names above when updating an existing script.

Boundary conditions support spatial values in linear and nonlinear steady solves, and parameterized moving values during time stepping. Normal-coordinate references use the wall position. Structured stress-free conditions preserve scalar component selection; periodic markers record metadata without adding constraints.

CPU concurrency uses exclusive scratch storage for independent Fourier derivatives and shared-factor matrix solves. GPU linear boundary solves keep solve buffers on the device. Nonlinear GPU boundary-value and GPU eigenvalue solves remain unsupported. Consult the time-stepper execution table for the supported combinations of operators, CPU, MPI, and GPU execution.

This site's dev version follows main; stable follows tagged releases. Pull-request previews are separate builds, so unreleased changes do not appear in the stable manual automatically.

Installation

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

That is the whole serial installation. MPI, PencilArrays, PencilFFTs and KernelAbstractions are hard dependencies and are installed with Tarang. For a distributed script, add MPI as a direct dependency of the script's project and install its compatible launcher once on Unix, macOS, or WSL:

julia --project=. -e 'using Pkg; Pkg.add("MPI"); using MPI; MPI.install_mpiexecjl()'

On native Windows, launch through MPI.mpiexec() as shown in the installation guide.

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.

julia --project=@v#.# -e 'using Pkg; Pkg.add("CUDA")' # enables TarangCUDAExt

1D Diffusion

using Tarang

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

problem = InitialValueProblem([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 = InitialValueProblem([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(128, 128; 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
InitialValueProblemInitial Value ProblemsTime-dependent Navier-Stokes
LinearBoundaryValueProblemLinear Boundary Value ProblemsPoisson equation
NonlinearBoundaryValueProblemNonlinear Boundary Value ProblemsSteady nonlinear systems
EigenvalueProblemEigenvalue 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-process CPU
MPIAdd MPI; use mpiexecjl (Unix/WSL) or MPI.mpiexec() (Windows)Multi-process CPU with the matching MPI runtime
GPUjulia --project=@v#.# -e 'using Pkg; Pkg.add("CUDA")'NVIDIA GPU acceleration (loads TarangCUDAExt)
Cluster MPIMPIPreferences.use_system_binary()Bind MPI.jl to the cluster's own MPI

Tarang installs MPI, MPIPreferences, PencilArrays, PencilFFTs and KernelAbstractions transitively. Julia scripts that import MPI directly must also add it to their active project. Then install MPI.jl's project-aware launcher once with MPI.install_mpiexecjl() on Unix/WSL and run mpiexecjl --project=. -n 4 julia run.jl; on Windows, use MPI.mpiexec() as described above. 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