Replies: 8 comments 1 reply
-
@iuryt this is a great question! Do you mind if I convert this to a discussion? I think there are multiple answers and a discussion might be better suited for keeping track of the solutions for future users. There are a few possible solutions. Note that the diffusivities can also be Solution: pre-define a Richardson number field and compute in
|
Beta Was this translation helpful? Give feedback.
-
If there are ways to make it easier (eg what would your "dream" code look like?) then we should pursue them. This is exactly the kind of thing that I think we want to make as easy as possible (and we can make as easy as possible) with Oceananigans. |
Beta Was this translation helpful? Give feedback.
-
This script: using Oceananigans
using Oceananigans.Units
using Oceananigans.BoundaryConditions: fill_halo_regions!
using GLMakie
grid = RectilinearGrid(size=128, z=(-100, 0), halo=3, topology=(Flat, Flat, Bounded))
# Pacanowski-Philander (eg https://glwagner.github.io/OceanTurb.jl/latest/models/pacanowskiphilander/)
Ri = Field{Center, Center, Center}(grid) # ∂z(b) / (∂z(u)^2 + ∂z(v)^2)
ν₀ = 1e-4
ν₁ = 1e-2
κ₀ = 1e-5
κ₁ = 1e-2
c = 5
n = 2
ν = ν₀ + ν₁ / (1 + c * Ri^n)
κᵇ = κ₀ + κ₁ / (1 + c * Ri^(n+1))
@show ν # it's an operation...
closure = ScalarDiffusivity(VerticallyImplicitTimeDiscretization(); ν, κ=(; b=κᵇ))
model = HydrostaticFreeSurfaceModel(; grid, closure, buoyancy=BuoyancyTracer(), tracers=:b)
simulation = Simulation(model, Δt=1minute, stop_time=1day)
# Define an AbstractOperation that computes Ri in advance
u, v, w = model.velocities
b = model.tracers.b
Ri_op = @at (Center, Center, Center) ∂z(b) / (∂z(u)^2 + ∂z(v)^2)
""" Compute the Richardson number and store in `Ri`. """
function compute_Ri!(sim)
Ri .= Ri_op
# Zero NaNs
Ri_parent = parent(Ri)
parent(Ri_parent)[isnan.(Ri_parent)] .= 0
fill_halo_regions!(Ri, sim.model.architecture)
return nothing
end
simulation.callbacks[:compute_Ri] = Callback(compute_Ri!) # uses default IterationInterval(1)
fields = []
function grab_fields!(sim)
uⁿ = Array(interior(u, 1, 1, :))
vⁿ = Array(interior(v, 1, 1, :))
bⁿ = Array(interior(b, 1, 1, :))
iter = iteration(sim)
t = time(sim)
push!(fields, (; t, iter, u=uⁿ, v=vⁿ, b=bⁿ))
return nothing
end
simulation.callbacks[:grabber] = Callback(grab_fields!, TimeInterval(10minutes))
N² = 1e-4
S² = 1e-3 # Ri = 0.1
z₀ = -50
Δzu = 4
Δzb = 16
step(x, c, w) = 1/2 * (1 + tanh((x - c) / w)) # smooth step function
uᵢ(x, y, z) = Δzu * sqrt(S²) * step(z, z₀, Δzu)
bᵢ(x, y, z) = Δzb * N² * step(z, z₀, Δzb)
set!(model, u=uᵢ, b=bᵢ)
run!(simulation)
z = znodes(Center, grid)
fig = Figure()
ax_u = Axis(fig[1, 1], ylabel="z (m)", xlabel="u (m s⁻¹)")
ax_b = Axis(fig[1, 2], ylabel="z (m)", xlabel="b (m s⁻²)")
slider = Slider(fig[2, :], range=1:length(fields), startvalue=1)
n = slider.value
uⁿ = @lift fields[$n].u
bⁿ = @lift fields[$n].b
lines!(ax_u, uⁿ, z)
lines!(ax_b, bⁿ, z)
title = @lift "Diffusing shear layer at t = " * prettytime(fields[$n].t)
Label(fig[0, :], title)
display(fig)
record(fig, "pacanowski_philander_diffusion.gif", 1:length(fields), framerate=12) do nn
@info "Drawing frame $nn of $(length(fields))..."
n[] = nn
end produces My main concern is that our broadcasting may not be performant when used online during a simulation. Possibly, we can fix that or make it better. |
Beta Was this translation helpful? Give feedback.
-
Ok, a bit better now! The following uses some lower-level Oceananigans functions and The main downside of this solution is that we need to understand the staggered grid to implement it. Maybe not too onerous (@simone-silvestri thinks I should give users more credit), but part of me feels like we should be able to auto-magic our way around this. The main barrier to using abstract operations here is figuring out how to implement this function with a I also like the following because it's the first known example of Working on this helped uncover a few wrinkles in the user API:
using Oceananigans
using Oceananigans.Units
using Oceananigans.Operators
using GLMakie
# A bit of code...
@inline f²(i, j, k, grid, f, args...) = @inbounds f(i, j, k, grid, args...)^2
@inline function Riᶜᶜᶜ(i, j, k, grid, U, b)
N² = ℑzᵃᵃᶜ(i, j, k, grid, ∂zᶜᶜᶠ, b)
S²u = ℑxzᶜᵃᶜ(i, j, k, grid, f², ∂zᶠᶜᶠ, U.u)
S²v = ℑyzᵃᶜᶜ(i, j, k, grid, f², ∂zᶜᶠᶠ, U.v)
S² = S²u + S²v
return ifelse(S² == 0, zero(eltype(grid)), N² / S²)
end
grid = RectilinearGrid(size=128, z=(-100, 0), halo=3, topology=(Flat, Flat, Bounded))
fake_model = HydrostaticFreeSurfaceModel(; grid, tracers=:b, buoyancy=BuoyancyTracer())
velocities = fake_model.velocities
tracers = fake_model.tracers
# Pacanowski-Philander implementation
#
# The following implements the Packanowski-Philander model for shear-modulated mixing
# with parameters:
ν₀ = 1e-4
ν₁ = 1e-2
κ₀ = 1e-5
κ₁ = 1e-2
c = 5
n = 2
# In Packanowski-Philander both the viscosity and diffusivity
# depend on the Richardson number:
Ri_op = KernelFunctionOperation{Center, Center, Center}(Riᶜᶜᶜ, grid, computed_dependencies=(velocities, tracers.b))
Ri = Field(Ri_op)
ν = ν₀ + ν₁ / (1 + c * Ri^n)
κᵇ = κ₀ + κ₁ / (1 + c * Ri^(n+1))
@show ν # it's an operation...
closure = ScalarDiffusivity(VerticallyImplicitTimeDiscretization(); ν, κ=(; b=κᵇ))
# For `closure` to work correctly, the Ri must be defined as an "auxiliary field" of the model.
# Fields in model.auxiliary_fields are updated every time-stepper stage.
model = HydrostaticFreeSurfaceModel(; grid, closure, tracers=:b, buoyancy=BuoyancyTracer(), auxiliary_fields = (; Ri))
model.velocities = velocities
model.tracers = tracers
# Initial condition with Ri ≈ 0.01
step(x, c, w) = 1/2 * (1 + tanh((x - c) / w)) # smooth step function
N² = 1e-4
bᵢ(x, y, z) = N² * z
S² = 1e-2
Δu = 4 # m
uᵢ(x, y, z) = Δu * sqrt(S²) * step(z, -grid.Lz/2, Δu)
set!(model, u=uᵢ, b=bᵢ)
simulation = Simulation(model, Δt=1minute, stop_time=1day)
# Alternative to writing output.
fields = []
function grab_fields!(sim)
u, v, w = sim.model.velocities
b = sim.model.tracers.b
Ri = sim.model.auxiliary_fields.Ri
uⁿ = Array(interior(u, 1, 1, :))
vⁿ = Array(interior(v, 1, 1, :))
bⁿ = Array(interior(b, 1, 1, :))
Riⁿ = Array(interior(Ri, 1, 1, :))
push!(fields, (; t=time(sim), i=iteration(sim), u=uⁿ, v=vⁿ, b=bⁿ, Ri=Riⁿ))
return nothing
end
simulation.callbacks[:grabber] = Callback(grab_fields!, TimeInterval(10minutes))
run!(simulation)
# Plot results
z = znodes(Center, grid)
fig = Figure()
ax_u = Axis(fig[1, 1], ylabel="z (m)", xlabel="u (m s⁻¹)")
ax_b = Axis(fig[1, 2], ylabel="z (m)", xlabel="b (m s⁻²)")
ax_R = Axis(fig[1, 3], ylabel="z (m)", xlabel="Richardson number")
slider = Slider(fig[2, :], range=1:length(fields), startvalue=1)
n = slider.value
uⁿ = @lift fields[$n].u
bⁿ = @lift fields[$n].b
Riⁿ = @lift fields[$n].Ri
lines!(ax_u, uⁿ, z)
lines!(ax_b, bⁿ, z)
lines!(ax_R, Riⁿ, z)
xlims!(ax_R, 0, 5.0)
title = @lift "Diffusing shear layer at t = " * prettytime(fields[$n].t)
Label(fig[0, :], title)
display(fig)
record(fig, "pacanowski_philander_diffusion.gif", 1:length(fields), framerate=12) do nn
@info "Drawing frame $nn of $(length(fields))..."
n[] = nn
end We probably put this or something like it into the docs at some point |
Beta Was this translation helpful? Give feedback.
-
@glwagner When I started this issue, I wasn't really expecting to have Pacanowski-Philander parameterization implemented in a few hours. Kudos to you!! Can you help me to see if I understood everything correctly? Please, sorry if I am asking too-trivial questions. Why you need to define I understand that you create a fake model and don't run it, simply use it to update the auxiliary field. I don't understand why you could not do the same for the main model. For instance, you could first create the model, define Ri and closure and then
Thinking on a more general way, it would be awesome to give a general function that could depend on any field in the model, including auxiliary. I am thinking that this way it would be easier for users to create and implement their own parameterizations. Including using any model in general that could come in a form of a function. For instance, a ML model could be simply used as a turbulence closure. Again, thanks for all attention you are giving to me. As a new user, it makes me learn more and more and maybe sometime I can also retribute by contributing to the model development. |
Beta Was this translation helpful? Give feedback.
-
Thanks for asking the question! Also is it ok if I convert to discussion? If it remains an issue we might lose track of it when the issue is closed. But this might be useful to people if its more visible.
Because we need to interpolate
Yes, your question makes a lot of sense! The reason we can't do that is because type of any of the properties of julia> mutable struct Test{T}
a :: T
end
julia> t = Test(1.0)
Test{Float64}(1.0)
julia> t.a = 2.0
2.0
julia> t.a = "hi"
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Float64
Closest candidates are:
convert(::Type{T}, ::T) where T<:Number at number.jl:6
convert(::Type{T}, ::Number) where T<:Number at number.jl:7
convert(::Type{T}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250
...
Stacktrace:
[1] setproperty!(x::Test{Float64}, f::Symbol, v::String)
@ Base ./Base.jl:34
[2] top-level scope
@ REPL[6]:1 Here, Another possible solution is to build a "dummy" closure with the correct type, and then replace it with the "real" closure after constructing model by writing
I guess that's sort of what we're doing here. The main issue is reducing boilerplate / making this as easy as possible. It'd be nice to write parameterizations with more abstract syntax rather than having to write a low-level kernel function. |
Beta Was this translation helpful? Give feedback.
-
Thinking about this more, I think we could "easily" (with some code changes") support a form similar to the "discrete form" forcing functions: It'd be more of a challenge to support a form like |
Beta Was this translation helpful? Give feedback.
-
This discussion is related to PR #2580 |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
How to create field derived fields? For instance, if I want the model to calculate and save Richardson number fields.
Also, I saw that the diffusivity can be a function of x,y,z,t, can I easily give a function that depend on the Richardson number or do I need to implement it as a Turbulence closure?
Beta Was this translation helpful? Give feedback.
All reactions