Skip to content
285 changes: 285 additions & 0 deletions examples/prescribed_sst.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
using Oceananigans
using Oceananigans.Units
using Printf
using AquaSkyLES

Nx = 128
Nz = 128
Ny = 1
Lz = 2 * 1024
grid = RectilinearGrid(size = (Nx, Ny, Nz),
x = (0, 2Lz),
y = (0, 2Lz),
z = (0, Lz),
topology = (Periodic, Periodic, Bounded))

p₀ = 101325 # Pa
θ₀ = 285 # K
reference_state = AquaSkyLES.ReferenceState(base_pressure=p₀, potential_temperature=θ₀)
buoyancy = AquaSkyLES.MoistAirBuoyancy(; reference_state)
thermodynamics = buoyancy.thermodynamics
condensation = thermodynamics.condensation

# ρ₀ = AquaSkyLES.base_density(buoyancy) # air density at z=0
# cₚ = buoyancy.thermodynamics.dry_air.heat_capacity

Δz = Lz / Nz # grid spacing

parameters = (;
drag_coefficient = 1e-3,
heat_transfer_coefficient = 1e-3,
vapor_transfer_coefficient = 1e-3,
sea_surface_temperature = θ₀ + 10,
gust_speed = 1e-2, # directly added to friction velocity (i.e. not multiplied by drag coefficient Cᴰ)
base_air_density = AquaSkyLES.base_density(buoyancy), # air density at z=0,
thermodynamics,
condensation
)

# Utility for computing the saturation specific humidity at the sea surface
@inline surface_saturation_specific_humidity(T, parameters) =
AquaSkyLES.saturation_specific_humidity(T, parameters.base_air_density,
parameters.thermodynamics,
parameters.condensation)


@inline function friction_velocity(i, j, grid, clock, model_fields, parameters)
Cᴰ = parameters.drag_coefficient
u = model_fields.u[i, j, 1]
v = model_fields.v[i, j, 1]
Δu = u # stationary ocean
Δv = v # stationary ocean
return sqrt(Cᴰ * (Δu^2 + Δv^2)) + parameters.gust_speed
end

# Take care to handle U = 0
@inline function x_momentum_flux(i, j, grid, clock, model_fields, parameters)
u = model_fields.u[i, j, 1]
v = model_fields.v[i, j, 1]
u★ = friction_velocity(i, j, grid, clock, model_fields, parameters)
U = sqrt(u^2 + v^2)
return - u★^2 * u / U * (U > 0)
end

@inline function y_momentum_flux(i, j, grid, clock, model_fields, parameters)
u = model_fields.u[i, j, 1]
v = model_fields.v[i, j, 1]
u★ = friction_velocity(i, j, grid, clock, model_fields, parameters)
U = sqrt(u^2 + v^2)
return - u★^2 * v / U * (U > 0)
end

@inline function temperature_flux(i, j, grid, clock, model_fields, parameters)
u★ = friction_velocity(i, j, grid, clock, model_fields, parameters)
θˢ = parameters.sea_surface_temperature
Cᴰ = parameters.drag_coefficient
Cᴴ = parameters.heat_transfer_coefficient
Δθ = model_fields.θ[i, j, 1] - θˢ
# Using the scaling argument: u★ θ★ = Cᴴ * U * Δθ
θ★ = Cᴴ / sqrt(Cᴰ) * Δθ
return - u★ * θ★
end

@inline function vapor_flux(i, j, grid, clock, model_fields, parameters)
u★ = friction_velocity(i, j, grid, clock, model_fields, parameters)
θˢ = parameters.sea_surface_temperature
qˢ = surface_saturation_specific_humidity(θˢ, parameters)
Cᴰ = parameters.drag_coefficient
Cᵛ = parameters.vapor_transfer_coefficient
Δq = model_fields.q[i, j, 1] - qˢ
# Using the scaling argument: u★ q★ = Cᵛ * U * Δq
q★ = Cᵛ / sqrt(Cᴰ) * Δq
return - u★ * q★
end

u_surface_flux = FluxBoundaryCondition(x_momentum_flux; discrete_form=true, parameters)
v_surface_flux = FluxBoundaryCondition(y_momentum_flux; discrete_form=true, parameters)
θ_surface_flux = FluxBoundaryCondition(temperature_flux; discrete_form=true, parameters)
q_surface_flux = FluxBoundaryCondition(vapor_flux; discrete_form=true, parameters)

u_bcs = FieldBoundaryConditions(bottom=u_surface_flux)
v_bcs = FieldBoundaryConditions(bottom=v_surface_flux)
θ_bcs = FieldBoundaryConditions(bottom=θ_surface_flux)
q_bcs = FieldBoundaryConditions(bottom=q_surface_flux)

