Solvers API
Solvers integrate PDEs in time or solve for steady states. Tarang.jl provides specialized solvers for different problem types.
Solver Types
InitialValueSolver
Time-stepping solver for Initial Value Problems (IVP).
Constructor:
InitialValueSolver(
problem::IVP,
timestepper::TimeStepper;
dt::Float64=0.001,
device::String="cpu"
)Arguments:
problem: IVP problem definitiontimestepper: Time integration scheme (RK222, CNAB2, etc.)dt: Initial timestep
Examples:
# IMEX Runge-Kutta
solver = InitialValueSolver(problem, RK222(), dt=0.001)
# IMEX timestepper
solver = InitialValueSolver(problem, CNAB2(), dt=0.01)
# Higher-order timestepper
solver = InitialValueSolver(problem, RK443(), dt=0.001)Properties:
solver.problem # IVP problem
solver.timestepper # Time integration scheme
solver.dt # Current timestep
solver.sim_time # Current simulation time
solver.iteration # Current iteration number
solver.wall_time # Elapsed wall clock time
solver.state # Current state vectorMethods:
run!
The recommended way to run a simulation. Handles the loop, progress reporting, and callbacks:
run!(solver;
stop_time=10.0,
log_interval=100,
callbacks=[
(10, s -> @info "Energy: $(compute_energy(s))"),
(0.5, s -> save_checkpoint(s)) # every 0.5 time units
])Arguments:
stop_time: Stop whensim_time >= stop_timestop_iteration: Stop after N iterationsstop_wall_time: Stop after N seconds of wall timecallbacks: Vector of(interval, function)pairslog_interval: Print progress every N iterations (0 to disable)
Callback intervals can be Int (every N iterations) or Float64 (every T time units).
step!
Advance solution by one timestep (for custom loops):
step!(solver) # Use solver.dt
step!(solver, dt) # Use specified dtproceed
Check if simulation should continue:
solver.stop_sim_time = 10.0
while proceed(solver)
step!(solver)
enddiagnose
Print a formatted summary of solver state:
diagnose(solver)Shows: timestepper, fields, DOF, architecture, transforms, compiled RHS status, dealiasing, BCs, performance stats.
Compiled RHS
During solver construction, Tarang.jl attempts to compile the RHS expression tree into a sequence of pre-allocated typed instructions. This eliminates runtime type dispatch and per-timestep allocation. If compilation fails (unsupported expression types), it falls back to the interpreted evaluation path silently.
solver = InitialValueSolver(problem, RK222(); dt=1e-3)
# Check compilation status:
diagnose(solver) # Shows "RHS: COMPILED (N instructions, M workspace fields)"BoundaryValueSolver
Solver for Linear Boundary Value Problems (LBVP).
Constructor:
BoundaryValueSolver(
problem::LBVP;
solver_type::String="direct",
tolerance::Float64=1e-10
)Arguments:
problem: LBVP problem definitionsolver_type: Solution method ("direct", "iterative", "multigrid")tolerance: Convergence tolerance for iterative solvers
Examples:
# Direct solver (for small problems)
solver = BoundaryValueSolver(problem, solver_type="direct")
# Iterative solver (for large problems)
solver = BoundaryValueSolver(problem, solver_type="iterative", tolerance=1e-8)Methods:
solve!
Solve the boundary value problem.
solve!(solver)Returns: Solution fields are updated in place
Example:
# Poisson equation
problem = LBVP([phi])
add_equation!(problem, "Δ(phi) = rho")
add_equation!(problem, "phi(z=0) = 0")
add_equation!(problem, "phi(z=1) = 0")
solver = BoundaryValueSolver(problem)
solve!(solver)
# Solution is now in phi field
phi_grid = get_grid_data(phi)NonlinearBoundaryValueSolver
Newton-Raphson solver for Nonlinear Boundary Value Problems (NLBVP).
Constructor:
NonlinearBoundaryValueSolver(
problem::NLBVP;
tolerance::Float64=1e-8,
max_iterations::Int=100,
damping::Float64=1.0
)Arguments:
problem: NLBVP problem definitiontolerance: Convergence tolerancemax_iterations: Maximum Newton iterationsdamping: Damping factor for Newton steps (0 < damping ≤ 1)
Examples:
# Standard Newton solver
solver = NonlinearBoundaryValueSolver(problem, tolerance=1e-8)
# With damping for stability
solver = NonlinearBoundaryValueSolver(problem, damping=0.5, max_iterations=50)Methods:
solve!
Solve using Newton-Raphson iteration.
success = solve!(solver)Returns: true if converged, false otherwise
Example:
# Steady Navier-Stokes
problem = NLBVP([u, v, p])
add_equation!(problem, "∂x(p) - nu*Δ(u) = -u*∂x(u) - v*∂z(u)")
add_equation!(problem, "∂z(p) - nu*Δ(v) = -u*∂x(v) - v*∂z(v)")
add_equation!(problem, "∂x(u) + ∂z(v) = 0")
solver = NonlinearBoundaryValueSolver(problem)
if solve!(solver)
println("Converged in $(solver.iterations) iterations")
else
println("Failed to converge")
endProperties:
solver.iterations # Number of iterations performed
solver.residual # Final residual norm
solver.converged # Convergence statusEigenvalueSolver
Solver for Eigenvalue Problems (EVP).
Constructor:
EigenvalueSolver(
problem::EVP;
nev::Int=10,
target::Complex{Float64}=0.0+0.0im,
which::String="LM"
)Arguments:
problem: EVP problem definitionnev: Number of eigenvalues to computetarget: Target eigenvalue for shift-invertwhich: Which eigenvalues to find ("LM", "SM", "LR", "SR", "LI", "SI")
Which options:
- "LM": Largest magnitude
- "SM": Smallest magnitude
- "LR": Largest real part
- "SR": Smallest real part
- "LI": Largest imaginary part
- "SI": Smallest imaginary part
Examples:
# Find 10 eigenvalues with largest growth rate
solver = EigenvalueSolver(problem, nev=10, which="LR")
# Find eigenvalues near target
solver = EigenvalueSolver(problem, nev=5, target=0.1+1.5im)
# Most unstable modes
solver = EigenvalueSolver(problem, nev=20, which="LR")Methods:
solve!
Compute eigenvalues and eigenvectors.
eigenvalues, eigenvectors = solve!(solver)Returns:
eigenvalues: Array of complex eigenvalueseigenvectors: Array of eigenvector fields
Example:
# Rayleigh-Bénard stability
problem = EVP([u, v, p, T], eigenvalue=:sigma)
# ... add equations and BCs ...
solver = EigenvalueSolver(problem, nev=10, which="LR")
eigenvalues, eigenvectors = solve!(solver)
# Find critical mode
max_idx = argmax(real.(eigenvalues))
growth_rate = real(eigenvalues[max_idx])
frequency = imag(eigenvalues[max_idx])
println("Maximum growth rate: $growth_rate")
println("Frequency: $frequency")
# Extract critical mode
critical_mode = eigenvectors[max_idx]Solver State Management
Saving and Loading State
# Save solver state
save_state(solver, "checkpoint.h5")
# Load solver state
load_state!(solver, "checkpoint.h5")
# Resume simulation
while proceed(solver)
step!(solver)
endState Vector Access
# Get state vector
state = get_state(solver)
# Set state vector
set_state!(solver, new_state)
# Useful for custom initialization or analysisTime Integration
Time Stepping Loop
Basic pattern for time integration:
# Create solver
solver = InitialValueSolver(problem, RK222(), dt=0.001)
# Set stop conditions
t_end = 10.0
# Time loop
while solver.sim_time < t_end
step!(solver)
# Optional: output and diagnostics
if solver.iteration % 100 == 0
println("t = $(solver.sim_time), iteration = $(solver.iteration)")
end
endAdaptive Time Stepping
With CFL condition:
# Create CFL calculator
cfl = CFL(problem, safety=0.5, max_change=1.5, min_change=0.5)
add_velocity!(cfl, u)
# Adaptive loop
while solver.sim_time < t_end
# Compute adaptive timestep
dt = compute_timestep(cfl)
# Take step
step!(solver, dt)
endOutput During Integration
# Setup output handler
output = add_netcdf_handler(
solver,
"output",
fields=[u, p, T],
write_interval=0.1
)
# Time loop with automatic output
while solver.sim_time < t_end
dt = compute_timestep(cfl)
step!(solver, dt)
# Output written automatically when t > next_write_time
endSolver Options
Convergence Criteria
For iterative and nonlinear solvers:
solver.tolerance = 1e-10 # Residual tolerance
solver.max_iterations = 1000 # Maximum iterations
solver.relative_tolerance = 1e-8 # Relative change tolerancePerformance Options
# Parallel options
solver.use_threading = true
solver.num_threads = 4
# Memory options
solver.preallocate_work = true
solver.work_array_count = 5Advanced Solver Features
Preconditioners
For iterative solvers:
# Set preconditioner type
solver.preconditioner = "jacobi" # or "ilu", "multigrid"
# Custom preconditioner
function my_preconditioner(A, b)
# Custom preconditioning operation
return P \ b
end
solver.preconditioner = my_preconditionerMatrix-Free Methods
For large problems:
# Use matrix-free Krylov methods
solver.matrix_free = true
solver.krylov_method = "gmres" # or "bicgstab", "cg"Continuation Methods
For nonlinear problems:
# Parameter continuation
Ra_values = [1e4, 5e4, 1e5, 5e5, 1e6]
solution = nothing
for Ra in Ra_values
problem.namespace["Ra"] = Ra
if solution !== nothing
# Use previous solution as initial guess
set_state!(solver, solution)
end
solve!(solver)
solution = get_state(solver)
println("Solved for Ra = $Ra")
endSolver Diagnostics
Convergence Monitoring
# Enable convergence monitoring
solver.verbose = true
solver.print_interval = 10
# Custom convergence callback
function convergence_callback(solver, iteration, residual)
if iteration % 10 == 0
println("Iteration $iteration: residual = $residual")
end
end
solver.convergence_callback = convergence_callbackPerformance Profiling
# Enable profiling
solver.profile = true
# After solving
print_performance_summary(solver)
# Outputs:
# - Time per iteration
# - Time in different solver phases
# - Memory usage
# - Communication time (MPI)Error Handling
Solver Failures
try
solve!(solver)
catch e
if isa(e, ConvergenceError)
println("Failed to converge: $(e.message)")
println("Residual: $(e.residual)")
elseif isa(e, LinearAlgebraError)
println("Linear algebra error: $(e.message)")
else
rethrow(e)
end
endRecovery Strategies
# For nonlinear solvers
if !solver.converged
# Try with damping
solver.damping = 0.5
solve!(solver)
if !solver.converged
# Try with better initial guess
# ... provide better initialization
solve!(solver)
end
endComplete Example
Time-Dependent Simulation
using Tarang, MPI
MPI.Init()
# Setup problem (2D Rayleigh-Bénard)
coords = CartesianCoordinates("x", "z")
dist = Distributor(coords, mesh=(2, 2))
x_basis = RealFourier(coords["x"], size=256, bounds=(0.0, 4.0))
z_basis = ChebyshevT(coords["z"], size=64, bounds=(0.0, 1.0))
domain = Domain(dist, (x_basis, z_basis))
u = VectorField(dist, coords, "u", (x_basis, z_basis))
p = ScalarField(dist, "p", (x_basis, z_basis))
T = ScalarField(dist, "T", (x_basis, z_basis))
# Define problem
problem = IVP([u.components[1], u.components[2], p, T])
problem.namespace["Ra"] = 1e6
problem.namespace["Pr"] = 1.0
add_equation!(problem, "∂t(u) + ∂x(p) - Pr*Δ(u) = -u*∂x(u) - w*∂z(u)")
add_equation!(problem, "∂t(w) + ∂z(p) - Pr*Δ(w) - Ra*Pr*T = -u*∂x(w) - w*∂z(w)")
add_equation!(problem, "∂x(u) + ∂z(w) = 0")
add_equation!(problem, "∂t(T) - Δ(T) = -u*∂x(T) - w*∂z(T)")
# Boundary conditions
add_equation!(problem, "u(z=0) = 0")
add_equation!(problem, "w(z=0) = 0")
add_equation!(problem, "T(z=0) = 1")
add_equation!(problem, "u(z=1) = 0")
add_equation!(problem, "w(z=1) = 0")
add_equation!(problem, "T(z=1) = 0")
# Initialize fields
# ... set initial conditions ...
# Create solver
solver = InitialValueSolver(problem, RK222(), dt=1e-4)
# CFL condition
cfl = CFL(problem, safety=0.5)
add_velocity!(cfl, u)
# Output
output = add_netcdf_handler(solver, "output", fields=[u, p, T], write_interval=0.1)
# Time integration
t_end = 10.0
while solver.sim_time < t_end
dt = compute_timestep(cfl)
step!(solver, dt)
if solver.iteration % 100 == 0 && MPI.Comm_rank(MPI.COMM_WORLD) == 0
println("t = $(solver.sim_time), dt = $dt")
end
end
MPI.Finalize()See Also
- Problems: Problem definition
- Timesteppers: Time integration schemes
- Analysis: CFL conditions and diagnostics
- Tutorial: IVP: Complete example