Analysis
Analysis tools for computing diagnostics, statistics, and derived quantities.
CFL Condition
Compute stable timesteps based on flow velocity.
The controller wraps the solver, not the problem — it needs the solver's communicator for the global reduction that makes the step agree across ranks.
using Tarang
coords = CartesianCoordinates("x", "y")
dist = Distributor(coords; dtype=Float64)
xbasis = RealFourier(coords["x"]; size=64, bounds=(0.0, 2π))
ybasis = RealFourier(coords["y"]; size=64, bounds=(0.0, 2π))
domain = Domain(dist, (xbasis, ybasis))
mesh = Tarang.create_meshgrid(domain)
x, y = mesh["x"], mesh["y"]
u = VectorField(domain, "u")
u.components[1]["g"] = @. sin(x) * cos(y)
u.components[2]["g"] = @. -cos(x) * sin(y)
problem = IVP([u]; namespace=Dict("u" => u))
add_equation!(problem, "∂t(u) = 0")
solver = InitialValueSolver(problem, RK222(); dt=1e-3)
# Create the CFL controller
cfl = CFL(solver;
initial_dt = 1e-3, # dt used until the first computation
safety = 0.5, # safety factor (0.3-0.5 typical)
cadence = 1, # recompute every N iterations
max_change = 1.5, # largest dt increase per commit
min_change = 0.5, # largest dt decrease per commit
threshold = 0.1, # only commit changes larger than 10% (see below)
max_dt = 0.1, # hard upper bound on dt
)
# Register the fields that constrain the step
add_velocity!(cfl, u)
# Compute a timestep…
dt = compute_timestep(cfl) # 0.0491 = safety * Δx / max|u| here
# …or hand the controller to `run!` and let it do that each iteration
# run!(solver; cfl=cfl, stop_iteration=1000)Every keyword is also a mutable field, so cfl.max_dt = 0.05 between steps works. There is no min_dt: dt has no floor, and the safety, min_change and threshold knobs are what keep it from collapsing.
threshold is sticky-dt hysteresis. Changing dt invalidates the cached LHS factorization in the implicit solve, so a proposed step within threshold (relative) of the current one is discarded and the current dt reused. Default 0.1; set 0.0 to commit every change.
With just add_velocity!, the returned dt accounts for advection only. Diffusion the timestepper integrates explicitly — notably an LES eddy viscosity νₑ, whose spatial variation rules out the implicit path — carries its own limit dt ≤ 1/(2 ν_max Σᵢ Δxᵢ⁻²) that nothing enforces here. Register it:
add_diffusivity!(cfl, nu) # constant ν
add_diffusivity!(cfl, get_eddy_viscosity(les_model)) # LES νₑ (per-rank slab)Both limits then apply and the smaller step wins. See Diffusive Limit.
Global Reductions
Compute global statistics across MPI processes.
using MPI
# Create reducer
reducer = GlobalArrayReducer(MPI.COMM_WORLD)
# Maximum value
global_max = reduce_scalar(reducer, local_max, MPI.MAX)
# Sum
global_sum = reduce_scalar(reducer, local_sum, MPI.SUM)
# Mean (requires division by total elements)
global_mean = global_sum / total_elementsFlow Statistics
Kinetic Energy
function compute_kinetic_energy(u, reducer)
local_energy = 0.0
for component in u.components
Tarang.ensure_layout!(component, :g)
local_energy += sum(get_grid_data(component).^2) / 2
end
return reduce_scalar(reducer, local_energy, MPI.SUM)
endEnstrophy
function compute_enstrophy(u, reducer)
# For 2D: ω = ∂v/∂x - ∂u/∂y
# Enstrophy = ∫ ω² dV
ux, uy = u.components[1], u.components[2]
# Compute vorticity (simplified)
# ... derivative calculation ...
return reduce_scalar(reducer, local_enstrophy, MPI.SUM)
endReynolds Number
function compute_reynolds_number(u, nu, L, reducer)
Tarang.ensure_layout!(u.components[1], :g)
# RMS velocity
local_u2 = sum(get_grid_data(u.components[1]).^2)
global_u2 = reduce_scalar(reducer, local_u2, MPI.SUM)
u_rms = sqrt(global_u2 / total_points)
return u_rms * L / nu
endHeat Transfer
Nusselt Number
function compute_nusselt(T, w, L, kappa, reducer)
Tarang.ensure_layout!(T, :g)
Tarang.ensure_layout!(w, :g)
# Convective heat flux
local_flux = sum(get_grid_data(T) .* get_grid_data(w))
global_flux = reduce_scalar(reducer, local_flux, MPI.SUM)
# Normalize
flux_mean = global_flux / total_points
# Nusselt = 1 + convective/conductive
Nu = 1.0 + flux_mean * L / kappa
return Nu
endSpectral Analysis
Energy Spectrum
function compute_spectrum(field, kmax)
Tarang.ensure_layout!(field, :c)
# Initialize spectrum bins
E_k = zeros(kmax)
# Get wavenumbers
k = get_wavenumbers(field.bases[1])
# Bin energy by wavenumber
for (i, ki) in enumerate(k)
k_bin = round(Int, abs(ki))
if 1 <= k_bin <= kmax
E_k[k_bin] += abs2(get_coeff_data(field)[i])
end
end
return E_k
endShell-Averaged 3D Spectrum
function compute_3d_spectrum(u, kmax)
E_k = zeros(kmax)
for component in u.components
Tarang.ensure_layout!(component, :c)
kx = get_wavenumbers(component.bases[1])
ky = get_wavenumbers(component.bases[2])
kz = get_wavenumbers(component.bases[3])
for i in eachindex(kx), j in eachindex(ky), k in eachindex(kz)
k_mag = sqrt(kx[i]^2 + ky[j]^2 + kz[k]^2)
k_bin = round(Int, k_mag)
if 1 <= k_bin <= kmax
E_k[k_bin] += abs2(get_coeff_data(component)[i,j,k])
end
end
end
return E_k
endTime Series
Recording Diagnostics
# Storage
times = Float64[]
energies = Float64[]
nusselts = Float64[]
# During simulation
while solver.sim_time < t_end
step!(solver, dt)
# Record
push!(times, solver.sim_time)
push!(energies, compute_kinetic_energy(u, reducer))
push!(nusselts, compute_nusselt(T, w, L, kappa, reducer))
endSaving Time Series
if MPI.Comm_rank(MPI.COMM_WORLD) == 0
using JLD2
@save "diagnostics.jld2" times energies nusselts
endSpatial Averages
Horizontal Average
function horizontal_average(field)
Tarang.ensure_layout!(field, :g)
# Average over x (first axis)
mean(get_grid_data(field), dims=1)
endVolume Average
function volume_average(field, reducer)
Tarang.ensure_layout!(field, :g)
local_sum = sum(get_grid_data(field))
global_sum = reduce_scalar(reducer, local_sum, MPI.SUM)
return global_sum / total_points
endSee Also
- Output: File output
- Solvers: Time integration
- API: Analysis: Complete reference