Skip to content
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

Allow users to mask output #3092

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/Grids/inactive_node.jl
Original file line number Diff line number Diff line change
Expand Up @@ -130,4 +130,3 @@ lies on the boundary between inactive and active cells in a `Bounded` direction.
Return `true` when the location `(LX, LY, LZ)` lies on a boundary.
"""
@inline boundary_node(i, j, k, grid, LX, LY, LZ) = peripheral_node(i, j, k, grid, LX, LY, LZ) & !inactive_node(i, j, k, grid, LX, LY, LZ)

4 changes: 2 additions & 2 deletions src/ImmersedBoundaries/ImmersedBoundaries.jl
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ We then have

* `inactive_node(i, 1, 1, grid, f, c, c) = false`

As well as
as well as

* `inactive_node(i, 1, 1, grid, c, c, c) = false`
* `inactive_node(i-1, 1, 1, grid, c, c, c) = true`
Expand Down Expand Up @@ -303,7 +303,7 @@ include("partial_cell_bottom.jl")
include("conditional_fluxes.jl")
include("immersed_boundary_condition.jl")
include("conditional_differences.jl")
include("mask_immersed_field.jl")
include("mask_immersed.jl")
include("immersed_reductions.jl")

# Extension to distributed Immersed boundaries
Expand Down
58 changes: 58 additions & 0 deletions src/ImmersedBoundaries/mask_immersed.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
using KernelAbstractions: @kernel, @index
using Statistics
using Oceananigans.Architectures: architecture
using Oceananigans.Fields: location, ZReducedField, Field

instantiate(T::Type) = T()
instantiate(t) = t

mask_immersed!(field, grid, loc, value) = nothing
mask_immersed!(field::Field, value=zero(eltype(field.grid))) =
mask_immersed!(field, field.grid, location(field), value)

"""
mask_immersed!(field::Field, grid::ImmersedBoundaryGrid, loc, value)

Mask `field` defined on `grid` with a `value` at `loc`ations at grid points where [`peripheral_node`](@ref)
is to `true`.
"""
function mask_immersed!(field::Field, grid::ImmersedBoundaryGrid, loc, value)
arch = architecture(field)
loc = instantiate.(loc)
launch!(arch, grid, :xyz, _mask_immersed!, field, loc, grid, value)
return nothing
end


@kernel function _mask_immersed!(field, loc, grid, value)
i, j, k = @index(Global, NTuple)
@inbounds field[i, j, k] = scalar_mask(i, j, k, grid, grid.immersed_boundary, loc..., value, field)
end

mask_immersed_horizontal!(field, args...; kw...) = nothing
mask_immersed_horizontal!(::Nothing, args...; kw...) = nothing
mask_immersed_horizontal!(field, value=zero(eltype(field.grid)); k, mask = peripheral_node) =
mask_immersed_horizontal!(field, field.grid, location(field), value; k, mask)

"""
mask_immersed_horizontal!(field::Field, grid::ImmersedBoundaryGrid, loc, value; k, mask)

