Kinetic energy equation
The KineticEnergyEquation module provides diagnostics for every term in the resolved kinetic energy (KE) budget. The kinetic energy per unit mass is
\[e_k = \tfrac{1}{2} u_i u_i ,\]
where $i$ runs over the prognostic velocity components of the model: $i = 1, 2, 3$ on a NonhydrostaticModel, which steps $u$, $v$ and $w$, and $i = 1, 2$ on a HydrostaticFreeSurfaceModel, which steps $u$ and $v$ and diagnoses $w$ from continuity. Currently, all diagnostics are computed at (Center, Center, Center).
The budget of $e_k$ follows from contracting the momentum equations with the velocity. The two model types differ in where buoyancy and the free surface enter it, so each is briefly derived in turn below, and the table that follows lists every quantity with its expression on each model and the diagnostic that computes it.
Nonhydrostatic models
A NonhydrostaticModel steps the Boussinesq momentum equations
\[\partial_t u_i + \partial_j (u_j u_i) + (\boldsymbol{f} \times \boldsymbol{u})_i = -\partial_i p' - \partial_i p_{nh} + b \, \delta_{i3} - \partial_j \tau_{ij} + F_{u_i} , \qquad \partial_j u_j = 0 , \qquad i = 1, 2, 3 ,\]
where $\tau_{ij}$ is the viscous or subgrid momentum flux Oceananigans' kernels carry (the negative of the stress tensor, $-2\nu S_{ij}$ for a constant viscosity), $F_{u_i}$ is the forcing, and the kinematic pressure is split into a hydrostatic anomaly and a nonhydrostatic part, $p = p' + p_{nh}$, with $\partial_z p' = b$ and $p' = 0$ at the top. The split is Oceananigans' default when there is a buoyancy model and the vertical direction is bounded; with hydrostatic_pressure_anomaly = nothing the whole pressure is $p_{nh}$ and $b$ stays in the $w$ equation. The nonhydrostatic pressure comes out of the Poisson problem that enforces $\partial_j u_j = 0$, and with a free_surface its value at the top is $g \eta$.
Contracting with $u_i$ gives, term by term: $u_i \partial_t u_i = \partial_t e_k$; the advection $u_i \partial_j (u_j u_i)$, which equals $\partial_j (u_j e_k)$ for a divergence-free flow; nothing from the Coriolis acceleration, which is normal to the velocity; $wb$ from the buoyancy force; the viscous work $-u_i \partial_j \tau_{ij} = -\partial_j (u_i \tau_{ij}) - \varepsilon_k$, with $\varepsilon_k = -\tau_{ij} \partial_j u_i$ the dissipation rate; and the pressure work $-u_i \partial_i (p' + p_{nh})$. The budget reads
\[\partial_t e_k = - \underbrace{u_i \partial_j(u_j u_i)}_{\text{advection}} - \underbrace{u_i \partial_i (p' + p_{nh})}_{\text{pressure work}} + \underbrace{wb}_{\text{conversion}} + \underbrace{u_i F_{u_i}}_{\text{forcing}} - \underbrace{u_i \partial_j \tau_{ij}}_{\text{viscous work}} , \qquad i = 1, 2, 3 ,\]
Hydrostatic models
A HydrostaticFreeSurfaceModel steps the horizontal momentum equations
\[\partial_t u_i + \partial_j (u_j u_i) + (\boldsymbol{f} \times \boldsymbol{u})_i = -\partial_i p' - \partial_i (g \eta) - \partial_j \tau_{ij} + F_{u_i} , \qquad i = 1, 2 , \quad j = 1, 2, 3 ,\]
with hydrostatic balance, $\partial_z p' = b$ and $p' = 0$ at the top, in place of a $w$ equation. $w$ comes from continuity, integrated up from the bottom, and the free surface from $\partial_t \eta = -\nabla_h \cdot \boldsymbol{U}$, with $\boldsymbol{U}$ the depth-integrated transport, which by continuity is $w$ at the surface. The total pressure is $p' + g \eta$.
Contracting with $u_i$, $i = 1, 2$, goes as before, with $\partial_j u_j = 0$ still holding for the diagnosed $w$, and no buoyancy term emerges:
\[\partial_t e_k = - \underbrace{u_i \partial_j(u_j u_i)}_{\text{advection}} - \underbrace{u_i \partial_i p'}_{\text{pressure work}} - \underbrace{u_i \partial_i (g \eta)}_{\text{barotropic pressure work}} + \underbrace{u_i F_{u_i}}_{\text{forcing}} - \underbrace{u_i \partial_j \tau_{ij}}_{\text{viscous work}} , \qquad i = 1, 2 , \quad j = 1, 2, 3 .\]
Buoyancy reaches $e_k$ through $p'$ alone. Using continuity and $\partial_z p' = b$, the work of the hydrostatic anomaly is
\[-u_i \partial_i p' = -\partial_j (u_j p') + wb , \qquad i = 1, 2, \quad j = 1, 2, 3 ,\]
so the conversion $wb$ is present pointwise, inside the work of $p'$ together with a transport term, which is why PotentialToKineticEnergyConversion uses the diagnosed $w$ on a hydrostatic model. The package writes the budget in the same form as the nonhydrostatic one. Adding and subtracting the vertical hydrostatic work $w \, \partial_z p'$, which is $wb$ by hydrostatic balance, and gathering the two pressure gradients into one, turns the pressure work into the three-component contraction with the total pressure $p' + g \eta$ plus the conversion,
\[\partial_t e_k = - \underbrace{u_i \partial_j(u_j u_i)}_{\text{advection}} - \underbrace{u_j \partial_j (p' + g \eta)}_{\text{pressure work}} + \underbrace{wb}_{\text{conversion}} + \underbrace{u_i F_{u_i}}_{\text{forcing}} - \underbrace{u_i \partial_j \tau_{ij}}_{\text{viscous work}} , \qquad i = 1, 2 , \quad j = 1, 2, 3 ,\]
where the pressure work now contracts all three components with the diagnosed $w$ and the total pressure, and is the same transport term as on the nonhydrostatic model, the divergence of the pressure flux $u_j (p' + g \eta)$, while $wb$ is the same conversion. This is the form KineticEnergyPressureRedistribution computes by default on a hydrostatic model. An ImplicitFreeSurface or a SplitExplicitFreeSurface applies $-g \nabla_h \eta$ in its free-surface step, outside the tendency kernel, so KineticEnergyTendency leaves the barotropic part out and the change of $e_k$ over a step is $u_i G_i - u_h \cdot \nabla_h (g \eta)$; with an ExplicitFreeSurface the gradient is inside $G_i$ and that part is already counted.
Vertically implicit closures
KineticEnergyTendency and KineticEnergyStress wrap Oceananigans' explicit tendency and its viscous term, which apply the closure's time discretization. A VerticallyImplicitTimeDiscretization (CATKE's default, and an option of ScalarDiffusivity and VerticalScalarDiffusivity) applies the $\partial_z u$ and $\partial_z v$ parts of the vertical fluxes in the implicit step, so both diagnostics leave that viscous work out, while KineticEnergyDissipationRate, built from the closure's explicit fluxes, keeps it. The terms still rebuild KineticEnergyTendency cell by cell, but that tendency is no longer the change of $e_k$ over a step and $\int u_i \partial_j \tau_{ij} \, \mathrm{d}V = \int \varepsilon_k \, \mathrm{d}V$ fails, so an integrated budget should use $\varepsilon_k$. Hydrostatic runs usually carry such a closure. The potential energy page has the same split between PotentialEnergyBuoyancyDiffusion and PotentialEnergyDiffusiveVerticalBuoyancyFlux.
Immersed boundaries
On an ImmersedBoundaryGrid the momentum tendencies subtract the interior $\partial_j \tau_{ij}$ and the stress through the immersed faces separately. KineticEnergyStress wraps the interior term only and KineticEnergyTendency carries both. The immersed stress vanishes under the default no-flux immersed conditions, but with an immersed ValueBoundaryCondition or a drag FluxBoundaryCondition on the velocities the budget is short by the wall-stress work, which the momentum equation exposes per component as ImmersedViscousDissipation. KineticEnergyDissipationRate applies no masking across immersed faces, so on such a grid it includes shear across faces the model treats as solid and $\int u_i \partial_j \tau_{ij} \, \mathrm{d}V = \int \varepsilon_k \, \mathrm{d}V$ does not hold.
Background fields
Background fields are not supported yet. On a NonhydrostaticModel built with background_fields, model.velocities holds the perturbation from the background flow, and every term except KineticEnergyTendency is built from the perturbation alone. KineticEnergyTendency comes off the model's own kernel, which also advects the perturbation with the background flow and the background flow with the perturbation. With BackgroundFields(; background_closure_fluxes = true) its viscous term is the closure flux of the total velocities, while KineticEnergyStress and KineticEnergyDissipationRate take the flux of the perturbation. No diagnostic here supplies the difference, so on such a model the terms do not add up to KineticEnergyTendency.
Terms and diagnostics
Every quantity below is pointwise. $G_i$ is Oceananigans' momentum tendency, and the hydrostatic $w$ is the one diagnosed from continuity. The advection, the pressure work and the viscous work enter the budget with a minus sign, the conversion and the forcing with a plus sign.
| Quantity | Nonhydrostatic | Hydrostatic | Diagnostic |
|---|---|---|---|
| Kinetic energy | $e_k = \tfrac{1}{2} u_i u_i$, with $i = 1, 2, 3$ | same, with $i = 1, 2$ | KineticEnergy |
| Tendency | $u_i G_i = \partial_t e_k + u_i \partial_i p_{nh}$ | $u_i G_i = \partial_t e_k + u_i \partial_i (g \eta)$, $i = 1, 2$, with an ImplicitFreeSurface or a SplitExplicitFreeSurface, and $u_i G_i = \partial_t e_k$ with an ExplicitFreeSurface | KineticEnergyTendency |
| Advection | $u_i \partial_j (u_j u_i)$ | same, with $i = 1, 2$ and the diagnosed $w$ in $u_j$ | KineticEnergyAdvection |
| Pressure work | $u_j \partial_j (p' + p_{nh})$ | $u_j \partial_j (p' + g \eta)$, $j = 1, 2, 3$, with the diagnosed $w$ | KineticEnergyPressureRedistribution |
| Conversion from potential energy | $wb$ | same, with the diagnosed $w$ | PotentialToKineticEnergyConversion |
| Forcing work | $u_i F_{u_i}$, $i = 1, 2, 3$ | same, with $i = 1, 2$ | KineticEnergyForcing |
| Viscous work | $u_i \partial_j \tau_{ij}$ | same, with $i = 1, 2$ | KineticEnergyStress |
| Dissipation rate | $\varepsilon_k = -\tau_{ij} \partial_j u_i$ | $\varepsilon_k = -(\tau_{1j} \partial_j u + \tau_{2j} \partial_j v)$ | KineticEnergyDissipationRate |
| Isotropic dissipation rate | $2 \nu S_{ij} S_{ij}$, equal to $\varepsilon_k$ for a constant viscosity | $2 \nu S_{ij} S_{ij}$, which keeps the $w$ row and so is not the dissipation of this budget | KineticEnergyIsotropicDissipationRate |
using Oceanostics brings in KineticEnergyAdvection, KineticEnergyForcing, etc. At the same time, using Oceanostics.KineticEnergyEquation brings in the short aliases: Advection, Forcing, etc.
Example
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid; buoyancy=BuoyancyTracer(), tracers=:b, closure=ScalarDiffusivity(ν=1e-4));julia> eₖ = KineticEnergyEquation.KineticEnergy(model)KineticEnergy KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: kinetic_energy_ccc (generic function with 1 method)└── arguments: ("Field", "Field", "Field")└── computes: kinetic energy ½uᵢuᵢjulia> εₖ = KineticEnergyEquation.KineticEnergyIsotropicDissipationRate(model)KineticEnergyIsotropicDissipationRate KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: isotropic_viscous_dissipation_rate_ccc (generic function with 1 method)└── arguments: ("Field", "Field", "Field", "NamedTuple")└── computes: isotropic kinetic energy dissipation rate 2νSᵢⱼSᵢⱼjulia> wb = KineticEnergyEquation.PotentialEnergyConversion(model)PotentialToKineticEnergyConversion KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: uᵢbᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "BuoyancyForce", "NamedTuple")└── computes: potential to kinetic energy conversion uᵢbᵢSummary of $e_k$ equation terms
Oceanostics.KineticEnergyEquation.KineticEnergy — Type
KineticEnergy(model; kwargs...)
Calculate the kinetic energy per unit mass eₖ = ½uᵢuᵢ of model. On a HydrostaticFreeSurfaceModel, which steps u and v and diagnoses w from continuity, this is ½(u² + v²).
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid);julia> KE = KineticEnergyEquation.KineticEnergy(model)KineticEnergy KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: kinetic_energy_ccc (generic function with 1 method)└── arguments: ("Field", "Field", "Field")└── computes: kinetic energy ½uᵢuᵢOceanostics.KineticEnergyEquation.KineticEnergyTendency — Type
KineticEnergyTendency(model; location)
Return a KernelFunctionOperation that computes the tendency uᵢGᵢ of the kinetic energy eₖ, excluding the nonhydrostatic pressure contribution:
∂ₜeₖ = ½∂ₜuᵢ² = uᵢGᵢ - uᵢ∂ᵢpₙₕₛOn a HydrostaticFreeSurfaceModel the sum runs over u and v only, and Gᵢ is Oceananigans' hydrostatic tendency, which leaves out the part of the barotropic pressure gradient the free surface treats implicitly (all of it for an ImplicitFreeSurface or a SplitExplicitFreeSurface), so the change of eₖ over a step is uₕGₕ - uₕ⋅∇ₕ(gη) there.
On either model Gᵢ is the explicit tendency. A closure with a VerticallyImplicitTimeDiscretization (CATKE's default, and an option of ScalarDiffusivity and VerticalScalarDiffusivity) takes the ∂zu and ∂zv parts of its vertical fluxes out of Gᵢ and applies them in the implicit step, so this term, like KineticEnergyStress, leaves out the vertical viscous work the model does. The change of eₖ over a time step then differs from uᵢGᵢ - uᵢ∂ᵢpₙₕₛ by that work, which DissipationRate still accounts for. On an ImmersedBoundaryGrid Gᵢ also carries the stress through the immersed faces, which KineticEnergyStress leaves out.
julia> using Oceananigansjulia> grid = RectilinearGrid(size = (1, 1, 4), extent = (1, 1, 1));julia> model = NonhydrostaticModel(grid);julia> using Oceanostics.KineticEnergyEquation: KineticEnergyTendencyjulia> ke_tendency = KineticEnergyTendency(model)KineticEnergyTendency KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢGᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("Centered", "Nothing", "Nothing", "Nothing", "Nothing", "Nothing", "Nothing", "Nothing", "BackgroundFields", "NamedTuple", "NamedTuple", "NamedTuple", "Nothing", "Nothing", "Clock", "NamedTuple")└── computes: kinetic energy tendency uᵢGᵢ (excl. nonhydrostatic pressure)Oceanostics.KineticEnergyEquation.KineticEnergyAdvection — Type
KineticEnergyAdvection(model; velocities, location)
Return a KernelFunctionOperation that computes the advection term, defined as
ADV = uᵢ∂ⱼ(uᵢuⱼ)On a HydrostaticFreeSurfaceModel it is uₕ⋅(u⃗⋅∇)uₕ over u and v only, built on the kernels (U_dot_∇u, U_dot_∇v) the hydrostatic momentum equations advect with, so it follows the model's scheme, VectorInvariant included. The hydrostatic metric terms that a flux-form scheme picks up on a curvilinear grid are left out, as they are by UMomentumEquation.Advection; they vanish on a RectilinearGrid and for a VectorInvariant scheme.
By default, the advection will be calculated using the resolved velocities and users can use the keyword velocities to modify that behavior:
julia> using Oceananigansjulia> grid = RectilinearGrid(size = (1, 1, 4), extent = (1,1,1));julia> model = NonhydrostaticModel(grid);julia> using Oceanostics.KineticEnergyEquation: KineticEnergyAdvectionjulia> ADV = KineticEnergyAdvection(model)KineticEnergyAdvection KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢ∂ⱼuⱼuᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "Centered")└── computes: kinetic energy advection uᵢ∂ⱼ(uᵢuⱼ)Oceanostics.KineticEnergyEquation.KineticEnergyStress — Type
KineticEnergyStress(model; location)
Return a KernelFunctionOperation that computes the diffusive term of the eₖ prognostic equation:
DIFF = uᵢ∂ⱼτᵢⱼwhere uᵢ are the velocity components and τᵢⱼ is the viscous momentum flux (minus the stress tensor). It enters the kinetic energy budget as -DIFF, and splits into a transport divergence plus the dissipation rate εₖ (DissipationRate).
A HydrostaticFreeSurfaceModel has no w-momentum equation, so on that model the sum runs over i = 1, 2 only and DIFF is the stress term of the budget of ½(u² + v²). The fluxes τ₁ⱼ and τ₂ⱼ are still computed with the diagnostic w, as in the model's own u and v tendencies.
∂ⱼτᵢⱼ is Oceananigans' own explicit viscous term, with the closure's time discretization applied. A closure with a VerticallyImplicitTimeDiscretization (CATKE's default, and an option of ScalarDiffusivity and VerticalScalarDiffusivity) takes the ∂zu and ∂zv parts of its vertical fluxes out of that term and applies them in the implicit step, so DIFF then leaves out the vertical viscous work the model does, as KineticEnergyTendency does, while DissipationRate keeps the full explicit flux. Under such a closure ∫DIFF dV = ∫εₖ dV no longer holds, and an integrated budget should be written with εₖ rather than with DIFF.
On an ImmersedBoundaryGrid the tendency kernels also subtract the stress through the immersed faces, immersed_∂ⱼτᵢⱼ, which DIFF does not include. It vanishes under the default no-flux immersed boundary conditions, but with an immersed ValueBoundaryCondition or a drag FluxBoundaryCondition on the velocities the wall-stress work is left out of the budget; the momentum modules expose it per component as ImmersedViscousDissipation.
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid; closure=ScalarDiffusivity(ν=1e-4));julia> DIFF = KineticEnergyEquation.KineticEnergyStress(model)KineticEnergyStress KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: uᵢ∂ⱼ_τᵢⱼᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "ScalarDiffusivity", "Nothing", "Clock", "NamedTuple", "Nothing")└── computes: kinetic energy stress/diffusion uᵢ∂ⱼτᵢⱼOceanostics.KineticEnergyEquation.KineticEnergyForcing — Type
KineticEnergyForcing(model; location)
Return a KernelFunctionOperation that computes the forcing term of the KE prognostic equation:
FORC = uᵢFᵤᵢwhere uᵢ are the velocity components and Fᵤᵢ is the forcing term(s) in the uᵢ prognostic equation (i.e. the forcing for uᵢ). On a HydrostaticFreeSurfaceModel the sum runs over u and v only, since w has no momentum equation, and so no forcing, there.
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid);julia> FORC = KineticEnergyEquation.KineticEnergyForcing(model)KineticEnergyForcing KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: uᵢFᵤᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "Clock", "NamedTuple")└── computes: kinetic energy forcing uᵢFᵤᵢOceanostics.KineticEnergyEquation.KineticEnergyPressureRedistribution — Type
KineticEnergyPressureRedistribution(
model;
velocities,
pressure,
location
)
Return a KernelFunctionOperation that computes the pressure redistribution term:
PR = uᵢ∂ᵢpwhere p is the pressure. By default p is taken to be the total pressure (nonhydrostatic + hydrostatic):
julia> using Oceananigansjulia> grid = RectilinearGrid(size = (1, 1, 4), extent = (1,1,1));julia> model = NonhydrostaticModel(grid);julia> using Oceanostics.KineticEnergyEquation: KineticEnergyPressureRedistributionjulia> ∇u⃗p = KineticEnergyPressureRedistribution(model)KineticEnergyPressureRedistribution KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢ∂ᵢpᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "Field")└── computes: kinetic energy pressure redistribution uᵢ∂ᵢpWe can also pass velocities and pressure keywords to perform more specific calculations. The example below illustrates calculation of the nonhydrostatic contribution to the pressure redistrubution term:
julia> ∇u⃗pNHS = KineticEnergyPressureRedistribution(model, pressure=model.pressures.pNHS)KineticEnergyPressureRedistribution KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢ∂ᵢpᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "Field")└── computes: kinetic energy pressure redistribution uᵢ∂ᵢpOn a HydrostaticFreeSurfaceModel the total pressure is p′ + gη: pressure defaults to the hydrostatic pressure anomaly model.pressure.pHY′, the pressure the hydrostatic momentum tendencies carry, and the work of the surface pressure gη of free_surface (model.free_surface by default) is added, so that PR is the work of the total pressure on both model types; on a NonhydrostaticModel with a free surface gη is the surface value of pNHS, and the default there already contains it. free_surface = nothing leaves the work of the anomaly alone. The sum runs over all three components with the diagnosed w, although the hydrostatic tendencies have no w row. The z row is w∂zp′ = wb by hydrostatic balance, and exactly so on Oceananigans' grid away from the top face, since p′ is integrated from the same face buoyancy that PotentialEnergyConversion multiplies by w. So -PR + wb is the work of the horizontal gradients, the one the model applies, and the budget reads ∂ₜeₖ = -uᵢ∂ⱼ(uⱼuᵢ) - uᵢ∂ⱼτᵢⱼ - PR + wb + uᵢFᵤᵢ on both model types; the two-component work of the anomaly alone is PR - uₕ⋅∇ₕ(gη) - wb.
On a NonhydrostaticModel that has taken a time step, Oceananigans (0.106 to 0.116 at least) leaves the halos of pNHS at the pressure times Δt: it corrects the velocities with p Δt and then divides the interior alone by Δt. The default p is a computed Field, which fills its own halos when the term is computed, so it is not affected. A pressure passed by hand, such as model.pressures.pNHS above, is differentiated across its own halos, so in the cells next to a periodic boundary the term is wrong until they are refilled with fill_halo_regions!(model.pressures.pNHS) right before it is evaluated. The error is a redistribution and barely changes a volume integral.
Oceanostics.KineticEnergyEquation.PotentialEnergyConversion — Type
PotentialEnergyConversion(
model;
velocities,
tracers,
location
)
Return a KernelFunctionOperation that computes the conversion of potential energy into kinetic energy, defined as
uᵢbᵢwhere bᵢ is the component of the buoyancy acceleration in the i-th direction (which is zero for x and y, except when gravity_unit_vector isn't aligned with the grid's z-direction) and all three components of i=1,2,3 are added up.
On a HydrostaticFreeSurfaceModel, w is the vertical velocity diagnosed from continuity. Buoyancy enters the hydrostatic momentum equations through the hydrostatic pressure, so wb is not one of the terms of uₕGₕ there; it is the conversion that the horizontal pressure work -uₕ⋅∇ₕp′ contains, since ∂p′/∂z = b. KineticEnergyPressureRedistribution contracts all three components on that model, so that its z row is this wb and the budget reads -PR + wb on both model types.
By default, the conversion will be calculated using the resolved velocities and tracers:
julia> using Oceananigansjulia> grid = RectilinearGrid(size = (1, 1, 4), extent = (1,1,1));julia> model = NonhydrostaticModel(grid; buoyancy=BuoyancyTracer(), tracers=:b);julia> using Oceanostics.KineticEnergyEquation: PotentialEnergyConversionjulia> wb = PotentialEnergyConversion(model)PotentialToKineticEnergyConversion KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢbᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "BuoyancyForce", "NamedTuple")└── computes: potential to kinetic energy conversion uᵢbᵢIf we want to calculate only the turbulent conversion rate, we can do so by passing turbulent perturbations to the velocities and/or tracers options):
julia> w′ = Field(model.velocities.w - Field(Average(model.velocities.w)));julia> b′ = Field(model.tracers.b - Field(Average(model.tracers.b)));julia> w′b′ = PotentialEnergyConversion(model, velocities=(u=model.velocities.u, v=model.velocities.v, w=w′), tracers=(b=b′,))PotentialToKineticEnergyConversion KernelFunctionOperation at (Center, Center, Center)├── grid: 1×1×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 1×1×3 halo├── kernel_function: uᵢbᵢᶜᶜᶜ (generic function with 1 method)└── arguments: ("NamedTuple", "BuoyancyForce", "NamedTuple")└── computes: potential to kinetic energy conversion uᵢbᵢOceanostics.KineticEnergyEquation.DissipationRate — Type
DissipationRate(
model;
U,
V,
W,
perturbation_boundary_conditions,
location
)
Calculate the kinetic energy dissipation rate, defined as
εₖ = -∂ⱼuᵢ ⋅ τᵢⱼwhere ∂ⱼuᵢ is the velocity gradient tensor and τᵢⱼ is the closure's viscous momentum flux (minus the stress tensor), in the same convention Oceananigans uses. That definition holds for any closure. For a Newtonian flux with a scalar viscosity, τᵢⱼ = -2νSᵢⱼ, it reduces on a NonhydrostaticModel to εₖ = 2νSᵢⱼSᵢⱼ ≥ 0, which KineticEnergyIsotropicDissipationRate computes directly; neither that reduction nor, for a flux that is not down-gradient, the sign carries over to other closures.
A HydrostaticFreeSurfaceModel has no w-momentum equation, so on that model the sum runs over i = 1, 2 only and εₖ is the dissipation rate of ½(u² + v²). The fluxes τ₁ⱼ and τ₂ⱼ are still computed with the diagnostic w, as in the model's own u and v tendencies. The reduction to 2νSᵢⱼSᵢⱼ needs the w row, so there εₖ is not that, and the isotropic rate, which keeps the row, is not a term of the hydrostatic budget. ∫εₖ dV is still the viscous sink of ½(u² + v²). Pointwise, εₖ ≥ 0 with a horizontal or vertical formulation (HorizontalScalarDiffusivity, VerticalScalarDiffusivity, CATKE), whose τ is built from u and v alone. With a three-dimensional ScalarDiffusivity, whose τ₁₃ carries ∂ₓw, the cross term ν ∂zu ∂ₓw has no w row to complete its square, so εₖ is indefinite cell by cell, by a term of order aspect ratio squared relative to ν(∂zu)², which is the order the hydrostatic approximation neglects elsewhere.
The fluxes are the closure's explicit viscous_flux_*, with no time discretization applied, so under a closure with a VerticallyImplicitTimeDiscretization (CATKE's default, and an option of ScalarDiffusivity and VerticalScalarDiffusivity) εₖ still includes the vertical shear term that the model applies in the implicit step. KineticEnergyStress and KineticEnergyTendency leave that term out, so ∫DIFF dV = ∫εₖ dV fails there and an integrated budget should be written with εₖ.
On an ImmersedBoundaryGrid the same bare fluxes are not masked across immersed faces, as the _viscous_flux_* behind ∂ⱼτᵢⱼ are, so εₖ there includes the shear across faces the model treats as solid and ∫DIFF dV = ∫εₖ dV does not hold either.
U, V and W are means removed from the velocities before εₖ is computed, which makes it the dissipation rate of the turbulent kinetic energy. Each perturbation is a computed Field whose boundary conditions are zero flux at every wall unless perturbation_boundary_conditions sets them, keyed by velocity. At a wall where a velocity has a value or gradient condition, the zero-flux default drops the wall shear from the cells next to it, so pass the condition the perturbation satisfies there. For a no-slip bottom and a horizontal mean, that is perturbation_boundary_conditions = (u = FieldBoundaryConditions(bottom = ValueBoundaryCondition(0)),). See Oceanostics.perturbation_fields for the details.
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid; closure=ScalarDiffusivity(ν=1e-4));julia> ε = KineticEnergyEquation.DissipationRate(model)KineticEnergyDissipationRate KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: viscous_dissipation_rate_ccc (generic function with 1 method)└── arguments: ("Nothing", "NamedTuple", "NamedTuple")└── computes: kinetic energy dissipation rate -∂ⱼuᵢ·τᵢⱼOceanostics.KineticEnergyEquation.KineticEnergyIsotropicDissipationRate — Type
KineticEnergyIsotropicDissipationRate(
u,
v,
w,
closure,
closure_fields,
model_fields,
clock;
location
)
Calculate the isotropic kinetic energy dissipation rate as
εₖ = 2 ν SᵢⱼSᵢⱼ,where Sᵢⱼ is the strain rate tensor, for a fluid with an isotropic turbulence closure (i.e., a turbulence closure where ν (eddy or not) is the same for all directions). It keeps the w row on both model types, so on a HydrostaticFreeSurfaceModel it is not the dissipation term of the budget of ½(u² + v²), which DissipationRate computes: it includes the dissipation of ½w², which that budget does not carry.
julia> using Oceananigans, Oceanosticsjulia> grid = RectilinearGrid(size=(4, 4, 4), extent=(1, 1, 1));julia> model = NonhydrostaticModel(grid; closure=ScalarDiffusivity(ν=1e-4));julia> ε = KineticEnergyEquation.KineticEnergyIsotropicDissipationRate(model)KineticEnergyIsotropicDissipationRate KernelFunctionOperation at (Center, Center, Center)├── grid: 4×4×4 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo├── kernel_function: isotropic_viscous_dissipation_rate_ccc (generic function with 1 method)└── arguments: ("Field", "Field", "Field", "NamedTuple")└── computes: isotropic kinetic energy dissipation rate 2νSᵢⱼSᵢⱼ