-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Modified dynamics #507
Comments
I can look at it on Monday. For these types of projects I'd
Looking at the equation, if I see this right doing some maths in my head, the Laplace of the Bernoulli potential appears when you take the divergence of that equation. That one we have too in our shallow water equations. What does the first term look like if you take curl and div? Because our models are formulated with vorticity and divergence as prognostic variables one should ideally do that here too. |
The modified dynamics will monotonically increase/decrease total energy but retain its PV (PV^2, PV^3... or Casimiars). I will do some math to get the equations of vorticity and divergence, and compare these two the standard equations. Maybe their differences can be implemented as the forcing terms. |
Sorry I made a mistake. The modified equations for shallow-water model should be Eq. (4.8) from Vallis et al. 1989 JFM: where |
In vorticity-divergence formulation, Scaling of the new term would be (continuity equation is scaled with radius So if I would design that from scratch then I'd calculate the continuity equation
We need to calculate the forcing before the volume flux divergene has been calculated, which is also after the Bernoulli potential is added to the divergence tendency. So not ideal for our situation here. I therefore see 3 (actually, currently only 1) options
(while trying to implement this I realised that our forcing/drag interface currently cannot depend on
Maybe in general our forcing/drag implementation should have the option to define a callsite, e.g.
The right-hand side (RHS) is currently defined here (after the using SpeedyWeather
# avoids a bunch of `SpeedyWeather.` for non-exported functions and types
import SpeedyWeather: SurfaceVariables, forcing!, drag!, vorticity_flux!, geopotential!,
bernoulli_potential!, volume_flux_divergence!
# MODIFIED
function SpeedyWeather.dynamics_tendencies!(
diagn::DiagnosticVariablesLayer,
progn::PrognosticVariablesLayer,
surface::SpeedyWeather.SurfaceVariables,
pres::LowerTriangularMatrix, # spectral pressure/η for geopotential
time::DateTime, # time to evaluate the tendencies at
model::ShallowWater,
)
(; forcing, drag, planet, atmosphere, orography) = model
(; spectral_transform, geometry) = model
# for compatibility with other ModelSetups pressure pres = interface displacement η here
forcing!(diagn, progn, forcing, time, model) # = (Fᵤ, Fᵥ, Fₙ) forcing for u, v, η
drag!(diagn, progn, drag, time, model) # drag term for momentum u, v
# = ∇×(v(ζ+f) + Fᵤ, -u(ζ+f) + Fᵥ), tendency for vorticity
# = ∇⋅(v(ζ+f) + Fᵤ, -u(ζ+f) + Fᵥ), tendency for divergence
vorticity_flux!(diagn, model)
# (MODIFIED: MOVED UP) = -∇⋅(uh, vh), tendency for "pressure" η
volume_flux_divergence!(diagn, surface, orography, atmosphere, geometry, spectral_transform)
geopotential!(diagn, pres, planet) # geopotential Φ = gη in shallow water
# (MODIFIED: ADDED) add +αgη_t to geopotential
modified_shallow_water!(diagn, surface, planet, geometry, forcing)
bernoulli_potential!(diagn, spectral_transform) # = -∇²(E+Φ+αgη_t), tendency for divergence
end
import SpeedyWeather: AbstractPlanet, AbstractGeometry
function modified_shallow_water!(
diagn::DiagnosticVariablesLayer,
surface::SurfaceVariables,
planet::AbstractPlanet,
geometry::AbstractGeometry,
forcing::ModifiedShallowWater,
)
(; geopot) = diagn.dynamics_variables
α = forcing.α / geometry.radius # unscale with radius because pres_tend is scaled!
geopot .+= α * planet.gravity * surface.pres_tend
end I have then defined a dummy forcing that holds the Base.@kwdef struct ModifiedShallowWater{NF} <: SpeedyWeather.AbstractForcing
α::NF = 0.1 # should be a time scale?
end
ModifiedShallowWater(SG::SpectralGrid; kwargs...) = ModifiedShallowWater{SG.NF}(; kwargs...)
SpeedyWeather.initialize!(::ModifiedShallowWater,::ModelSetup) = nothing
# dummy forcing as it only holds the α for modified_shallow_water!
function SpeedyWeather.forcing!(
diagn::DiagnosticVariablesLayer,
progn::PrognosticVariablesLayer,
forcing::ModifiedShallowWater,
time::DateTime,
model::ModelSetup)
return nothing
end So that we can do spectral_grid = SpectralGrid(trunc=31,nlev=1)
modified_sw = ModifiedShallowWater(spectral_grid, α=0.1)
model = ShallowWaterModel(;spectral_grid, forcing=modified_sw)
simulation = initialize!(model)
run!(simulation) This seems to work for me for a wide range of |
I don't understand? This is the same as before, no? |
Hi @milankl, thank you very much for all these detailed explanations.
Yes, this is the same for
Hope this does not give you too many troubles. I am also expecting the GPU version of the model.
I understand that the overall integration flows are twisted. My purpose here would be too special to be generalized. I think the hacky way is OK here. Thanks for all the code snippets. Right now I can make it work.
I tried several If I want to output some new user-defined diagnostics, like mass streamfunction |
Yes, whether you call it
No no it's great that you ask because these things should be easily possible. And to be fair, I also like the hacky way because it illustrates that you can indeed just redefine parts of the simulation. Sure, you need to know where to hook in (meaning I need to tell you I guess 😉) but at least you don't have to wait for the next release or a merged pull request to start experimenting with it.
Great. I'm also thinking about a general way that one can just change the order in which terms are calculated. Imagine you could just pass on
The normal streamfunction vor = simulation.prognostic_variables.layers[1].timesteps[1].vor
R = spectral_grid.radius
Ψ = SpeedyTransforms.∇⁻²(vor) * R^2 # all gradient operators omit the radius scaling, do manually
plot(gridded(Ψ)) Do I see this right (doing some maths in my head) that you are looking for u = simulation.diagnostic_variables.layers[1].grid_variables.u_grid
v = simulation.diagnostic_variables.layers[1].grid_variables.v_grid
η = simulation.diagnostic_variables.surface.pres_grid
H = model.atmosphere.layer_thickness
Hb = model.orography.orography
h = @. η + H - Hb
uh = @. u*h
vh = @. v*h
vorh = SpeedyTransforms.curl(uh, vh) / R
Ψ = SpeedyTransforms.∇⁻²(vorh) * R^2
plot(gridded(Ψ)) For dζdx = zero(vor)
dζdy = zero(vor)
SpeedyTransforms.∇!(dζdx, dζdy, vor, model.spectral_transform)
dζdx ./= R
dζdy ./= R
ζx = gridded(dζdx)
ζy = gridded(dζdy)
RingGrids.scale_coslat⁻¹!(ζx)
RingGrids.scale_coslat⁻¹!(ζy) (this should be a convenience function like |
I try to follow your codes, and write everything into a callback, as previous global integrated diagnostics: # add callbacks
function gradient(v)
grdx = zero(v)
grdy = zero(v)
SpeedyTransforms.∇!(grdx, grdy, vor, model.spectral_transform)
grdx ./= R
grdy ./= R
∂x = gridded(grdx)
∂y = gridded(grdy)
RingGrids.scale_coslat⁻¹!(∂x)
RingGrids.scale_coslat⁻¹!(∂y)
return ∂x, ∂y
end
function extra_diagnostics(u, v, ζ, η, model)
uh = zero(u)
vh = zero(u)
ζh = zero(u)
ψh = zero(u)
δh = zero(u)
χh = zero(u)
# constants from model
H = model.atmosphere.layer_thickness
Hb = model.orography.orography
R = model.spectral_grid.radius
@. h = η + H - Hb # thickness
@. uh = u*h
@. vh = v*h
ζh = SpeedyTransforms.curl(uh, vh) / R
Ψh = SpeedyTransforms.∇⁻²(ζh) * R^2
δh = SpeedyTransforms.divergence(uh, vh) / R
χh = SpeedyTransforms.∇⁻²(δh) * R^2
vhs, uhs = gradient(Ψh)
uhd, vhd = gradient(χh)
return Ψh, -uhs, vhs, χh, uhd, vhd
end
# unpack diagnostic variables and call global_diagnostics from above
function extra_diagnostics(diagn::DiagnosticVariables, model::ModelSetup)
u = diagn.layers[1].grid_variables.u_grid
v = diagn.layers[1].grid_variables.v_grid
ζR = diagn.layers[1].grid_variables.vor_grid
η = diagn.surface.pres_grid
# vorticity during simulation is scaled by radius R, unscale here
ζ = zero(ζR)
@. ζ = ζR / diagn.scale[]
return extra_diagnostics(u, v, ζ, η, model)
end
# define a GlobalDiagnostics callback and the fields it needs
Base.@kwdef mutable struct ExtraDiagnostics <: SpeedyWeather.AbstractCallback
timestep_counter::Int = 0
time::Vector{DateTime} = []
M::Vector{Float64} = [] # mean mass per time step
C::Vector{Float64} = [] # mean circulation per time step
Λ::Vector{Float64} = [] # mean angular momentum per time step
K::Vector{Float64} = [] # mean kinetic energy per time step
P::Vector{Float64} = [] # mean potential energy per time step
Q::Vector{Float64} = [] # mean enstrophy per time step
end
# define how to initialize a GlobalDiagnostics callback
function SpeedyWeather.initialize!(
callback::ExtraDiagnostics,
progn::PrognosticVariables,
diagn::DiagnosticVariables,
model::ModelSetup,
)
# replace with vector of correct length
n = progn.clock.n_timesteps + 1 # +1 for initial conditions
callback.time = zeros(DateTime, n)
v = diagn.layers[1].grid_variables.u_grid
callback.ψh = zeros(v)
callback.χh = zeros(v)
callback.uhs = zeros(v)
callback.vhs = zeros(v)
callback.uhd = zeros(v)
callback.vhd = zeros(v)
Ψh, uhs, vhs, χh, uhd, vhd = extra_diagnostics(diagn, model)
callback.time[1] = progn.clock.time
callback.Ψh = Ψh # set initial conditions
callback.uhs = uhs # set initial conditions
callback.vhs = vhs # set initial conditions
callback.χh = χh # set initial conditions
callback.uhd = uhd # set initial conditions
callback.vhd = vhd # set initial conditions
callback.timestep_counter = 1 # (re)set counter to 1
return nothing
end
# define what a GlobalDiagnostics callback does on every time step
function SpeedyWeather.callback!(
callback::GlobalDiagnostics,
progn::PrognosticVariables,
diagn::DiagnosticVariables,
model::ModelSetup,
)
callback.timestep_counter += 1
i = callback.timestep_counter
Ψh, uhs, vhs, χh, uhd, vhd = extra_diagnostics(diagn, model)
# store current time and diagnostics for timestep i
callback.time[i] = progn.clock.time
callback.Ψh = Ψh
callback.uhs = uhs
callback.vhs = vhs
callback.χh = χh
callback.uhd = uhd
callback.vhd = vhd
end
# define how to finish a GlobalDiagnostics callback after simulation finished
function SpeedyWeather.finish!(
callback::GlobalDiagnostics,
progn::PrognosticVariables,
diagn::DiagnosticVariables,
model::ModelSetup,
)
n_timesteps = callback.timestep_counter
# create a netCDF file in current path
ds = NCDataset(joinpath(pwd(), "extra_diagnostics.nc"), "c")
# save diagnostics variables within
defDim(ds, "time", n_timesteps)
defVar(ds, "sfh", callback.time, ("time",))
defVar(ds, "uhs", callback.M, ("time",))
defVar(ds, "vhs", callback.C, ("time",))
defVar(ds, "vph", callback.Λ, ("time",))
defVar(ds, "uhd", callback.K, ("time",))
defVar(ds, "vhd", callback.P, ("time",))
close(ds)
return nothing
end There could be two problems:
I really hope I can simply add some names of diagnostics to the |
This
is the actual problem. Ideally I'd like to have a user interface that lets you add any user-defined variable easily, maybe like so output = OutputWriter(spectral_grid)
streamfunction = StreamfunctionOutput(spectral_grid, output)
add!(output, streamfunction) meaning that one would just need to define a field (or even a scalar) inside Second first
# run a simulation as normal
using SpeedyWeather
spectral_grid = SpectralGrid(trunc=31, nlev=1)
model = ShallowWaterModel(;spectral_grid);
simulation = initialize!(model);
run!(simulation, output=true)
# now load in that data
using NCDatasets
ds = NCDataset("run_0001/output.nc")
vor = Float32.(ds["vor"][:,:,end]) # vorticity on last time step with "end", Float32. to convert from Union{Missing,Float32} to Float32
# wrap into FullGaussianGrid to do analysis with SpeedyWeather
vor_grid = FullGaussianGrid(vor)
vor_spectral = spectral(vor_grid)
Ψ = SpeedyTransforms.∇⁻²(vor_spectral) * spectral_grid.radius^2 yields again (see post above). So if you load in the output as netcdf then it comes as Julia array, but you can wrap this into a grid with And first second
|
(Little feedback on best practices writing Julia functions, in this situation here function gradient(v)
grdx = zero(v)
grdy = zero(v)
SpeedyTransforms.∇!(grdx, grdy, vor, model.spectral_transform)
grdx ./= R
grdy ./= R you are using globals. Neither I can recommend https://docs.julialang.org/en/v1/manual/variables-and-scoping/ for more details! Hope this helps!!) |
You are right. I should define
That's great. I was a bit worry about writing data out because I thought this would lose some information. So I tried offline calculation as: using NCDatasets
dsO = NCDataset(path * "run_0004/output.nc")
dsN = NCDataset(path * "run_0004/extra_diagnostics.nc", "c")
defDim(dsN, "time", Inf) # unlimited time dimension
defDim(dsN, "lat", size(dsO["lat"])[1])
defDim(dsN, "lon", size(dsO["lon"])[1])
defVar(dsN, "time", Float64, ("time",),
attrib=Dict("units"=>"day", "long_name"=>"time",
"standard_name"=>"time", "calendar"=>"proleptic_gregorian"))
# coordinates of particles (the variables inside netCDF)
defVar(dsN, "lon", Float64, ("lon",), attrib =
Dict("long_name"=>"longitude", "units"=>"degrees_north"))
defVar(dsN, "lat", Float64, ("lat",), attrib =
Dict("long_name"=>"latitude", "units"=>"degrees_east"))
defVar(dsN, "vor", Float64, ("lon", "lat", "time"), attrib =
Dict("long_name"=>"vorticity", "units"=>"1/s"))
dsN["lon"][:] = dsO["lon"]
dsN["lat"][:] = dsO["lat"]
for i in 1:size(dsO["time"])[1]
u = Float64.(dsO["u"][:,:,1,i]) # vorticity on last time step with "end", Float32. to convert from Union{Missing,Float32} to Float32
v = Float64.(dsO["v"][:,:,1,i])
# wrap into FullxxxGrid to do analysis with SpeedyWeather
u_grid = FullClenshawGrid(u)
v_grid = FullClenshawGrid(v)
S = SpectralTransform(spectral_grid)
u_spec = spectral(u_grid, S)
v_spec = spectral(v_grid, S)
R = model.spectral_grid.radius
ζ_spec = SpeedyTransforms.curl(u_spec, v_spec) / R
Ψ_spec = SpeedyTransforms.∇⁻²(ζ_spec) * R^2
dsN["time"][i] = i
dsN["vor"][:, :, i] = gridded(ζ_spec, S)
NCDatasets.sync(dsN)
end
NCDatasets.close(dsN)
NCDatasets.close(dsO) Then I can compare the online and offline vorticity. But I get this: Maybe I missed something here. I do need to write another file, as I still need to analyse using other python package. I also tried windspharm to do this offline calculation in python, but it turns out I cannot reproduce the online vorticity exactly. |
I'm a bit confused why you are dealing with two nc datasets but the only thing you're missing is unscaling the coslat for the curl.
needs to have RingGrids.scale_coslat⁻¹!(u_grid)
RingGrids.scale_coslat⁻¹!(v_grid) before the transform. This is explained in the docstring help?> SpeedyWeather.curl
curl(
u::SpeedyWeather.RingGrids.AbstractGrid,
v::SpeedyWeather.RingGrids.AbstractGrid
) -> Any
Curl (∇×) of two vector components u, v on a grid. Applies 1/coslat scaling, transforms
to spectral space and returns the spectral curl, which is scaled with the radius of the
sphere. Divide by radius for unscaling.
───────────────────────────────────────────────────────────────────────────────────────
curl(
u::LowerTriangularMatrix,
v::LowerTriangularMatrix
) -> Any
Curl (∇×) of two vector components u, v of size (n+1)xn, the last row will be set to
zero in the returned LowerTriangularMatrix. This function requires both u, v to be
transforms of fields that are scaled with 1/cos(lat). An example usage is therefore
RingGrids.scale_coslat⁻¹!(u_grid)
RingGrids.scale_coslat⁻¹!(v_grid)
u = spectral(u_grid)
v = spectral(v_grid)
vor = curl(u, v)
vor_grid = gridded(div) |
The first is output from the model integration. I just want to output extra diagnostics to another nc file (hope you could refactor the output stream so this second file can be easily merged into the first one during integration). Right now I am testing if the vorticity can be reconstructed exactly. Then I will output streamfunction etc. to the second nc file.
I should have thought that, because it is so close (similar pattern). |
Yes, you can visually see that there's a coslat scaling applied dampening the signals towards the poles 😉 |
The time step is automatically chosen depending on resolution. At T31 it's time_stepping = Leapfrog(spectral_grid, Δt_at_T31 = Minute(15))
model = ShallowWaterModel(;spectral_grid, time_stepping) |
From the JOSS paper, I see that "operators used inside SpeedyWeather.jl are exposed to the user, facilitating analysis of the simulation data". Also, the slogan is "Play atmospheric modelling like it's LEGO".
Now I would like to try to modified the dynamics of shallow-water equations, hoping the design of SpeedyWeather could help on my own purpose. The paper by Theodore Shepherd provide a method of finding extremal states given an initial one. For shallow-water case [Eq. 3.26a, b],
the modified dynamics is Eq. (3.26a'): one would get extremal state through integrating Eq. (3.26a').
To implement this using speedy, I guess I need to:
\zeta_spec = curl(u_spec, v_spec)
;prod_spec = prod(A_spec, B_spec)
;Not sure if this is enough but we can add more. I know that for spectral model, the first two can be done straighforwardly. I just need a doc to do this. The last two should follow the doc here. But I still need some code examples to really give a try. I could also contribute to the docs once this is done, and if you guys feel it is necessary.
The text was updated successfully, but these errors were encountered: