LES Models API
The SGS models are array-level utilities: they consume grid-space velocity-gradient arrays and produce an eddy-viscosity array. They are not automatically coupled into an IVP — you evaluate the gradients, call the model, and apply the resulting stress yourself.
Usage
using Tarang
coords = CartesianCoordinates("x", "y")
dist = Distributor(coords; dtype=Float64, device=CPU())
xbasis = RealFourier(coords["x"]; size=16, bounds=(0.0, 2π))
ybasis = RealFourier(coords["y"]; size=16, bounds=(0.0, 2π))
domain = Domain(dist, (xbasis, ybasis))
u = VectorField(domain, "u")
x, y = local_grids(dist, xbasis, ybasis)
ensure_layout!(u, :g)
get_grid_data(u.components[1]) .= sin.(x) .* cos.(y')
get_grid_data(u.components[2]) .= .-cos.(x) .* sin.(y')
ensure_layout!(u, :c)
# grad(u) is a TensorField; its components are the velocity-gradient tensor in
# component-major order: (∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y) — exactly the order the models want.
G = evaluate(grad(u))
∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y = (get_grid_data(g) for g in G.components)
Δx, Δy = grid_spacing(domain)
model = SmagorinskyModel(C_s=0.17, filter_width=(Δx, Δy), field_size=(16, 16))
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y)
νₑ = get_eddy_viscosity(model) # 16×16 Matrix{Float64}
mean_eddy_viscosity(model) # 0.00352…
max_eddy_viscosity(model) # 0.00891…
# deviatoric SGS stress τᵢⱼ = -2 νₑ S̄ᵢⱼ
S11 = ∂u∂x
S12 = 0.5 .* (∂u∂y .+ ∂v∂x)
S22 = ∂v∂y
τ11, τ12, τ22 = compute_sgs_stress(model, S11, S12, S22)
mean_sgs_dissipation(model, model.strain_magnitude) # 0.00643…Usage under MPI
The models hold plain per-rank arrays, so size the model to this rank's slab, not to the global grid. Under MPI get_grid_data returns a PencilArray whose size is already the local shape; parent gives the raw local storage:
G = evaluate(grad(u))
∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y = (parent(get_grid_data(g)) for g in G.components)
Δx, Δy = grid_spacing(domain)
model = SmagorinskyModel(C_s=0.17, filter_width=(Δx, Δy),
field_size=size(∂u∂x)) # LOCAL slab: (16, 8) at np=2
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y)
mean_eddy_viscosity(model) # 0.00352… — global, Allreduced across ranks
max_eddy_viscosity(model) # 0.00891… — globalmean_eddy_viscosity, max_eddy_viscosity and mean_sgs_dissipation each perform their own reduction over MPI.COMM_WORLD, so they return domain-global values: for the 16×16 case above they give the same numbers at np = 1, 2 and 4. Being collectives, they must be called on every rank — reaching for one inside an if rank == 0 block deadlocks. Pass global_reduce=false to skip the reduction and get this rank's slab value, which is safe to call from a subset of ranks:
# Deadlocks at np > 1 — only rank 0 enters the collective.
rank == 0 && @info "mean νₑ" mean_eddy_viscosity(model)
# Correct: every rank reduces, one rank prints.
ν̄ = mean_eddy_viscosity(model)
rank == 0 && @info "mean νₑ" ν̄
# Or opt out of the reduction entirely.
rank == 0 && @info "local νₑ" mean_eddy_viscosity(model; global_reduce=false)get_eddy_viscosity(model) is, by contrast, this rank's slab — broadcast it against the other parent(...) arrays to build the stress locally.
Sizing the model to the global grid is a loud error rather than a silent wrong answer:
DimensionMismatch: Gradient array 1 has size (16, 8), expected (16, 16)Types
Abstract Types
abstract type SGSModel end
abstract type EddyViscosityModel <: SGSModel endSmagorinskyModel
Tarang.SmagorinskyModel — Type
SmagorinskyModel{T, N, A, Arch}Classic Smagorinsky (1963) subgrid-scale model.
Mathematical Formulation
The eddy viscosity is:
νₑ = (Cₛ Δ)² |S̄|where:
- Cₛ is the Smagorinsky constant (typically 0.1-0.2)
- Δ is the filter width (grid spacing)
- |S̄| = √(2 S̄ᵢⱼ S̄ᵢⱼ) is the strain rate magnitude
Fields
C_s::T: Smagorinsky constantfilter_width::NTuple{N, T}: Filter width in each direction (Δx, Δy, ...)eddy_viscosity::A: Cached eddy viscosity field (Array or CuArray)strain_magnitude::A: Cached |S̄| fieldarchitecture::Arch: CPU() or GPU() architecture
Example
# Create model for 256³ domain with Δ = 2π/256
model = SmagorinskyModel(
C_s = 0.17,
filter_width = (2π/256, 2π/256, 2π/256),
field_size = (256, 256, 256)
)
# Create GPU model
model_gpu = SmagorinskyModel(
C_s = 0.17,
filter_width = (2π/256, 2π/256, 2π/256),
field_size = (256, 256, 256),
architecture = GPU()
)
# Compute eddy viscosity from velocity gradients
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂u∂z, ∂v∂x, ∂v∂y, ∂v∂z, ∂w∂x, ∂w∂y, ∂w∂z)
# Access the result
νₑ = get_eddy_viscosity(model)Classic Smagorinsky (1963) subgrid-scale model.
Type signature:
mutable struct SmagorinskyModel{T<:AbstractFloat, N,
A<:AbstractArray{T, N},
Arch<:AbstractArchitecture} <: EddyViscosityModelThe array parameter A is the type the model's internal buffers are stored in — an Array{T,N} on CPU(), a CuArray on GPU().
Fields:
| Field | Type | Description |
|---|---|---|
C_s | T | Smagorinsky constant |
filter_width | NTuple{N, T} | Filter width (Δx, Δy, Δz) |
effective_delta | T | Effective Δ = (Δx Δy Δz)^(1/N) |
eddy_viscosity | A | νₑ field |
strain_magnitude | A | |S̄| field |
field_size | NTuple{N, Int} | Grid dimensions |
architecture | Arch | CPU() or GPU() |
Constructor:
SmagorinskyModel(;
C_s = 0.17,
filter_width::NTuple{N, Real},
field_size::NTuple{N, Int},
dtype = Float64,
architecture = CPU()
)AMDModel
Tarang.AMDModel — Type
AMDModel{T, N, A, Arch}Anisotropic Minimum Dissipation model (Rozema et al., 2015).
Mathematical Formulation
The eddy viscosity is:
νₑ = max(0, νₑ†)where the predictor is:
νₑ† = -C (Δₖ² ∂uᵢ/∂xₖ ∂uⱼ/∂xₖ Sᵢⱼ) / (∂uₘ/∂xₙ ∂uₘ/∂xₙ)Key features:
- Uses anisotropic filter widths Δₖ in each direction
- Automatically switches off in laminar/transitional regions
- Provides minimum dissipation required for subgrid energy transfer
- No explicit filtering or test-filtering required
Fields
C::T: Poincaré constant (model constant)filter_width::NTuple{N, T}: Anisotropic filter widths (Δx, Δy, Δz)eddy_viscosity::A: Cached eddy viscosity field (Array or CuArray)eddy_diffusivity::A: Cached eddy diffusivity (for scalars)architecture::Arch: CPU() or GPU() architecture
Model Constant Recommendations
| Discretization | C |
|---|---|
| Spectral methods | 1/12 ≈ 0.0833 |
| 4th-order finite difference | 0.212 |
| 2nd-order finite difference | 0.3 |
Example
# Create AMD model for anisotropic grid
model = AMDModel(
C = 1/12, # Spectral method
filter_width = (2π/256, 2π/256, 2π/64), # Anisotropic
field_size = (256, 256, 64)
)
# Create GPU AMD model
model_gpu = AMDModel(
C = 1/12,
filter_width = (2π/256, 2π/256, 2π/64),
field_size = (256, 256, 64),
architecture = GPU()
)
# Compute eddy viscosity
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂u∂z, ∂v∂x, ∂v∂y, ∂v∂z, ∂w∂x, ∂w∂y, ∂w∂z)References
Rozema, W., Bae, H.J., Moin, P., Verstappen, R. (2015). "Minimum-dissipation models for large-eddy simulation", Physics of Fluids 27, 085107.
Anisotropic Minimum Dissipation model (Rozema et al., 2015).
Type signature:
mutable struct AMDModel{T<:AbstractFloat, N,
A<:AbstractArray{T, N},
Arch<:AbstractArchitecture} <: EddyViscosityModelFields:
| Field | Type | Description |
|---|---|---|
C | T | Poincaré constant |
filter_width | NTuple{N, T} | Anisotropic filter widths |
filter_width_sq | NTuple{N, T} | Δₖ² for each direction |
eddy_viscosity | A | νₑ field |
eddy_diffusivity | A | κₑ field (for scalars) |
field_size | NTuple{N, Int} | Grid dimensions |
clip_negative | Bool | Whether to clip νₑ < 0 |
architecture | Arch | CPU() or GPU() |
Constructor:
AMDModel(;
C = 1/12,
filter_width::NTuple{N, Real},
field_size::NTuple{N, Int},
clip_negative = true,
dtype = Float64,
architecture = CPU()
)Eddy Viscosity Computation
computeeddyviscosity!
Tarang.compute_eddy_viscosity! — Function
compute_eddy_viscosity!(model::AMDModel, velocity_gradients...)Compute AMD eddy viscosity from velocity gradient components.
GPU-aware: Uses broadcasting for GPU arrays, optimized SIMD loops for CPU.
2D Case
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y)3D Case
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂u∂z, ∂v∂x, ∂v∂y, ∂v∂z, ∂w∂x, ∂w∂y, ∂w∂z)The AMD formula uses anisotropic scaling: νₑ† = -C (Δₖ² ∂uᵢ/∂xₖ ∂uⱼ/∂xₖ Sᵢⱼ) / (∂uₘ/∂xₙ ∂uₘ/∂xₙ)
Compute eddy viscosity from velocity gradient components. The gradients are plain grid-space arrays of size model.field_size, passed in component-major order (all derivatives of u, then all of v, then all of w).
2D Signature:
compute_eddy_viscosity!(model, ∂u∂x, ∂u∂y, ∂v∂x, ∂v∂y)3D Signature:
compute_eddy_viscosity!(model,
∂u∂x, ∂u∂y, ∂u∂z,
∂v∂x, ∂v∂y, ∂v∂z,
∂w∂x, ∂w∂y, ∂w∂z
)Returns: The eddy viscosity array model.eddy_viscosity
computeeddydiffusivity!
Tarang.compute_eddy_diffusivity! — Function
compute_eddy_diffusivity!(model::AMDModel, velocity_gradients..., scalar_gradients...)Compute eddy diffusivity for scalar transport using AMD model.
GPU-aware: Uses broadcasting for GPU arrays, optimized SIMD loops for CPU.
For a scalar field b with gradient ∇b, the AMD eddy diffusivity (Abkar, Bae & Moin 2016, eq. 2.7) is the FULL double contraction over the scaled-gradient direction k AND all velocity components i:
κₑ = max(0, κₑ†), κₑ† = -C · [ Σₖ δₖ² (∂ₖ uᵢ)(∂ₖ b)(∂ᵢ b) ] / [ (∂ₗ b)(∂ₗ b) ]i.e. for each direction k form the inner sum Σᵢ (∂ₖ uᵢ)(∂ᵢ b) over ALL velocity components uᵢ, weight by δₖ²(∂ₖ b), and sum over k. The method therefore needs every velocity-gradient component ∂uᵢ/∂xₖ (2D: 4 of them; 3D: 9), passed in component-major order, followed by the scalar gradients ∂b/∂xₖ. (An earlier version summed only a single velocity component, contracting the scaled velocity gradient with the SAME scalar-gradient direction twice — that is NOT the AMD diffusivity and is fixed here.)
Compute eddy diffusivity for scalar transport (AMD model only). The AMD diffusivity (Abkar, Bae & Moin 2016, eq. 2.7) is a full double contraction over the scaled-gradient direction k and every velocity component i:
κₑ† = -C · [ Σₖ Δₖ² (∂ₖuᵢ)(∂ₖb)(∂ᵢb) ] / [ (∂ₗb)(∂ₗb) ]so the method needs the complete velocity-gradient tensor (2D: 4 components, 3D: 9), not just the gradient of one velocity component. The scalar gradients follow.
2D Signature:
compute_eddy_diffusivity!(model::AMDModel,
∂u∂x, ∂u∂y,
∂v∂x, ∂v∂y,
∂b∂x, ∂b∂y
)3D Signature:
compute_eddy_diffusivity!(model::AMDModel,
∂u∂x, ∂u∂y, ∂u∂z,
∂v∂x, ∂v∂y, ∂v∂z,
∂w∂x, ∂w∂y, ∂w∂z,
∂b∂x, ∂b∂y, ∂b∂z
)Returns: The eddy diffusivity array model.eddy_diffusivity
Subgrid Stress
computesgsstress
Tarang.compute_sgs_stress — Function
compute_sgs_stress(model::EddyViscosityModel, strain_components...)Compute the deviatoric subgrid stress tensor:
τᵢⱼᵈ = -2 νₑ S̄ᵢⱼGPU-aware: Uses broadcasting which works for both CPU and GPU arrays.
2D Output
Returns (τ11, τ12, τ22).
3D Output
Returns (τ11, τ12, τ13, τ22, τ23, τ33).
Compute deviatoric SGS stress tensor τᵢⱼ = -2 νₑ S̄ᵢⱼ.
2D Signature:
compute_sgs_stress(model, S11, S12, S22) -> (τ11, τ12, τ22)3D Signature:
compute_sgs_stress(model, S11, S12, S13, S22, S23, S33)
-> (τ11, τ12, τ13, τ22, τ23, τ33)Accessors
geteddyviscosity
Tarang.get_eddy_viscosity — Function
get_eddy_viscosity(model::EddyViscosityModel)Return the current eddy viscosity field.
Return the current eddy viscosity field (an Array on CPU(), a CuArray on GPU()).
get_eddy_viscosity(model::EddyViscosityModel) -> A <: AbstractArray{T, N}geteddydiffusivity
Tarang.get_eddy_diffusivity — Function
get_eddy_diffusivity(model::AMDModel)Return the current eddy diffusivity field (AMD model only).
Return the current eddy diffusivity field (AMD only).
get_eddy_diffusivity(model::AMDModel) -> A <: AbstractArray{T, N}getfilterwidth
Tarang.get_filter_width — Function
get_filter_width(model::EddyViscosityModel)Return the filter width(s).
Return the filter width tuple.
get_filter_width(model::EddyViscosityModel) -> NTuple{N, T}Statistics
meaneddyviscosity
Tarang.mean_eddy_viscosity — Function
mean_eddy_viscosity(model::EddyViscosityModel; global_reduce=true)Compute the domain-averaged eddy viscosity.
With global_reduce=true (the default) this is a collective call on MPI.COMM_WORLD: every rank must call it, or the ones that do will hang. In particular rank == 0 && @info mean_eddy_viscosity(model) deadlocks — compute on all ranks first, then log on one. Pass global_reduce=false for this rank's slab only, which is safe to call from a subset of ranks.
Compute domain-averaged eddy viscosity.
mean_eddy_viscosity(model::EddyViscosityModel) -> Tmaxeddyviscosity
Tarang.max_eddy_viscosity — Function
max_eddy_viscosity(model::EddyViscosityModel; global_reduce=true)Return the maximum eddy viscosity in the domain.
See mean_eddy_viscosity — with global_reduce=true every rank must call this or the callers hang.
Return maximum eddy viscosity in the domain.
max_eddy_viscosity(model::EddyViscosityModel) -> TDissipation Rate
sgs_dissipation
Tarang.sgs_dissipation — Function
sgs_dissipation(model::EddyViscosityModel, strain_magnitude::AbstractArray)Compute the subgrid-scale dissipation rate:
εₛₛ = νₑ |S̄|²where |S̄| = √(2 S̄ᵢⱼS̄ᵢⱼ) (the convention used by compute_eddy_viscosity!). The exact dissipation is εₛₛ = -τᵢⱼ S̄ᵢⱼ = 2 νₑ S̄ᵢⱼS̄ᵢⱼ = νₑ |S̄|² with that |S̄|; an extra factor of 2 here would double-count (the strain magnitude already carries it).
GPU-aware: Uses broadcasting which works for both CPU and GPU arrays. Returns the dissipation field.
Compute the SGS dissipation rate field: εₛₛ = νₑ |S̄|²
There is no extra factor of 2 here. The exact dissipation is εₛₛ = -τᵢⱼ S̄ᵢⱼ = 2 νₑ S̄ᵢⱼS̄ᵢⱼ, and the |S̄| = √(2 S̄ᵢⱼS̄ᵢⱼ) convention used by compute_eddy_viscosity! already carries it, so εₛₛ = νₑ |S̄|².
sgs_dissipation(model::EddyViscosityModel, strain_magnitude) -> A <: AbstractArray{T, N}The natural argument is the strain magnitude the model just cached, model.strain_magnitude (Smagorinsky only — the AMD model does not cache one).
meansgsdissipation
Tarang.mean_sgs_dissipation — Function
mean_sgs_dissipation(model::EddyViscosityModel, strain_magnitude::AbstractArray)Compute domain-averaged SGS dissipation rate. GPU-aware: Uses broadcasting and sum() which work for both CPU and GPU arrays.
See mean_eddy_viscosity — with global_reduce=true every rank must call this or the callers hang.
Compute domain-averaged SGS dissipation rate.
mean_sgs_dissipation(model::EddyViscosityModel, strain_magnitude) -> TConfiguration
set_constant!
Tarang.set_constant! — Function
set_constant!(model::SmagorinskyModel, C_s::Real)Update the Smagorinsky constant.
set_constant!(model::AMDModel, C::Real)Update the AMD Poincaré constant.
Update the model constant.
set_constant!(model::SmagorinskyModel, C_s::Real)
set_constant!(model::AMDModel, C::Real)reset!
Reset eddy viscosity (and diffusivity for AMD) to zero.
reset!(model::EddyViscosityModel)
reset!(model::AMDModel) # Also resets eddy_diffusivityExports
export SGSModel, EddyViscosityModel
export SmagorinskyModel, AMDModel
export compute_eddy_viscosity!, compute_eddy_diffusivity!
export compute_sgs_stress
export get_eddy_viscosity, get_eddy_diffusivity, get_filter_width
export mean_eddy_viscosity, max_eddy_viscosity
export reset!, set_constant!, set_filter_width!
export sgs_dissipation, mean_sgs_dissipationMutating a model
set_constant!(model, C) and set_filter_width!(model, Δ) validate their argument and keep the cached derived quantities (filter_width_sq, effective_delta) in step. Assigning model.filter_width directly is also honoured — the kernels re-derive Δ² and the geometric-mean Δ on every call — but it leaves those cached fields reading stale, so prefer the setter.
Index
Tarang.AMDModelTarang.SmagorinskyModelTarang.compute_eddy_diffusivity!Tarang.compute_eddy_viscosity!Tarang.compute_sgs_stressTarang.get_eddy_diffusivityTarang.get_eddy_viscosityTarang.get_filter_widthTarang.max_eddy_viscosityTarang.mean_eddy_viscosityTarang.mean_sgs_dissipationTarang.set_constant!Tarang.sgs_dissipation