Tutorial: Analysis and Output
This tutorial covers saving simulation data and computing diagnostics in Tarang.jl.
File Output
Tarang.jl supports multiple output formats for saving simulation data.
NetCDF Output
NetCDF is the recommended format for parallel simulations.
using Tarang
# Create output handler
handler = add_file_handler(
"simulation_output", # Base filename
dist, # Distributor
Dict("u" => u, "T" => T); # Fields to track
parallel="gather", # Parallel I/O mode
max_writes=100 # Files per handler
)
# Add field outputs
add_task(handler, u; name="velocity")
add_task(handler, T; name="temperature")
# Process output at each timestep
process!(handler;
iteration=solver.iteration,
wall_time=solver.wall_time,
sim_time=solver.sim_time,
timestep=solver.dt
)Output Modes
# Gather mode: All data collected to root process
handler = add_file_handler(path, dist, fields; parallel="gather")
# Virtual mode: Each process writes its own file
handler = add_file_handler(path, dist, fields; parallel="virtual")Output Frequency
# Write every N iterations
if solver.iteration % output_cadence == 0
process!(handler; iteration=solver.iteration, ...)
end
# Write at specific simulation times
if solver.sim_time >= next_output_time
process!(handler; sim_time=solver.sim_time, ...)
next_output_time += output_interval
endAnalysis Tasks
Computing Means
# Horizontal average
add_mean_task!(handler, T; dims=1, name="T_mean_x")
# Vertical average
add_mean_task!(handler, T; dims=2, name="T_mean_z")
# Full spatial average (scalar)
add_mean_task!(handler, T; dims=(1,2), name="T_avg")Extracting Slices
# Slice at specific index
add_slice_task!(handler, T; dim=1, idx=64, name="T_slice_x")
# Multiple slices
add_slice_task!(handler, T; dim=2, idx=1, name="T_bottom")
add_slice_task!(handler, T; dim=2, idx=Nz, name="T_top")Custom Analysis
# Define custom postprocessing function
function kinetic_energy(u_data)
return 0.5 * sum(u_data.^2)
end
# Add custom task
add_task(handler, u;
name="kinetic_energy",
postprocess=kinetic_energy
)Global Diagnostics
CFL Condition
using Tarang
# Create CFL calculator
cfl = CFL(problem; safety=0.5, max_change=1.5, min_change=0.5)
# Register velocity field
add_velocity!(cfl, u)
# Compute adaptive timestep
dt = compute_timestep(cfl)Flow Statistics
using Tarang
# Create global reducer for MPI operations
reducer = GlobalArrayReducer(dist.comm)
# Compute global max
u_max = reduce_scalar(reducer, maximum(abs.(get_grid_data(u))), MPI.MAX)
# Compute global mean
u_mean = reduce_scalar(reducer, mean(get_grid_data(u)), MPI.SUM) / dist.size
# Compute global energy
function global_energy(u, reducer)
local_energy = sum(get_grid_data(u).^2) / 2
return reduce_scalar(reducer, local_energy, MPI.SUM)
endReynolds Number
function compute_reynolds_number(u, nu, L)
ensure_layout!(u, :g)
# RMS velocity
u_rms = sqrt(mean(get_grid_data(u).^2))
# Reynolds number
Re = u_rms * L / nu
return Re
endNusselt Number
For thermal convection:
function compute_nusselt(T, uz, L, kappa)
ensure_layout!(T, :g)
ensure_layout!(uz, :g)
# Convective heat flux
flux_conv = mean(get_grid_data(T) .* get_grid_data(uz))
# Conductive flux (from temperature gradient)
dT = 1.0 # Temperature difference
# Nusselt = total flux / conductive flux
Nu = 1.0 + flux_conv * L / (kappa * dT)
return Nu
endEnergy Spectra
1D Spectrum
function compute_1d_spectrum(u, axis)
ensure_layout!(u, :c) # Spectral space
data = get_coeff_data(u)
N = size(data, axis)
# Sum over other dimensions
spectrum = zeros(N÷2)
for k in 1:N÷2
spectrum[k] = sum(abs2.(selectdim(data, axis, k)))
end
return spectrum
endShell-Averaged Spectrum
function compute_shell_spectrum(u, kmax)
ensure_layout!(u, :c)
# Initialize spectrum bins
E_k = zeros(kmax)
counts = zeros(Int, kmax)
# Get wavenumbers
kx = get_wavenumbers(u.bases[1])
ky = get_wavenumbers(u.bases[2])
kz = get_wavenumbers(u.bases[3])
# Bin energy by wavenumber magnitude
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(u)[i,j,k])
counts[k_bin] += 1
end
end
return E_k
endTime Series
Storing Scalar Time Series
# Initialize storage
times = Float64[]
energies = Float64[]
nusselts = Float64[]
# During simulation
while solver.sim_time < t_end
step!(solver, dt)
# Record diagnostics
push!(times, solver.sim_time)
push!(energies, compute_kinetic_energy(u))
push!(nusselts, compute_nusselt(T, uz, L, kappa))
end
# Save to file
if rank == 0
using JLD2
@save "diagnostics.jld2" times energies nusselts
endVisualization Integration
Plots.jl
using Plots
function plot_field(field, title="")
ensure_layout!(field, :g)
data = get_grid_data(field)
heatmap(data',
xlabel="x", ylabel="z",
title=title,
colorbar=true
)
end
# Save figure
plot_field(T, "Temperature")
savefig("temperature.png")Makie.jl
using CairoMakie
function plot_field_makie(field)
ensure_layout!(field, :g)
data = get_grid_data(field)
fig = Figure()
ax = Axis(fig[1,1], xlabel="x", ylabel="z")
hm = heatmap!(ax, data')
Colorbar(fig[1,2], hm)
return fig
endCheckpointing
Saving State
function save_checkpoint(solver, filename)
state = Dict(
"sim_time" => solver.sim_time,
"iteration" => solver.iteration,
"dt" => solver.dt,
"fields" => Dict()
)
for (name, field) in solver.problem.fields
ensure_layout!(field, :c)
state["fields"][name] = copy(get_coeff_data(field))
end
if MPI.Comm_rank(MPI.COMM_WORLD) == 0
using JLD2
@save filename state
end
endLoading State
function load_checkpoint!(solver, filename)
using JLD2
@load filename state
solver.sim_time = state["sim_time"]
solver.iteration = state["iteration"]
solver.dt = state["dt"]
for (name, data) in state["fields"]
field = solver.problem.fields[name]
get_coeff_data(field) .= data
field.current_layout = :c
end
endComplete Example
using Tarang
using MPI
using Printf
MPI.Init()
rank = MPI.Comm_rank(MPI.COMM_WORLD)
# Setup (abbreviated)
# ... create domain, fields, problem ...
# Output handler
handler = add_file_handler("output", dist, Dict("T" => T);
parallel="gather", max_writes=100)
add_task(handler, T; name="temperature")
add_mean_task!(handler, T; dims=1, name="T_mean")
# Create solver
solver = InitialValueSolver(problem, RK222(), dt=1e-3)
# CFL
cfl = CFL(problem, safety=0.5)
add_velocity!(cfl, u)
# Diagnostics storage
times, energies = Float64[], Float64[]
# Main loop
output_interval = 0.1
next_output = output_interval
while solver.sim_time < 10.0
dt = compute_timestep(cfl)
step!(solver, dt)
# Store diagnostics
push!(times, solver.sim_time)
push!(energies, compute_kinetic_energy(u))
# Periodic output
if solver.sim_time >= next_output
process!(handler;
iteration=solver.iteration,
sim_time=solver.sim_time,
wall_time=0.0,
timestep=dt
)
if rank == 0
@printf("t = %.3f, E = %.6e\n", solver.sim_time, energies[end])
end
next_output += output_interval
end
end
MPI.Finalize()See Also
- Analysis API: Full API reference
- I/O API: NetCDF output documentation
- Parallelism: Parallel I/O configuration