advection = WENO() #(momentum=WENO(), θ=WENO(), q=WENO(bounds=(0, 1)))
tracers = (:θ, :q)
model = NonhydrostaticModel(; grid, advection, buoyancy,
tracers = (:θ, :q),
boundary_conditions = (u=u_bcs, v=v_bcs, θ=θ_bcs, q=q_bcs))

Lz = grid.Lz
Δθ = 2.5 # K
Tₛ = reference_state.θ # K
θᵢ(x, y, z) = Tₛ + Δθ * z / Lz + 1e-2 * Δθ * randn()
qᵢ(x, y, z) = 1e-2 + 1e-5 * rand()
set!(model, θ=θᵢ, q=qᵢ)

simulation = Simulation(model, Δt=10, stop_time=4hours)
conjure_time_step_wizard!(simulation, cfl=0.7)

T = AquaSkyLES.TemperatureField(model)
qˡ = AquaSkyLES.CondensateField(model, T)
qᵛ★ = AquaSkyLES.SaturationField(model, T)
δ = Field(model.tracers.q - qᵛ★)

# Output BCs
# Sensible heat flux
Jθ = Oceananigans.Models.BoundaryConditionOperation(model.tracers.θ, :bottom, model)
ρ₀ = AquaSkyLES.base_density(buoyancy) # air density at z=0
cₚ = buoyancy.thermodynamics.dry_air.heat_capacity
JSH = Field(ρ₀ * cₚ * Jθ) # Heat flux [W/m^2]

# Latent heat flux
Jq = Oceananigans.Models.BoundaryConditionOperation(model.tracers.q, :bottom, model)
Lᵛ = parameters.condensation.latent_heat
JLH = Field(ρ₀ * Lᵛ * Jq) # [W/m^2]

# Kinematic momentum flux
Ju = Oceananigans.Models.BoundaryConditionOperation(model.velocities.u, :bottom, model) # [m^2/s^2]
Jv = Oceananigans.Models.BoundaryConditionOperation(model.velocities.v, :bottom, model) # [m^2/s^2]


function progress(sim)
compute!(T)
compute!(qˡ)
compute!(δ)
q = sim.model.tracers.q
θ = sim.model.tracers.θ
u, v, w = sim.model.velocities

umax = maximum(u)
vmax = maximum(v)
wmax = maximum(w)

qmin = minimum(q)
qmax = maximum(q)
qˡmax = maximum(qˡ)
δmax = maximum(δ)

θmin = minimum(θ)
θmax = maximum(θ)

msg = @sprintf("Iter: %d, t = %s, max|u|: (%.2e, %.2e, %.2e)",
iteration(sim), prettytime(sim), umax, vmax, wmax)

msg *= @sprintf(", extrema(q): (%.2e, %.2e), max(qˡ): %.2e, min(δ): %.2e, extrema(θ): (%.2e, %.2e)",
qmin, qmax, qˡmax, δmax, θmin, θmax)

@info msg

return nothing
end

add_callback!(simulation, progress, IterationInterval(10))

outputs = merge(model.velocities, model.tracers, (; T, qˡ, qᵛ★, JSH, JLH, Ju, Jv))

ow = JLD2Writer(model, outputs,
filename = "prescribed_sst_convection.jld2",
schedule = TimeInterval(2minutes),
overwrite_existing = true)

simulation.output_writers[:jld2] = ow

run!(simulation)

θt = FieldTimeSeries("prescribed_sst_convection.jld2", "θ")
Tt = FieldTimeSeries("prescribed_sst_convection.jld2", "T")
qt = FieldTimeSeries("prescribed_sst_convection.jld2", "q")
qˡt = FieldTimeSeries("prescribed_sst_convection.jld2", "qˡ")
times = qt.times
Nt = length(θt)

using GLMakie, Printf

n = Observable(1)

θn = @lift θt[$n]
qn = @lift qt[$n]
Tn = @lift Tt[$n]
qˡn = @lift qˡt[$n]
title = @lift "t = $(prettytime(times[$n]))"

fig = Figure(size=(800, 400), fontsize=12)
axθ = Axis(fig[1, 1], xlabel="x (m)", ylabel="z (m)")
axq = Axis(fig[1, 2], xlabel="x (m)", ylabel="z (m)")
axT = Axis(fig[2, 1], xlabel="x (m)", ylabel="z (m)")
axqˡ = Axis(fig[2, 2], xlabel="x (m)", ylabel="z (m)")

fig[0, :] = Label(fig, title, fontsize=22, tellwidth=false)

