-
Notifications
You must be signed in to change notification settings - Fork 9
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
Add background potential energy computation #172
base: main
Are you sure you want to change the base?
Conversation
@jbisits thanks this is great! I'll only have time to look at this more carefully next week, but a couple of comments that I'll say right now are:
A quick note is that I'm surprised that a |
I'm a little confused about the sorting. Don't you want to sort only in the vertical? In the example you posted, the sorted field not only has a vertical buoyancy gradient but also a left-right buoyancy gradient which seems arbitrary... Also, you may just want to extend |
Probably Personally I think that I think we should be able to support this without too much difficulty, provided we only support dimension-by-dimension sorting. If we do actually need to support sorting of Fields that are reshaped, we have a bit more work to do with |
A little bit more about that... consider julia> grid = RectilinearGrid(size=(1, 2, 3), x=(0, 1), y=(0, 1), z=(0, 1))
1×2×3 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo
├── Periodic x ∈ [0.0, 1.0) regularly spaced with Δx=1.0
├── Periodic y ∈ [0.0, 1.0) regularly spaced with Δy=0.5
└── Bounded z ∈ [0.0, 1.0] regularly spaced with Δz=0.333333
julia> c = CenterField(grid)
1×2×3 Field{Center, Center, Center} on RectilinearGrid on CPU
├── grid: 1×2×3 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Periodic, east: Periodic, south: Periodic, north: Periodic, bottom: ZeroFlux, top: ZeroFlux, immersed: ZeroFlux
└── data: 7×8×9 OffsetArray(::Array{Float64, 3}, -2:4, -2:5, -2:6) with eltype Float64 with indices -2:4×-2:5×-2:6
└── max=0.0, min=0.0, mean=0.0
julia> rc = reshape(c, :)
6-element reshape(::Field{Center, Center, Center, Nothing, RectilinearGrid{Float64, Periodic, Periodic, Bounded, Float64, Float64, Float64, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64}}, CPU}, Tuple{Colon, Colon, Colon}, OffsetArrays.OffsetArray{Float64, 3, Array{Float64, 3}}, Float64, FieldBoundaryConditions{BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Periodic, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}}, Nothing, Oceananigans.Fields.FieldBoundaryBuffers{Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing, Nothing}}, 6) with eltype Float64:
0.0
0.0
0.0
0.0
0.0
0.0
julia> @which sort(rc)
sort(v::AbstractVector; kws...)
@ Base.Sort sort.jl:1489 This calls But julia> Base.copymutable(rc)
6-element Vector{Float64}:
0.0
0.0
0.0
0.0
0.0
0.0 because we haven't defined help?> Base.ReshapedArray
No documentation found.
Summary
≡≡≡≡≡≡≡
struct Base.ReshapedArray{T, N, P<:AbstractArray, MI<:Tuple{Vararg{Base.MultiplicativeInverses.SignedMultiplicativeInverse{Int64}}}} Supporting sort!(f::Field; kw...) = sort!(interior(f); kw...) Note this also works on GPU. For sort(f::Field; kw...) = sort!(copy(f); kw...) (we need to check that Then, if we don't need to support |
Thanks for the comments and suggestions @tomchor and @glwagner! @glwagner that does look like a cleaner (and better) implementation for
Yes the horizontal buoyancy gradient that appears in the figures is arbitrary but part of that arises from |
Co-authored-by: Tomas Chor <tomaschor@gmail.com>
No problem! In response to the questions above:
|
@jbisits did you try sort!(f::Field; kw...) = sort!(interior(f); kw...)
sort(f::Field; kw...) = sort!(copy(f); kw...) ?
But wouldn't any horizontal structure be destroyed if we resort in the horizontal? Consider doing this for the global ocean. It's cold at the poles and warm on the equator. Resorting in the horizontal would put cold water at the north pole and warm water at the south pole. That doesn't make sense. We must be assuming horizontal homogeneity. Is this necessary? |
Not yet I will get to it soon.
I am not sure if it matters that horizontal structure is not maintained after resorting (sorry if I am not understanding something here) because using the sorted array we want to attain an approximation to the minimal potential energy state which can be attained through adiabatic rearrangement of fluid parcels (or volume elements in Boussinesq case). For your example above, if we first reshape a 3D density field for the global ocean into a 1D array and then sort it we would see the lighter, warmer water around the equator at the start of the sorted array and the deep, dense bottom water at the end end of the sorted array (with other water masses in between). Redistributing this sorted array throughout the original grid would then have the lightest waters (e.g. from the around the equator) at the top of the grid As you point out this could potentially result in warm water from the equator at the poles which does not make sense but I am not sure if that matters as we just want the lightest water on top to give a minimal potential energy state. In the case of some experiments I have been running (more along direct numerical simulation lines) I would like to be able first reshape a 3D density field into a 1D array, sort this, then redistribute throughout the original grid. We could add a keyword argument that only sorts over the vertical dimension of the domain though? So we would have two cases one that sorts the reshaped array and redistributes over the grid function sort!(f::Field; vertical_only = false, kw...)
sorted_1D_field = reshape(interior(f), :)
sort!(sorted_1D_field)
f.data = reshape(sorted_1D_field, size(interior(f)) # I know this wont work because data has the halos is different size is immutable but this is the idea
return f
end and another that just sorts over the vertical dimension (assuming vertical dimension is 3 in example below) of the domain sort!(f::Field; vertical_only = true, kw...) = sort!(interior(f), dims = 3; kw...) Again sorry if I have misunderstood something but this is how I have been thinking about the BPE and its implementation for experiments I have been running. |
It's easy to construct a counter example. If we simply flip the definition of the y coordinate (so that increasing j-index implies decreasing latitude), we end up with a different background energy state. But there's only one physical background energy state --- right? More severely I think the background energy state cannot be a function of horizontal position. It can only be a function of Possibly the solution is as simple as averaging the sorted state in the horizontal directions. I'm not quite sure though. I think we are making additional assumptions. For example, it seems like we are assuming a regular grid (both vertical and horizontal). I think we need to account for the differing grid volumes to define the sorted background state in a way that conserves mass... It's also important to understand that this only works because of the indexing convention that we use. In other words there is an implicit assumption in the code that the z-index is the last index. Somehow, it might be nicer to write the code so that this assumption is either more clear or so that the code would still work if the z-index were 1st or 2nd. |
As far as I understand, the physical background state is indeed unique. It necessarily has no horizontal buoyancy gradients. But we're only approximating that numerically, and the "numerical" background state doesn't need to be unique. In fact, like @glwagner pointed out, just flipping one of the horizontal directions would produce a different state. I think that's the best we can do numerically and that's what winters et al suggest in their papers. I think averaging the horizontal directions would go against the premise of the BPE, which is an adiabatic resorting of the buoyancy field by definition. Usually though the result of the sorting is better than what's pictured here and the horizontal gradients are quite small (the original paper gives estimates for when this is a good approximation). But since @jbisits is using white noise here as an example, the approximation isn't very good and the horizontal gradients are more pronounced than they'd normally be. |
In the Winters papers, the background state is only a function of z and not of horizontal directions. Shouldn't our background state also be 1D then? The solution is fairly easy in the uniform grid case --- we just average the sorted field in the horizontal directions. For stretched grids we need to sort more carefully, taking into account the cell volumes. I don't think this is too difficult though and we have |
In the physical world the background state is a function of Your point of forcing the numerical, discrete background state to also be a function of
I feel like the equation above will be less performant (I think it'll always scale with O( @glwagner thoughts? |
I'm not suggesting to change the sorting. I'm saying the background state should be derived by
This algorithm produces identical background states even if the horizontal coordinate direciton is reversed for example. I'm hazy on the relationship between the sorted field and |
Just a note that @ikeshwani has been independently computing BPE, for now just by brute-force offline for the non-immersed case, but eventually including the We'll be keeping a close eye on these developments and hope to contribute to generalizations in these more complicated contexts. |
Still need a test for checking z*
@jbisits, have you tested the new diagnostics on anything more complicated than a 1D profile and a random 2D field? @ikeshwani implemented a nice notebook that qualitatively replicates the Winters et al. (1995) shear instability example, which you might find useful. |
Not the diagnostics in this PR (I have used It could be a good addition to the documentation once this PR is finished if @ikeshwani is interested? |
Agreed! To me it sounds like it'd be great to merge @ikeshwani's notebook with the Kelvin-Helmholtz example that's already in the docs. That example performs some very simple diagnostics, and it would be great to use that to reproduce results from Winters et al. (1995) in addition to what's already there. |
So that `z✶` increases in the same direction as `z`.
Also rename functions to `minus_bz✶_ccc` to try and make clearer what they are doing
I've finally caught up on this whole thread and want to clarify a few things: Regarding approximations
@tomchor, could you clarify what the approximation being made is? My understanding is that the method is only approximate for equations of state that depend on pressure, and are exact for linear EOS or a buoyancy tracer. Some negligible error might be introduced if the discretization method does not align perfectly with the model's grid discretization, but that is a fundamentally different thing than saying we are making a physical approximation to the Boussinesq BPE. (Or maybe you are saying that Boussinesq BPE is already an approximate to non-Boussinesq BPE?) But the physics of our diagnostics should be consistent with our model physics. Regarding irregular grids
@tomchor, the sorting method is easily generalized to account for unequal grid cell volumes; you just have to account for the actual volume of a parcel when you stack up the elevation increments to compute Regarding global sorting, the sorted buoyancy field
|
I've implemented a very efficient offline algorithm for computing Two modifications to the algorithm are required to generalize the method to support immersed boundaries:
|
Taking a step back @tomchor, @glwagner, @jbisits: Why do we actually care about the sorted buoyancy field or the "background state" as a function of physical space (i.e. as a 3D/4D array)? We never actually use that to calculate any terms in the BPE or APE budgets, as far as I can tell. All we ever need is Am I missing something? (Feel free to suggest any additional calculations to make this discussion more concrete.) |
At the time we were planning to do a simple 1D sorting, and the reshape everything back to 3D, exactly like described in the original paper. This would incur horizontal gradients, which is what I was talking about. Since then, after discussion, we decided to do a 1D sorting only the vertical and flatten the cell volumes, without any 3D reshaping (you can follow the previous comments for more details). With this, there's no further approximating other than the ones you mention.
Agreed. Again, this comment was before we decided to do a 1D sorting along with cell flattening. Note that we still probably want to implement the expensive |
I don't think that's true. I've already implemented a sorting-based method for immersed boundaries (see my notebook linked above) and I think the method could be generalized to tilted domains but that one is more complicated because of the background buoyancy field and boundary conditions.
I meant that the sorting-based method scales like whatever |
@@ -169,9 +143,9 @@ end | |||
|
|||
grid = model.grid | |||
C = model.tracers | |||
b = buoyancy_model.model | |||
b_model = buoyancy_model.model |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This line doesn't read well. But I also don't have immediate suggestions on how to improve it... irrc the whole nomenclature of BuoyancyModel
is kinda confusing and we discussed changing it over at Oceananigans a few times.
Maybe changing it from buoyancy_model
to buoyancy_treatment
might help...?
function OneDReferenceField(f::Union{Field, AbstractOperation}; rev = false) | ||
|
||
area = sum(AreaField(f.grid)) | ||
volume_field = VolumeField(f.grid) | ||
v = reshape(interior(volume_field), :) | ||
field_data = reshape(interior(f), :) | ||
|
||
p = sortperm(field_data; rev) | ||
sorted_field_data = field_data[p] | ||
z✶ = -cumsum(v[p]) / area # divide by area at surface | ||
|
||
grid_arch = f.grid.architecture | ||
grid_size = prod(size(f.grid)) | ||
new_grid = RectilinearGrid(grid_arch, size = grid_size, z = (-f.grid.Lz, 0), topology=(Flat, Flat, Bounded)) | ||
|
||
sorted_field = CenterField(new_grid) | ||
set!(sorted_field, reshape(sorted_field_data, size(new_grid))) | ||
z✶_field = CenterField(new_grid) | ||
set!(z✶_field, reshape(z✶, size(new_grid))) | ||
|
||
return sorted_field, z✶_field | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the core of the PR, right? It looks correct to me, but we should make sure to test that the area/volume calculations are correct with a stretched grid.
It seems to me that this works for horizontally-stretched grids as well, correct?
b_sorted = b[p] | ||
|
||
v_grid = VolumeField(model.grid) | ||
A = sum(AreaField(model.grid)) | ||
z✶_check = -cumsum(reshape(v_grid, :)[p]) / A | ||
|
||
sorted_buoyancy, z✶ = OneDReferenceField(model.tracers.b, rev = true) | ||
|
||
@test isequal(reshape(interior(sorted_buoyancy), :), b_sorted) | ||
@test isequal(reshape(interior(z✶), :), z✶_check) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When comparing b_sorted
and sorted_buoiyancy
, it's always gonna return true by construction, right? Since b_sorted
is calculated with the permutations that are used to calculate sorted_buoyancy
? I think we should still keep this test (since it tests that the code is working and all), but maybe we need another one that's a little more rigorous.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would starting with linear buoyancy distribution in the x
direction (where we know db/dx
) and sorting it, making sure that db/dz
matches, work? We could try that for grids that are stretched in the horizontal and vertical directions separately. If we could test something along those lines it would be great.
elseif model.buoyancy isa BuoyancyLinearEOSModel | ||
|
||
b = Field(linear_eos_buoyancy(model.grid, model.buoyancy.model, model.tracers)) | ||
compute!(b) | ||
b_flat = reshape(interior(b), :) | ||
p = sortperm(b_flat, rev = true) | ||
b_sorted = b_flat[p] | ||
|
||
v_grid = VolumeField(model.grid) | ||
A = sum(AreaField(model.grid)) | ||
z✶_check = -cumsum(reshape(v_grid, :)[p]) / A | ||
|
||
sorted_buoyancy, z✶ = OneDReferenceField(b, rev = true) | ||
|
||
@test isequal(reshape(interior(sorted_buoyancy), :), b_sorted) | ||
@test isequal(reshape(interior(z✶), :), z✶_check) | ||
|
||
elseif model.buoyancy isa BuoyancyBoussinesqEOSModel | ||
|
||
ρ = Field(seawater_density(model; geopotential_height)) | ||
compute!(ρ) | ||
ρ_flat = reshape(interior(ρ), :) | ||
p = sortperm(ρ_flat) | ||
ρ_sorted = ρ_flat[p] | ||
|
||
v_grid = VolumeField(model.grid) | ||
A = sum(AreaField(model.grid)) | ||
z✶_check = -cumsum(reshape(v_grid, :)[p]) / A | ||
|
||
sorted_density, z✶ = OneDReferenceField(ρ) | ||
|
||
@test isequal(reshape(interior(sorted_density), :), ρ_sorted) | ||
@test isequal(reshape(interior(z✶), :), z✶_check) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The comment above extends to these other model.buoyancy
types. A separate comment here is that you're writing a lot of code 3 times. You can probably reuse code, so that you write most things only once.
I think indeed if we can get what we need without explicitly computing @jbisits you're the one implementing this; what do you think? |
Ah, yes, I just now saw that comment. I'll look into it later.
Agreed, if we can get the sorting method to work with immersed boundaries and tilted domains we don't need/want the brute-force approach of a double-loop heaviside iteration. |
I am sorry I have been slow in replying to the recent comments. I will be able to get back to this around the middle of this coming week - my apologies! |
Sorry to have fallen asleep on this PR! The way I was implementing this was to define a function OneDReferenceField(f::Union{Field, AbstractOperation}; rev = false)
area = sum(AreaField(f.grid))
volume_field = VolumeField(f.grid)
v = reshape(interior(volume_field), :)
field_data = reshape(interior(f), :)
p = sortperm(field_data; rev)
sorted_field_data = field_data[p]
z✶ = -cumsum(v[p]) / area # divide by area at surface
grid_arch = f.grid.architecture
grid_size = prod(size(f.grid))
new_grid = RectilinearGrid(grid_arch, size = grid_size, z = (-f.grid.Lz, 0), topology=(Flat, Flat, Bounded))
sorted_field = CenterField(new_grid)
set!(sorted_field, reshape(sorted_field_data, size(new_grid)))
z✶_field = CenterField(new_grid)
set!(z✶_field, reshape(z✶, size(new_grid)))
return sorted_field, z✶_field
end It looks like Further, for this to work on grids that do not have uniform horizontal area I think the area at each depth of the water column still needs to be calculated (as pointed out here). With that in mind is there a way that @hdrake's method for non-uniform horizontal area to be put into a GPU kernel? I am happy to look into this if that is something that will give the best results. @tomchor depending on what you think about the method above it may also be worth closing this PR and implementing any new changes/different methods in a new PR (maybe with this PR converted to an issue though have not looked into if that can be done). Or I can try to improve on what is already in place in this PR --- maybe the hiatus from this PR will have helped! I will leave everything as is until a decision is made. Again sorry to have let this go! |
@jbisits don't worry about it! We all have lots to do we have to prioritize :)
I don't think this can be implemented using
It does look like @hdrake's method would give the best results. I haven't looked at that method in a while (so I'll let @hdrake chime in here) but if this PR has something that already works (even though it's limited to regular grids and you're not 100% happy with the main function) I'd say it's okay to merge. Then we can improve this design later or even replace it if we think it's necessary. Personally, I think having something you can already use helps to visualize better ways of doing things. |
Ok sounds good. I will try to get this cleaned up and checked over the next week or so! |
This PR adds functions to compute the
BackgroundPotentialEnergy
, BPE, (see #168 for discussion on function naming) during asimulation
. It does so using the definition given in Winters et al. (1995),To compute$E_{b}$ the buoyancy/density $z^{*}$ (for a density
Field
from a model is first reshaped into a 1DArray
and then sorted. TheField
) is defined as,To compute$z^{*}$ , each volume element from the
model.grid
is computed, reshaped into a 1DArray
, sorted using thesortperm
from the buoyancy/density sorting, cumulatively summed then divided by the horizontal area of themodel.grid
.These computations are done in$z^{*}$ $E_{b}$ defined above.
OneDReferenceField
which returns two new fields, the sorted buoyancy/densityField
and theField
. These returnedField
s are on a grid with a vertical extent equal to the originalmodel.grid
but with total number of elements equal to the number of points in the domain (i.e.model.grid.Nx * model.grid.Ny * model.grid.Nz
). These two newField
s are then used to compute the BPE per unit volume. CallingIntegral(Field(BackgroundPotentialEnergy))
will give the volume integratedI have handled the sorting of buoyancy and density separately because in the case of buoyancy, the buoyancy in the sorted profile should decrease as$z*$ increases,
whereas the density should increase as$z*$ increases. If this is not clear (or wrong!) please let me know!
I have not tried this using a
GPU
but I think that these functions (reshape
andsort
) work withCuArray
s but I might have some scalar indexing that will cause problems --- when I get a chance to I will try on aGPU
.The tests I have handled are really only validations that
OneDReferenceField
is computing things and returning things in the correct way. If you have any other suggestions for tests let me know.Examples of the sorting for buoyancy and density using random noise as input data are below (they should be able to be run provided this branch of Oceanostics.jl is being used).
cc @janzika