Mask `field` on `grid` with a `value` on the slices `[:, :, k]` where `mask` is `true`.
"""
function mask_immersed_horizontal!(field::Field, grid::ImmersedBoundaryGrid, loc, value; k, mask)
arch = architecture(field)
loc = instantiate.(loc)
return launch!(arch, grid, :xy, _mask_immersed_horizontal!, field, loc, grid, value, k, mask)
end

@kernel function _mask_immersed_horizontal!(field, loc, grid, value, k, mask)
i, j = @index(Global, NTuple)
@inbounds field[i, j, k] = scalar_mask(i, j, k, grid, grid.immersed_boundary, loc..., value, field, mask)
end

#####
##### Masking for GridFittedBoundary
#####

@inline scalar_mask(i, j, k, grid, ::AbstractGridFittedBoundary, LX, LY, LZ, value, field, mask=peripheral_node) =
@inbounds ifelse(mask(i, j, k, grid, LX, LY, LZ), value, field[i, j, k])
59 changes: 0 additions & 59 deletions src/ImmersedBoundaries/mask_immersed_field.jl

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ end

function local_compute_integrated_volume_flux!(∫ᶻQ, velocities, arch)

foreach(mask_immersed_field!, velocities)
foreach(mask_immersed!, velocities)

# Compute barotropic volume flux. Blocking.
compute_vertically_integrated_volume_flux!(∫ᶻQ, velocities)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,13 +299,14 @@ function split_explicit_free_surface_step!(free_surface::SplitExplicitFreeSurfac
set!(free_surface.η, free_surface.state.η̅)
end


fields_to_fill = (free_surface.state.U̅, free_surface.state.V̅)
fill_halo_regions!(fields_to_fill; async = true)

# Preparing velocities for the barotropic correction
@apply_regionally begin
mask_immersed_field!(model.velocities.u)
mask_immersed_field!(model.velocities.v)
mask_immersed!(model.velocities.u)
mask_immersed!(model.velocities.v)
end

return nothing
Expand Down Expand Up @@ -381,6 +382,11 @@ function setup_free_surface!(model, free_surface::SplitExplicitFreeSurface, χ)
Guⁿ = model.timestepper.Gⁿ.u
Gvⁿ = model.timestepper.Gⁿ.v

mask_immersed!(Gu⁻)
mask_immersed!(Gv⁻)
mask_immersed!(Guⁿ)
mask_immersed!(Gvⁿ)

auxiliary = free_surface.auxiliary

@apply_regionally setup_split_explicit_tendency!(auxiliary, free_surface_grid, Gu⁻, Gv⁻, Guⁿ, Gvⁿ, χ)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ using Oceananigans.BoundaryConditions
using Oceananigans: UpdateStateCallsite
using Oceananigans.Biogeochemistry: update_biogeochemical_state!
using Oceananigans.TurbulenceClosures: compute_diffusivities!
using Oceananigans.ImmersedBoundaries: mask_immersed_field!, mask_immersed_field_xy!, inactive_node
using Oceananigans.ImmersedBoundaries: mask_immersed!, mask_immersed_horizontal!, inactive_node
using Oceananigans.Models.NonhydrostaticModels: update_hydrostatic_pressure!, p_kernel_parameters
using Oceananigans.Fields: replace_horizontal_vector_halos!

Expand Down Expand Up @@ -59,10 +59,10 @@ function mask_immersed_model_fields!(model, grid)

foreach(fields_to_mask) do field
if field !== η
mask_immersed_field!(field)
mask_immersed!(field)
end
end
mask_immersed_field_xy!(η, k=size(grid, 3)+1, mask = inactive_node)
mask_immersed_horizontal!(η, k=size(grid, 3)+1, mask = inactive_node)

return nothing
end
Expand Down
2 changes: 1 addition & 1 deletion src/Models/NonhydrostaticModels/pressure_correction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Calculate the (nonhydrostatic) pressure correction associated `tendencies`, `vel
function calculate_pressure_correction!(model::NonhydrostaticModel, Δt)

# Mask immersed velocities
foreach(mask_immersed_field!, model.velocities)
foreach(mask_immersed!, model.velocities)

fill_halo_regions!(model.velocities, model.clock, fields(model))

Expand Down
4 changes: 2 additions & 2 deletions src/Models/NonhydrostaticModels/set_nonhydrostatic_model.jl
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ function set!(model::NonhydrostaticModel; enforce_incompressibility=true, kwargs
end

# Apply a mask
foreach(mask_immersed_field!, model.tracers)
foreach(mask_immersed_field!, model.velocities)
foreach(mask_immersed!, model.tracers)
foreach(mask_immersed!, model.velocities)
update_state!(model)

if enforce_incompressibility
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ using Oceananigans.BoundaryConditions
using Oceananigans.Biogeochemistry: update_biogeochemical_state!
using Oceananigans.TurbulenceClosures: compute_diffusivities!
using Oceananigans.Fields: compute!
using Oceananigans.ImmersedBoundaries: mask_immersed_field!
using Oceananigans.ImmersedBoundaries: mask_immersed!
using Oceananigans.Models: update_model_field_time_series!

import Oceananigans.TimeSteppers: update_state!
Expand All @@ -20,7 +20,7 @@ function update_state!(model::NonhydrostaticModel, callbacks=[]; compute_tendenc

# Mask immersed tracers
foreach(model.tracers) do tracer
@apply_regionally mask_immersed_field!(tracer)
@apply_regionally mask_immersed!(tracer)
end

# Update all FieldTimeSeries used in the model
Expand Down
4 changes: 2 additions & 2 deletions src/Models/ShallowWaterModels/update_shallow_water_state.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using Oceananigans.BoundaryConditions: fill_halo_regions!
using Oceananigans.ImmersedBoundaries: mask_immersed_field!
using Oceananigans.ImmersedBoundaries: mask_immersed!
using Oceananigans.Models: update_model_field_time_series!

import Oceananigans.TimeSteppers: update_state!
Expand All @@ -13,7 +13,7 @@ If `callbacks` are provided (in an array), they are called in the end.
function update_state!(model::ShallowWaterModel, callbacks=[]; compute_tendencies = true)

# Mask immersed fields
foreach(mask_immersed_field!, model.solution)
foreach(mask_immersed!, model.solution)

# Update possible FieldTimeSeries used in the model
update_model_field_time_series!(model, model.clock)
Expand Down
18 changes: 15 additions & 3 deletions src/OutputWriters/fetch_output.jl
Original file line number Diff line number Diff line change
@@ -1,16 +1,28 @@
using CUDA

using Oceananigans.Fields: AbstractField, compute_at!, ZeroField
using Oceananigans.ImmersedBoundaries: mask_immersed!
using Oceananigans.Models.LagrangianParticleTracking: LagrangianParticles

# Needed to support `fetch_output` with `model::Nothing`.
time(model) = model.clock.time
time(::Nothing) = nothing

fetch_output(output, model) = output(model)
# Default fetch_output with mask_immersed = nothing
fetch_output(output, model) = fetch_output(output, model, nothing)

function fetch_output(field::AbstractField, model)
function fetch_output(output, model, mask_immersed)
fetched = output(model)
if fetched isa Field
!isnothing(mask_immersed) && mask_immersed!(fetched, mask_immersed)
return parent(fetched)
end
return fetched
end

function fetch_output(field::AbstractField, model, mask_immersed)
compute_at!(field, time(model))
!isnothing(mask_immersed) && mask_immersed!(field, mask_immersed)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@glwagner I think this won't work because what happens is that, e.g., if u is the output to be saved then model.velocities.u is masked and then it includes NaNs at the next step so the whole state NaN...

At least that's what I found happening and I boil it down to this line here masking and then returning the parent of the field. Am I right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok that makes sense. But the real problem is how we define masking. Consider: the mask we apply to a field for output should not affect time-stepping. If it does, there's a problem. So let's fix that before hacking around it.

The issue is velocity components I think. The "mask" is defined to cover the points that are on the boundary, not just inside it. Thus masking the velocity to 0 is how we ensure impenetrability.

The confusion is therefore: are velocity nodes on the boundary immersed or not?

I think we should probably define this points as not immersed. Therefore to fix the problem we can:

  1. Implement a new function that satisfies impenetrability. We can start by doing this just for immersed grids.
  2. Define "mask_immersed" as something that only affects cells that are fully immersed. This has no effect on fully centered fields, but for staggered fields this implies a change: cells are only masked if they are not adjacent to active cells (I think the distinction between inactive_node and peripheral_node can be used for this).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was thinking of point 2 you make and modified masked_immersed! to use inactive_node (just to test the hypothesis) but that didn't work either.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There might also be a problem with the implicit solver to fix. Our strategy right now is to zero out elements of the matrix that correspond to immersed cells. Unfortunately 0 * NaN = NaN so any NaN in the immersed boundary will be propagated immediately in the whole column

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

Can't we just apply the mask to the saved output?

Copy link
Member

@glwagner glwagner Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that "implicit solver".

I guess that is the code here:

@inline immersed_ivd_peripheral_node(i, j, k, ibg, LX, LY, ::Center) = immersed_peripheral_node(i, j, k+1, ibg, LX, LY, Face())
@inline immersed_ivd_peripheral_node(i, j, k, ibg, LX, LY, ::Face) = immersed_peripheral_node(i, j, k, ibg, LX, LY, Center())
# Extend the upper and lower diagonal functions of the batched tridiagonal solver
for location in (:upper_, :lower_)
immersed_func = Symbol(:immersed_ivd_, location, :diagonal)
ordinary_func = Symbol(:ivd_ , location, :diagonal)
@eval begin
# Disambiguation
@inline $ordinary_func(i, j, k, ibg::GFIBG, closure, K, id, ℓx, ℓy, ℓz::Face, clock, Δt, κz) =
$immersed_func(i, j, k, ibg::GFIBG, closure, K, id, ℓx, ℓy, ℓz, clock, Δt, κz)
@inline $ordinary_func(i, j, k, ibg::GFIBG, closure, K, id, ℓx, ℓy, ℓz::Center, clock, Δt, κz) =
$immersed_func(i, j, k, ibg::GFIBG, closure, K, id, ℓx, ℓy, ℓz, clock, Δt, κz)
@inline function $immersed_func(i, j, k, ibg::GFIBG, closure, K, id, ℓx, ℓy, ℓz, clock, Δt, κz)
return ifelse(immersed_ivd_peripheral_node(i, j, k, ibg, ℓx, ℓy, ℓz),
zero(eltype(ibg.underlying_grid)),
$ordinary_func(i, j, k, ibg.underlying_grid, closure, K, id, ℓx, ℓy, ℓz, clock, Δt, κz))
end
end
end

Seems like we would need

f₁ = get_coefficient(f, i, j, 1, grid, p, tridiagonal_direction, args...)

to return 0 for immersed_cell.

To accomplish this without explicit masking we might have to wrap field in a ConditionalOperation:

return solve!(field, implicit_solver, field,
# ivd_*_diagonal gets called with these args after (i, j, k, grid):
vi_closure, vi_diffusivity_fields, tracer_index, map(ℓ -> (), loc)..., clock, Δt, κz)

hmm...

Another possibility is to move mask_immersed! from update_state!; for example if we masked fields before ab2_step! then I think we'd be ok:

ab2_step!(model, Δt, χ) # full step for tracers, fractional step for velocities.

Not sure if that would affect non-masked output. Hopefully not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we just apply the mask to the saved output?

What do you mean?

I mean only mask the data written in the jld2/nc file, not the actual data in the u, v, w, fields, etc.

Copy link
Member

@glwagner glwagner Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Data has to be written from an existing array. So, masking data being saved to disk without touching the "actual" fields requires allocating a temporary array. Note, in the GPU case we already do this on the CPU; however we would then have to mask on the CPU which could slow things down for large fields.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see!

Copy link
Member

@glwagner glwagner Jul 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But maybe what you're suggesting could be achieved by digging into JLD2's array-saving code. I'm not sure about NetCDF. I took a quick look and couldn't figure it out but such masking during i/o might be possible...

return parent(field)
end

Expand Down Expand Up @@ -38,7 +50,7 @@ convert_output(outputs::NamedTuple, writer) =
NamedTuple(name => convert_output(outputs[name], writer) for name in keys(outputs))

function fetch_and_convert_output(output, model, writer)
fetched = fetch_output(output, model)
fetched = fetch_output(output, model, writer.mask_immersed)
return convert_output(fetched, writer)
end

Expand Down
16 changes: 12 additions & 4 deletions src/OutputWriters/jld2_output_writer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ default_included_properties(::NonhydrostaticModel) = [:grid, :coriolis, :buoyanc
default_included_properties(::ShallowWaterModel) = [:grid, :coriolis, :closure]
default_included_properties(::HydrostaticFreeSurfaceModel) = [:grid, :coriolis, :buoyancy, :closure]

mutable struct JLD2OutputWriter{O, T, D, IF, IN, KW} <: AbstractOutputWriter
mutable struct JLD2OutputWriter{O, T, D, IF, IN, KW, FT} <: AbstractOutputWriter
filepath :: String
outputs :: O
schedule :: T
Expand All @@ -21,6 +21,7 @@ mutable struct JLD2OutputWriter{O, T, D, IF, IN, KW} <: AbstractOutputWriter
overwrite_existing :: Bool
verbose :: Bool
jld2_kw :: KW
mask_immersed :: FT
end

noinit(args...) = nothing
Expand All @@ -32,6 +33,7 @@ ext(::Type{JLD2OutputWriter}) = ".jld2"
indices = (:, :, :),
with_halos = false,
array_type = Array{Float64},
mask_immersed = nothing,
max_filesize = Inf,
overwrite_existing = false,
init = noinit,
Expand Down Expand Up @@ -78,6 +80,9 @@ Keyword arguments
- `array_type`: The array type to which output arrays are converted to prior to saving.
Default: `Array{Float64}`.

- `mask_immersed`: The value with which immersed boundary regions are filled with before saving.
You can either choose a numerical value or `NaN`. Default: `nothing`.

## File management

- `max_filesize`: The writer will stop writing to the output file once the file size exceeds `max_filesize`,
Expand Down Expand Up @@ -163,6 +168,7 @@ function JLD2OutputWriter(model, outputs; filename, schedule,
indices = (:, :, :),
with_halos = false,
array_type = Array{Float64},
mask_immersed = nothing,
max_filesize = Inf,
overwrite_existing = false,
init = noinit,
Expand All @@ -175,7 +181,7 @@ function JLD2OutputWriter(model, outputs; filename, schedule,
filename = auto_extension(filename, ".jld2")
filepath = joinpath(dir, filename)
overwrite_existing && isfile(filepath) && rm(filepath, force=true)

outputs = NamedTuple(Symbol(name) => construct_output(outputs[name], model.grid, indices, with_halos)
for name in keys(outputs))

Expand All @@ -184,8 +190,10 @@ function JLD2OutputWriter(model, outputs; filename, schedule,

initialize_jld2_file!(filepath, init, jld2_kw, including, outputs, model)

return JLD2OutputWriter(filepath, outputs, schedule, array_type, init,
including, part, max_filesize, overwrite_existing, verbose, jld2_kw)
mask_immersed = mask_immersed isa Number ? eltype(model.grid)(mask_immersed) : mask_immersed

return JLD2OutputWriter(filepath, outputs, schedule, array_type, init, including, part,
max_filesize, overwrite_existing, verbose, jld2_kw, mask_immersed)
end

function initialize_jld2_file!(filepath, init, jld2_kw, including, outputs, model)
Expand Down
Loading