-
Notifications
You must be signed in to change notification settings - Fork 210
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
Introduces SumOfFields
#3676
Introduces SumOfFields
#3676
Conversation
How do we know that |
It might make sense to take a step back and ask what you're trying to accomplish because I'm not quite sure this makes sense. It might work for you, but seems to rely on hidden assumptions about |
Hmm yeah, that is true, we never make any guarantees about what is in a In the same way there is no check on what e.g. biogeochemical_auxiliary_fields` is returning so it could return just a normal array which would also break at this step. I just rearranged it a bit so it just runs |
Well no --- we don't trust developers to do that. We only use it inside a kernel and in a case we know it is used correctly. This PR does something different, it introduces "support" for It is interesting to consider putting together an abstraction that represents a sum of fields (though note we already have this through |
Also with |
Okay, I understand your point. Since it's not part of the public API I wasn't thinking that this suggests to a user that it should be used, but more made it possible for developers to do things like what my other PR does. What I meant about trusting users for auxiliary fields is that you can do e.g. this: model = NonhydrostaticModel(; grid, auxiliary_fields=(; A = randn(1, 2)) and it doesn't error, and the only reason the same for a So this change wouldn't affect how the model failed because putting arrays etc. into the sum would do the same thing. But yeah, having a |
mask_immersed_field!
for SumOfArrays
SumOfFields
I've tried adding it now. I put it in the fields module since it is an |
|
||
adapt_structure(to, sum::SumOfFields{N}) where N = SumOfFields{N}((adapt_structure(to, field) for field in sum.fields)...) | ||
|
||
summary(::SumOfFields{N, LX, LY, LZ}) where {N, LX, LY, LZ} = "Sum of $N fields at ($LX, $LY, $LZ)" |
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 summary does not state the name of the type (SumOfFields
). The purpose of summary
and show
is to give information about the object so I think it's important to state the name.
Also the summary
should be more uniform with other field summaries
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.
I suggest this
function Base.summary(field::SumOfFields{N}) where N
LX, LY, LZ = location(field)
prefix = string(size_summary(size(field)), " SumOfFields{$LX, $LY, $LZ} with $N fields")
reduced_dims = reduced_dimensions(field)
suffix = reduced_dims === () ?
string(" on ", grid_name(field), " on ", summary(architecture(field))) :
string(" reduced over dims = ", reduced_dims,
" on ", grid_name(field), " on ", summary(architecture(field)))
return string(prefix, suffix)
end
This is plagiarized from summary
for Fields
.
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.
There's also a nice show
method for NamedTuple of Fields that could inspire a show
method for SumOfFields
@propagate_inbounds function getindex(s::SumOfFields{N, LX, LY, LZ, G, T, F}, i...) where {N, LX, LY, LZ, G, T, F} | ||
first = getindex(SumOfFields{3, F, LX, LY, LZ, G, T}((s.fields[1], s.fields[2], s.fields[3]), s.grid), i...) | ||
last = getindex(SumOfFields{N - 3, F, LX, LY, LZ, G, T}(s.fields[4:N], s.grid), i...) | ||
return first + last |
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.
write this on more lines please.
as a rule, try not to nest function calls within one another. Sometimes one-argument functions can be ok. Here though, the inner call has 7 type parameters and two arguments while the outer call has two arguments.
end | ||
|
||
return nothing | ||
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.
All mask_immersed_field
belong in the immersed boundaries module
It might have been better to have a new PR for this because the conversation will be hard to follow, but we can continue. We need a mission statement for |
Doesn't this suggest there is a problem with |
Why do we want to support arrays in The main usage for |
Co-authored-by: Gregory L. Wagner <wagner.greg@gmail.com>
grid = first(fields).grid | ||
loc = location(first(fields)) | ||
|
||
all(f.grid == grid for f in fields) || |
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.
all(f.grid == grid for f in fields) || | |
all(f.grid === grid for f in fields) || |
I think object equality is appropriate here. And below. (This will be faster because we do not have to perform numeric comparison)
end | ||
end | ||
|
||
adapt_structure(to, sum::SumOfFields{N}) where N = SumOfFields{N}((adapt_structure(to, field) for field in sum.fields)...) |
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.
please write this on more lines
So I started making the suggested changes but then decided to do more benchmarking and it appears that when I interpolate the sums into the benchmark properly there isn't a difference (i.e. https://juliaci.github.io/BenchmarkTools.jl/stable/manual/#Interpolating-values-into-benchmark-expressions): julia> @btime sof[1, 2, 3]
45.412 ns (1 allocation: 16 bytes)
-1.0065959327459124
julia> @btime bo[1, 2, 3]
44.571 ns (1 allocation: 16 bytes)
-1.0065959327459124
julia> @btime fbo[1, 2, 3]
238.209 ns (1 allocation: 16 bytes)
-1.0065959327459124
julia> @btime $(sof)[1, 2, 3]
3.958 ns (0 allocations: 0 bytes)
-1.0065959327459124
julia> @btime $(bo)[1, 2, 3]
2.708 ns (0 allocations: 0 bytes)
-1.0065959327459124
julia> @btime $(fbo)[1, 2, 3]
2.666 ns (0 allocations: 0 bytes)
-1.0065959327459124 where julia> sof = SumOfFields{3}(c[1:3]...)
200×200×200 SumOfFields{3, Center, Center, Center} on RectilinearGrid on CPU
└── grid: 200×200×200 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo
julia> bo = sum(c[1:3])
BinaryOperation at (Center, Center, Center)
├── grid: 200×200×200 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo
└── tree:
+ at (Center, Center, Center)
├── + at (Center, Center, Center)
│ ├── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU
│ └── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU
└── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU
julia> fbo = c[1] + c[2] + c[3]
MultiaryOperation at (Center, Center, Center)
├── grid: 200×200×200 RectilinearGrid{Float64, Periodic, Periodic, Bounded} on CPU with 3×3×3 halo
└── tree:
+ at (Center, Center, Center)
├── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU
├── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU
└── 200×200×200 Field{Center, Center, Center} on RectilinearGrid on CPU To check that the interpolation into the benchmark wasn't important I also tried benchmarking a kernel that fills an array with the sum and got these results: julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, $(A), $(sof))
20.066 ms (21 allocations: 15.66 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, $(A), $(bo))
20.174 ms (21 allocations: 19.27 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, $(A), $(fbo))
1.286 s (21 allocations: 15.66 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, $(A), $(fbo))
1.269 s (21 allocations: 15.66 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, A, sof)
20.074 ms (20 allocations: 13.67 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, A, bo)
20.167 ms (20 allocations: 16.77 KiB)
julia> @btime launch!(grid.architecture, grid, :xyz, _fill_with_sum!, A, fbo)
1.289 s (20 allocations: 13.67 KiB) So in conclusion |
Ok. The case where I think |
I'm now having the problem that if I return a This fallback:
is already there but since we get there via: Oceananigans.jl/src/ImmersedBoundaries/mask_immersed_field.jl Lines 9 to 10 in bf767af
it fails |
Should we extend |
We don't want a fallback I don't think --- the failure is intentional. We want to know when we need to define a new method |
Masking an operation might be a bit tough because we do not know "a priori" what is the neutral value and most likely there are many ways to "mask" an operation by acting on the memory in the operands. How would this look like practically? |
There are a few ways it could be implemented. One simple way is function mask_immersed_field(bop::BinaryOperation, value=zero(b.grid))
mask_immersed_field(bop.a, value)
mask_immersed_field(bop.b, value)
return nothing
end One could also be more specific, eg const FieldBinaryOperation = BinaryOperation{<:Any, <:Any, <:Any, <:Any, <:Field, <:Field} Then we are sure its a binary operation between two fields. And the operation can be restricted to addition as well. Just some ideas... |
Okay, that all makes sense. For
but otherwise, I guess would work. Perhaps if we only do this for #fallback for `+` and `-`, I guess for `*` too since `a` could just be a number so if we only masked `a` it wouldn't be correct
function mask_immersed_field(bop::BinaryOperation, value=zero(b.grid))
mask_immersed_field(bop.a, value)
mask_immersed_field(bop.b, value)
return nothing
end
function mask_immersed_field(bop::BinaryOperation{<:Any, <:Any, <:Any, typeof(/)}, value=zero(b.grid))
mask_immersed_field(bop.a, value)
return nothing
end
function mask_immersed_field(bop::BinaryOperation{<:Any, <:Any, <:Any, typeof(^)}, value=zero(b.grid))
mask_immersed_field(bop.a, value)
mask_immersed_field(bop.a, one(b.grid))
return nothing
end Which would make the values correct for all binary operations I think? |
I'm ok to try this out, hopefully we won't run into issues |
For |
This PR adds a method to
mask_immersed_field!
forSumOfArrays
. I was trying to make a model which has an auxiliary field which is aSumOfArrays
but ran into the error that all of the prognostic and auxiliary fields in theHydrostaticFreeSurfaceModel
get masked during theupdate_state!
step.(Ran into the issue here: OceanBioME/OceanBioME.jl#195)