Overview
Dedalus supports four main problem types for solving PDEs: Initial Value Problems (IVP), Eigenvalue Problems (EVP), Linear Boundary Value Problems (LBVP), and Nonlinear Boundary Value Problems (NLBVP). Each problem type is defined in dedalus/core/problems.py.
Initial Value Problems (IVP)
Time-dependent problems that evolve from initial conditions.
Problem Structure
IVPs support equations of the form:
Where:
- M, L: Linear operators (time-independent)
- X: Problem variables
- F: Possibly nonlinear RHS (no explicit time derivatives)
- t: Time variable
Creating an IVP
import numpy as np
import dedalus.public as d3
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.float64)
xbasis = d3.RealFourier(coords['x'], size=128, bounds=(0, 4), dealias=3/2)
zbasis = d3.ChebyshevT(coords['z'], size=64, bounds=(0, 1), dealias=3/2)
# Define fields
p = dist.Field(name='p', bases=(xbasis, zbasis))
b = dist.Field(name='b', bases=(xbasis, zbasis))
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
# Tau terms for boundary conditions
tau_p = dist.Field(name='tau_p')
tau_b1 = dist.Field(name='tau_b1', bases=xbasis)
tau_b2 = dist.Field(name='tau_b2', bases=xbasis)
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
# Create IVP with time variable
problem = d3.IVP(
[p, b, u, tau_p, tau_b1, tau_b2, tau_u1, tau_u2],
time='t',
namespace=locals()
)
Parameters
- variables: List of Field objects to solve for
- time: Time variable name (default: ‘t’) or Field object
- namespace: Dictionary for parsing string equations (recommended:
locals())
Adding Equations
# Parameters
kappa = 1e-3
nu = 1e-3
Lz = 1
# Grids and substitutions
x, z = dist.local_grids(xbasis, zbasis)
ex, ez = coords.unit_vector_fields(dist)
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
grad_u = d3.grad(u) + ez*lift(tau_u1)
grad_b = d3.grad(b) + ez*lift(tau_b1)
# Add equations
problem.add_equation("trace(grad_u) + tau_p = 0")
problem.add_equation("dt(b) - kappa*div(grad_b) + lift(tau_b2) = -u@grad(b)")
problem.add_equation("dt(u) - nu*div(grad_u) + grad(p) - b*ez + lift(tau_u2) = -u@grad(u)")
# Boundary conditions
problem.add_equation("b(z=0) = Lz")
problem.add_equation("u(z=0) = 0")
problem.add_equation("b(z=Lz) = 0")
problem.add_equation("u(z=Lz) = 0")
# Pressure gauge
problem.add_equation("integ(p) = 0")
Equation Constraints
IVP LHS Requirements:
- Must be linear in problem variables
- Must be time-independent
- Must be first-order in time derivatives
IVP RHS Requirements:
- Can be nonlinear in problem variables
- Can depend on time
- Cannot contain explicit time derivatives
Eigenvalue Problems (EVP)
Linear eigenvalue problems for stability analysis.
Problem Structure
EVPs support equations of the form:
Where:
- λ: Eigenvalue
- M, L: Linear operators
- X: Eigenvector (problem variables)
Creating an EVP
import numpy as np
import dedalus.public as d3
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.complex128) # Usually complex
xbasis = d3.ComplexFourier(coords['x'], size=128, bounds=(0, 4))
zbasis = d3.ChebyshevT(coords['z'], size=64, bounds=(0, 1))
# Define perturbation fields
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
b = dist.Field(name='b', bases=(xbasis, zbasis))
p = dist.Field(name='p', bases=(xbasis, zbasis))
# Tau terms
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
tau_b1 = dist.Field(name='tau_b1', bases=xbasis)
tau_b2 = dist.Field(name='tau_b2', bases=xbasis)
tau_p = dist.Field(name='tau_p')
# Create eigenvalue field
eigenvalue = dist.Field(name='λ')
# Create EVP
problem = d3.EVP(
[u, b, p, tau_u1, tau_u2, tau_b1, tau_b2, tau_p],
eigenvalue,
namespace=locals()
)
Adding Equations
# Parameters
Ra = 1e6
Pr = 1
kappa = (Ra * Pr)**(-1/2)
nu = (Ra / Pr)**(-1/2)
# Substitutions
ex, ez = coords.unit_vector_fields(dist)
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
grad_u = d3.grad(u) + ez*lift(tau_u1)
grad_b = d3.grad(b) + ez*lift(tau_b1)
# EVP equations (λ appears explicitly)
problem.add_equation("trace(grad_u) + tau_p = 0")
problem.add_equation("λ*b - kappa*div(grad_b) + lift(tau_b2) = 0")
problem.add_equation("λ*u - nu*div(grad_u) + grad(p) - b*ez + lift(tau_u2) = 0")
# Boundary conditions
problem.add_equation("b(z=0) = 0")
problem.add_equation("u(z=0) = 0")
problem.add_equation("b(z=1) = 0")
problem.add_equation("u(z=1) = 0")
problem.add_equation("integ(p) = 0")
Equation Constraints
EVP Requirements:
- LHS must be linear in problem variables
- LHS must be affine in the eigenvalue (can have constant term)
- RHS must be zero
Converting IVP to EVP
For time-independent IVPs, you can automatically generate an EVP:
# Create IVP
ivp = d3.IVP([u, b, p, ...], namespace=locals())
ivp.add_equation("dt(u) + grad(p) = 0")
# ... add more equations ...
# Convert to EVP for stability analysis
evp = ivp.build_EVP(eigenvalue=λ,
backgrounds=[U, B, P], # Background state
perturbations=[u, b, p]) # Perturbation fields
This converts dt(X) → λ*X and linearizes the RHS around the background state.
Linear Boundary Value Problems (LBVP)
Steady-state linear problems.
Problem Structure
LBVPs support equations of the form:
Where:
- L: Linear operator
- X: Problem variables
- F: Inhomogeneous term (independent of X)
Creating an LBVP
import numpy as np
import dedalus.public as d3
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.float64)
xbasis = d3.RealFourier(coords['x'], size=128, bounds=(0, 4))
zbasis = d3.ChebyshevT(coords['z'], size=64, bounds=(0, 1))
# Define fields
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
p = dist.Field(name='p', bases=(xbasis, zbasis))
# Tau terms
tau_p = dist.Field(name='tau_p')
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
# Create LBVP
problem = d3.LBVP(
[u, p, tau_p, tau_u1, tau_u2],
namespace=locals()
)
Adding Equations
# Parameters
nu = 1e-3
U0 = 1.0 # Forcing
# Substitutions
ex, ez = coords.unit_vector_fields(dist)
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
grad_u = d3.grad(u) + ez*lift(tau_u1)
# Stokes flow equations
problem.add_equation("trace(grad_u) + tau_p = 0")
problem.add_equation("-nu*div(grad_u) + grad(p) + lift(tau_u2) = 0")
# Boundary conditions
problem.add_equation("u(z=0) = 0")
problem.add_equation("u(z=1) = U0*ex") # Driven lid
problem.add_equation("integ(p) = 0")
Equation Constraints
LBVP Requirements:
- LHS must be strictly linear in problem variables
- RHS must be independent of problem variables
- Cannot have affine terms on LHS (use RHS instead)
Nonlinear Boundary Value Problems (NLBVP)
Steady-state nonlinear problems solved via Newton iteration.
Problem Structure
NLBVPs support equations of the form:
Solved via Newton-Kantorovich method:
F(X) = G(X) - H(X) = 0
dF(X[n])·dX = -F(X[n])
X[n+1] = X[n] + dX
Creating an NLBVP
import numpy as np
import dedalus.public as d3
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.float64)
xbasis = d3.RealFourier(coords['x'], size=128, bounds=(0, 4))
zbasis = d3.ChebyshevT(coords['z'], size=64, bounds=(0, 1))
# Define fields
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
T = dist.Field(name='T', bases=(xbasis, zbasis))
p = dist.Field(name='p', bases=(xbasis, zbasis))
# Tau terms
tau_p = dist.Field(name='tau_p')
tau_T1 = dist.Field(name='tau_T1', bases=xbasis)
tau_T2 = dist.Field(name='tau_T2', bases=xbasis)
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
# Create NLBVP
problem = d3.NLBVP(
[u, T, p, tau_p, tau_T1, tau_T2, tau_u1, tau_u2],
namespace=locals()
)
Adding Equations
# Parameters
Ra = 1e6
Pr = 1
kappa = (Ra * Pr)**(-1/2)
nu = (Ra / Pr)**(-1/2)
# Substitutions
ex, ez = coords.unit_vector_fields(dist)
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
grad_u = d3.grad(u) + ez*lift(tau_u1)
grad_T = d3.grad(T) + ez*lift(tau_T1)
# Nonlinear steady-state equations
problem.add_equation("trace(grad_u) + tau_p = 0")
problem.add_equation("-kappa*div(grad_T) + lift(tau_T2) = -u@grad(T)") # Nonlinear
problem.add_equation("-nu*div(grad_u) + grad(p) - T*ez + lift(tau_u2) = -u@grad(u)") # Nonlinear
# Boundary conditions
problem.add_equation("T(z=0) = 1")
problem.add_equation("u(z=0) = 0")
problem.add_equation("T(z=1) = 0")
problem.add_equation("u(z=1) = 0")
problem.add_equation("integ(p) = 0")
Equation Constraints
NLBVP Requirements:
No linearity restrictions - equations can be arbitrarily nonlinear. The Frechet differential is computed symbolically.
Boundary Conditions
All problem types support boundary conditions via interpolation.
Dirichlet Conditions
# Fixed value at boundary
problem.add_equation("u(z=0) = 0") # No-slip at bottom
problem.add_equation("T(z=1) = 273") # Fixed temperature at top
Neumann Conditions
# Fixed derivative at boundary
problem.add_equation("dz(T)(z=0) = 0") # Insulating bottom
Robin Conditions
# Mixed condition
problem.add_equation("dz(T)(z=0) + alpha*T(z=0) = 0") # Newton cooling
Stress Conditions
# Stress-free boundary
problem.add_equation("dz(u)(z=1) + ez*tau_u1 = 0")
Complete Examples
IVP: 2D Rayleigh-Bénard Convection
import numpy as np
import dedalus.public as d3
# Parameters
Lx, Lz = 4, 1
Nx, Nz = 256, 64
Rayleigh = 2e6
Prandtl = 1
# Setup
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.float64)
xbasis = d3.RealFourier(coords['x'], size=Nx, bounds=(0, Lx), dealias=3/2)
zbasis = d3.ChebyshevT(coords['z'], size=Nz, bounds=(0, Lz), dealias=3/2)
# Fields
p = dist.Field(name='p', bases=(xbasis, zbasis))
b = dist.Field(name='b', bases=(xbasis, zbasis))
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
tau_p = dist.Field(name='tau_p')
tau_b1 = dist.Field(name='tau_b1', bases=xbasis)
tau_b2 = dist.Field(name='tau_b2', bases=xbasis)
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
# Substitutions
kappa = (Rayleigh * Prandtl)**(-1/2)
nu = (Rayleigh / Prandtl)**(-1/2)
x, z = dist.local_grids(xbasis, zbasis)
ex, ez = coords.unit_vector_fields(dist)
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
grad_u = d3.grad(u) + ez*lift(tau_u1)
grad_b = d3.grad(b) + ez*lift(tau_b1)
# Problem
problem = d3.IVP([p, b, u, tau_p, tau_b1, tau_b2, tau_u1, tau_u2],
namespace=locals())
problem.add_equation("trace(grad_u) + tau_p = 0")
problem.add_equation("dt(b) - kappa*div(grad_b) + lift(tau_b2) = -u@grad(b)")
problem.add_equation("dt(u) - nu*div(grad_u) + grad(p) - b*ez + lift(tau_u2) = -u@grad(u)")
problem.add_equation("b(z=0) = Lz")
problem.add_equation("u(z=0) = 0")
problem.add_equation("b(z=Lz) = 0")
problem.add_equation("u(z=Lz) = 0")
problem.add_equation("integ(p) = 0")
# Initial conditions
b.fill_random('g', seed=42, distribution='normal', scale=1e-3)
b['g'] *= z * (Lz - z)
b['g'] += Lz - z
# Solver
solver = problem.build_solver(d3.RK222)
solver.stop_sim_time = 50
EVP: Stability Analysis
import numpy as np
import dedalus.public as d3
# Setup for EVP
coords = d3.CartesianCoordinates('x', 'z')
dist = d3.Distributor(coords, dtype=np.complex128)
kx = 3.117 # Horizontal wavenumber
xbasis = d3.ComplexFourier(coords['x'], size=1, bounds=(0, 2*np.pi/kx))
zbasis = d3.ChebyshevT(coords['z'], size=64, bounds=(0, 1))
# Perturbation fields
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
b = dist.Field(name='b', bases=(xbasis, zbasis))
p = dist.Field(name='p', bases=(xbasis, zbasis))
tau_u1 = dist.VectorField(coords, name='tau_u1', bases=xbasis)
tau_u2 = dist.VectorField(coords, name='tau_u2', bases=xbasis)
tau_b1 = dist.Field(name='tau_b1', bases=xbasis)
tau_b2 = dist.Field(name='tau_b2', bases=xbasis)
tau_p = dist.Field(name='tau_p')
# Eigenvalue
eigenvalue = dist.Field(name='λ')
# Problem
problem = d3.EVP([u, b, p, tau_u1, tau_u2, tau_b1, tau_b2, tau_p],
eigenvalue, namespace=locals())
# EVP equations...
# (same as IVP but with dt(...) → λ*...)
# Solver
solver = problem.build_solver()
solver.solve_dense(solver.subproblems[0])
print(f"Eigenvalues: {solver.eigenvalues}")
Best Practices
Problem Selection:
- Use IVP for time evolution problems
- Use EVP for linear stability analysis
- Use LBVP for linear steady-state problems
- Use NLBVP for nonlinear steady-state solutions
Namespace Management:
Always pass namespace=locals() when creating problems to ensure string equations can access all variables and operators in scope.
First-Order Reduction:
For problems with Chebyshev/Jacobi bases and boundary conditions, use first-order reduction by defining auxiliary gradient variables with lifted tau terms.
See Also