Overview
Fields are the fundamental data containers in Dedalus, representing scalar, vector, and tensor quantities on domains. Operators act on fields to build equation expressions.
Field Types
All field classes are defined in dedalus/core/field.py.
Creating Fields
Field (Generic)
General field with arbitrary tensor signature.
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))
# Scalar field (no tensorsig)
f = dist.Field(name='f', bases=(xbasis, zbasis))
# Field without spatial bases (constant)
const = dist.Field(name='const', dtype=np.float64)
ScalarField
Scalar field with no tensor indices.
# Equivalent to Field with no tensorsig
s = dist.ScalarField(name='s', bases=(xbasis, zbasis))
VectorField
Vector field with one tensor index.
# Vector in Cartesian coordinates
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
print(u.tensorsig) # (coords,)
print(u['g'].shape) # (2, Nx, Nz) for 2D coordinates
TensorField
Tensor field with two or more indices.
# Rank-2 tensor (e.g., stress tensor)
T = dist.TensorField((coords, coords), name='T', bases=(xbasis, zbasis))
print(T.tensorsig) # (coords, coords)
print(T['g'].shape) # (2, 2, Nx, Nz) for 2D coordinates
Field Properties
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))
u = dist.VectorField(coords, name='u', bases=(xbasis, zbasis))
# Basic properties
print(u.name) # 'u'
print(u.domain) # Domain object
print(u.dist) # Distributor object
print(u.dtype) # np.float64
print(u.tensorsig) # (coords,)
# Data access
print(u.data) # Current layout data
print(u.layout) # Current layout object
print(u['g']) # Grid space data
print(u['c']) # Coefficient space data
# Shape information
print(u.data.shape) # Shape in current layout
print(u.global_shape()) # Global shape
print(u.local_shape()) # Local shape on this process
Accessing Field Data
Layout System
Fields can be in grid space (‘g’) or coefficient space (‘c’).
# Access grid space data
f['g'] = np.sin(2*np.pi*x/Lx) * np.cos(np.pi*z/Lz)
# Access coefficient space data
f['c'][0, 0] = 1.0
# Check current layout
print(f.layout) # Layout object
# Change layout explicitly
f.change_layout('c') # Transform to coefficient space
f.change_layout('g') # Transform to grid space
Indexing
# Scalar field
f['g'][i, j] = value # Grid space at point (i, j)
f['c'][kx, kz] = value # Coefficient at mode (kx, kz)
# Vector field
u['g'][0, i, j] = value # x-component at (i, j)
u['g'][1, i, j] = value # z-component at (i, j)
# Tensor field
T['g'][0, 1, i, j] = value # T_xz component at (i, j)
Slicing
# Get x-component of vector field
ux = u['g'][0]
# Get slice at z=constant
f_slice = f['g'][:, k]
# Set entire field
f['g'][:] = 0 # Zero out
f['g'] = other_field['g'] # Copy
Setting Field Values
From Arrays
import numpy as np
# Set directly
f['g'] = np.sin(2*np.pi*x/Lx)
# Set components
u['g'][0] = np.cos(2*np.pi*x/Lx)
u['g'][1] = 0
Using fill_random
# Random noise
f.fill_random('g', seed=42, distribution='normal', scale=1e-3)
# Random with specific distribution
f.fill_random('g', seed=42, distribution='uniform', low=-1, high=1)
Preset Scales
Control grid resolution for evaluation.
# Set scales for dealiasing
f.preset_scales(3/2)
# Access data at specified scale
print(f['g'].shape) # Shape with 3/2 dealiasing
Arithmetic Operations
Fields support standard arithmetic operations.
# Scalar operations
f2 = 2 * f + 1
f3 = f * g # Nonlinear term
f4 = f / g
f5 = f ** 2
# Vector operations
w = u + v # Vector addition
w = 2 * u # Scalar multiplication
# Dot product (returns scalar)
kinetic = u @ u # u·u
# Vector-vector multiplication (outer product, returns tensor)
T = u * v # Returns tensor with u_i * v_j
Differential Operators
All operators are defined in dedalus/core/operators.py.
Gradient
Compute the gradient of a scalar field.
import dedalus.public as d3
# Gradient of scalar field (returns vector)
grad_f = d3.grad(f)
# Or use string parsing
problem.add_equation("grad(f) = 0")
Output:
- Input: Scalar field
- Output: Vector field
- Components: ∇f = (∂f/∂x, ∂f/∂y, ∂f/∂z)
Divergence
Compute the divergence of a vector field.
# Divergence of vector field (returns scalar)
div_u = d3.div(u)
# In equations
problem.add_equation("div(u) = 0") # Incompressibility
Output:
- Input: Vector field
- Output: Scalar field
- Value: ∇·u = ∂u_x/∂x + ∂u_y/∂y + ∂u_z/∂z
Curl
Compute the curl of a vector field.
# Curl of vector field (returns vector in 3D, scalar in 2D)
vorticity = d3.curl(u)
# In equations
problem.add_equation("w - curl(u) = 0")
Output:
- Input: Vector field
- Output: Vector field (3D) or Scalar field (2D)
- 2D: ∇×u = ∂u_y/∂x - ∂u_x/∂y
- 3D: (∇×u)_i = ε_ijk ∂u_k/∂x_j
Laplacian
Compute the Laplacian of a field.
# Laplacian (works on scalars, vectors, tensors)
lap_f = d3.lap(f)
lap_u = d3.lap(u)
# In equations
problem.add_equation("dt(f) - kappa*lap(f) = 0") # Diffusion
Output:
- ∇²f = ∂²f/∂x² + ∂²f/∂y² + ∂²f/∂z²
- Acts component-wise on vectors and tensors
Trace
Compute the trace of a tensor.
# Trace of rank-2 tensor (returns scalar)
trace_T = d3.trace(T)
# Trace of gradient (equivalent to divergence)
problem.add_equation("trace(grad(u)) = 0") # div(u) = 0
Skew-Symmetric Part
Compute the skew-symmetric part of a tensor.
# Skew-symmetric part
skew_grad_u = d3.skew(d3.grad(u))
# Vorticity from skew(grad(u))
omega = -d3.div(d3.skew(d3.grad(u))) # Equivalent to curl in 2D
Transpose
Transpose tensor components.
# Transpose tensor
T_transpose = d3.TransposeComponents(T)
# Or use property
T_transpose = T.T
Advanced Operators
Interpolation
Interpolate field to a specific location.
# Interpolate to z=0
f_bottom = d3.Interpolate(f, z=0)
# Or use parentheses syntax
f_bottom = f(z=0)
# In equations (boundary conditions)
problem.add_equation("f(z=0) = 1")
Integration
Integrate field over a coordinate.
# Integrate over z
f_integrated = d3.Integrate(f, coords['z'])
# In equations (pressure gauge)
problem.add_equation("integ(p) = 0")
Averaging
Compute average over a coordinate.
# Average over x
f_avg = d3.Average(f, coords['x'])
# In string parsing
problem.add_equation("u_mean - Average(u, x) = 0")
Lifting
Lift boundary data to the domain interior.
# Lift tau term for first-order reduction
lift_basis = zbasis.derivative_basis(1)
lift = lambda A: d3.Lift(A, lift_basis, -1)
# Use in gradient
grad_u = d3.grad(u) + ez * lift(tau_u1)
Component Access
Unit Vectors
# Get coordinate unit vectors
ex, ey, ez = coords.unit_vector_fields(dist)
# Use in equations
problem.add_equation("dt(u) + grad(p) - b*ez = -u@grad(u)")
Vector Components
# Extract specific component
ux = u @ ex # x-component
uz = u @ ez # z-component
# Or use indexing
ux_data = u['g'][0] # x-component data
Radial/Angular Components
For curvilinear coordinates.
# Radial component in spherical coordinates
u_r = d3.RadialComponent(u)
# Angular component
u_angular = d3.AngularComponent(u)
# Azimuthal component
u_phi = d3.AzimuthalComponent(u)
Complete Example
import numpy as np
import dedalus.public as d3
# Setup
Lx, Lz = 4, 1
Nx, Nz = 256, 64
Rayleigh = 2e6
Prandtl = 1
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 terms (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)
# Parameters
kappa = (Rayleigh * Prandtl)**(-1/2)
nu = (Rayleigh / Prandtl)**(-1/2)
# Grids and unit vectors
x, z = dist.local_grids(xbasis, zbasis)
ex, ez = coords.unit_vector_fields(dist)
# First-order reduction
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())
# Equations using operators
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")
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)
print(f"Velocity field: {u}")
print(f"Velocity shape: {u['g'].shape}")
print(f"Gradient of u: {grad_u}")
print(f"Divergence: {d3.div(u)}")
print(f"Laplacian of b: {d3.lap(b)}")
Best Practices
Operator Composition:
Compose operators to build complex expressions:
div(grad(f)) for Laplacian
trace(grad(u)) for divergence
-div(skew(grad(u))) for vorticity
Grid vs Coefficient Space:
Nonlinear operations must be performed in grid space. Dedalus automatically transforms fields as needed, but explicit change_layout('g') can avoid repeated transforms.
First-Order Reduction:
For problems with boundary conditions, use first-order reduction by defining auxiliary gradient variables and lifting tau terms. This ensures proper boundary treatment.
See Also