Tmin = minimum(Tt)
Tmax = maximum(Tt)

hmθ = heatmap!(axθ, θn, colorrange=(Tₛ, Tₛ+Δθ))
hmq = heatmap!(axq, qn, colorrange=(0.97e-2, 1.05e-2), colormap=:magma)
hmT = heatmap!(axT, Tn, colorrange=(Tmin, Tmax))
hmqˡ = heatmap!(axqˡ, qˡn, colorrange=(0, 1.5e-3), colormap=:magma)

# Label(fig[0, 1], "θ", tellwidth=false)
# Label(fig[0, 2], "q", tellwidth=false)
# Label(fig[0, 1], "θ", tellwidth=false)
# Label(fig[0, 2], "q", tellwidth=false)

Colorbar(fig[1, 0], hmθ, label = "θ [K]", vertical=true)
Colorbar(fig[1, 3], hmq, label = "q", vertical=true)
Colorbar(fig[2, 0], hmT, label = "T [K]", vertical=true)
Colorbar(fig[2, 3], hmqˡ, label = "qˡ", vertical=true)

fig

record(fig, "prescribed_sst.mp4", 1:Nt, framerate=12) do nn
n[] = nn
end


# surface flux timeseries
QSH = FieldTimeSeries("prescribed_sst_convection.jld2", "JSH")
QLH = FieldTimeSeries("prescribed_sst_convection.jld2", "JLH")
Qu = FieldTimeSeries("prescribed_sst_convection.jld2", "Ju")
Qv = FieldTimeSeries("prescribed_sst_convection.jld2", "Jv")

QSH_av = zeros(length(QSH.times))
QLH_av = zeros(length(QSH.times))
ΔT_av = zeros(length(QSH.times))
Qv_av = zeros(length(QSH.times))
Qu_av = zeros(length(QSH.times))

for n in 1:length(QSH.times)
QSHn = Field(Average(QSH[n], dims=1))
compute!(QSHn)
QSH_av[n] = QSHn[1, 1, 1]

QLHn = Field(Average(QLH[n], dims=1))
compute!(QLHn)
QLH_av[n] = QLHn[1, 1, 1]

Qun = Field(Average(Qu[n], dims=1))
compute!(Qun)
Qu_av[n] = Qun[1, 1, 1]

Qvn = Field(Average(Qv[n], dims=1))
compute!(Qvn)
Qv_av[n] = Qvn[1, 1, 1]

ΔT_av[n] = -mean(θt[:,1,1,n])+parameters.sea_surface_temperature

end

ΔTtitle = string(znodes(grid, Center())[1], "m air-sea temperature difference")
fig = Figure(size=(600, 700), fontsize=12)
axtau = Axis(fig[1, 1], ylabel=L"\tau/\rho_0 ~(\mathrm{m}^2/\mathrm{s}^2)", xlabel = "time [h]")
axΔT = Axis(fig[3, 1], ylabel=L"\Delta T ~(\mathrm{K})", xlabel = "time [h]", title = ΔTtitle)
axSH = Axis(fig[2, 1], ylabel=L"J~(\mathrm{W}/\mathrm{m}^2)", xlabel = "time [h]")

lines!(axtau, QSH.times/3600, Qu_av, label = L"\tau_x/\rho_0")
lines!(axtau, QSH.times/3600, Qv_av, label = L"\tau_y/\rho_0")
lines!(axΔT, QSH.times/3600, ΔT_av)
lines!(axSH, QSH.times/3600, QSH_av, label = L"J^{SH}")
lines!(axSH, QLH.times/3600, QLH_av, label = L"J^{LH}")

fig[1, 2] = Legend(fig, axtau, framevisible = false)
fig[2, 2] = Legend(fig, axSH, framevisible = false)
save("Surface_fluxes.png", fig, px_per_unit = 4)

8 changes: 8 additions & 0 deletions src/atmospheric_thermodynamics.jl
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,14 @@ end
return ref.p₀ * (1 - g * z / (cᵖᵈ * ref.θ))^ϰᵈ⁻¹
end

"""
saturation_specific_humidity(T, z, reference_state, thermodynamics, phase_transition)

Using the Clasius-Claperyon relation for saturation vapor pressure, compute the
saturation specific humidity at temperature `T`, height `z`, given the
`reference_state`, `thermodynamics` constants, and `phase_transition` constants representing
parameters representing either condensation (vapor to liquid) or deposition (vapor to solid).
"""
@inline function saturation_specific_humidity(T, z, ref::ReferenceState, thermo, phase_transition)
ρ = reference_density(z, ref, thermo)
return saturation_specific_humidity(T, ρ, thermo, phase_transition)
Expand Down