Filters
Oceanostics provides spatial filters that operate directly on Oceananigans fields. All filters are built on top of Oceananigans' KernelFunctionOperation, so they compose with the rest of the Oceananigans ecosystem (outputs, reductions, other operations, etc.) and run on both CPU and GPU.
Box filter
The BoxFilter computes a local running-mean (box average) of a field over one or more grid directions. The stencil width is controlled by the width keyword: each filtered direction uses a (2*width + 1)-point symmetric average centred on the current cell.
Multi-direction filters are fused into a single kernel at compile time, so a 3D box filter performs one pass over the data, not three.
Basic usage
using Oceananigans
using Oceanostics
grid = RectilinearGrid(size=(32, 32), x=(0, 1), z=(0, 1),
topology=(Periodic, Flat, Bounded))
c = CenterField(grid)
set!(c, (x, z) -> sin(2π * x) * z)
c̄ = Field(BoxFilter(c; dims=(1, 3), width=2))32×1×32 Field{Center, Center, Center} on RectilinearGrid on CPU
├── grid: 32×1×32 RectilinearGrid{Float64, Periodic, Flat, Bounded} on CPU with 3×0×3 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Periodic, east: Periodic, south: Nothing, north: Nothing, bottom: ZeroFlux, top: ZeroFlux, immersed: Nothing
├── operand: KernelFunctionOperation at (Center, Center, Center)
├── status: time=0.0
└── data: 38×1×38 OffsetArray(::Array{Float64, 3}, -2:35, 1:1, -2:35) with eltype Float64 with indices -2:35×1:1×-2:35
└── max=0.912364, min=-0.912364, mean=8.78204e-18Boundary handling
For Periodic directions, stencil offsets are always wrapped — the boundary keyword is silently ignored. For Bounded directions the boundary keyword selects how out-of-bounds offsets are treated:
boundary | Behaviour |
|---|---|
:shrink (default) | Drop out-of-bounds offsets from sum and count (honest local average; stencil shrinks near walls). |
:edge | Replicate the nearest interior cell (ψ[1] or ψ[N]). |
(left=a, right=b) | Pad with constant a on the low side and b on the high side. |
A single spec applies to every filtered dimension, or a tuple gives per-dimension control:
# Same policy for both dims
c̄_edge = Field(BoxFilter(c; dims=(1, 3), width=1, boundary=:edge))
# Per-dim: :shrink in x, constant-pad in z
c̄_mixed = Field(BoxFilter(c; dims=(1, 3), width=1, boundary=(:shrink, (left=0.0, right=0.0))))32×1×32 Field{Center, Center, Center} on RectilinearGrid on CPU
├── grid: 32×1×32 RectilinearGrid{Float64, Periodic, Flat, Bounded} on CPU with 3×0×3 halo
├── boundary conditions: FieldBoundaryConditions
│ └── west: Periodic, east: Periodic, south: Nothing, north: Nothing, bottom: ZeroFlux, top: ZeroFlux, immersed: Nothing
├── operand: KernelFunctionOperation at (Center, Center, Center)
├── status: time=0.0
└── data: 38×1×38 OffsetArray(::Array{Float64, 3}, -2:35, 1:1, -2:35) with eltype Float64 with indices -2:35×1:1×-2:35
└── max=0.936385, min=-0.936385, mean=1.00492e-17API reference
Oceanostics.Filters.BoxFilter — Type
BoxFilter(ψ; dims, width, boundary)
Return a KernelFunctionOperation that computes a local box-average of ψ over the directions listed in dims.
dims is a tuple of distinct integers drawn from (1, 2, 3) (where 1, 2, 3 correspond to x, y, z). width is the half-width of the stencil in cells, so each selected direction uses a (2*width + 1)-point running mean centered on the current cell.
A multi-directional filter is assembled as a single KernelFunctionOperation whose kernel function is a 1D BoxFilterKernel{d₁}, with the next dimension's BoxFilterKernel{d₂} (and so on) threaded into the argument list. The nested 1D kernels inline into a single fused read pass at compile time.
Boundary handling
Stencil offsets that leave the interior 1:N of a direction are handled per-direction. For Periodic directions offsets are always wrapped via mod1, independent of the boundary keyword. For Bounded directions the boundary keyword picks the policy (default: :shrink):
:shrink— drop out-of-bounds offsets from both the sum and the count, so the filter is an honest local average whose effective stencil shrinks withinwidthcells of a wall. This is the default forBoundeddirections.:edge— replicate the boundary-cell value (readsψ[1]orψ[N]for offsets past either end).(left=a, right=b)— pad with constantaon the low-index side andbon the high-index side (aandbare promoted to a common type).
boundary may be a single spec applied to every filtered dim, or a tuple with one entry per dim in dims (in the order the user passed them):
BoxFilter(ψ; dims=(1, 2), width=3, boundary=:edge)
BoxFilter(ψ; dims=(1, 2), width=3, boundary=(:shrink, :edge))
BoxFilter(ψ; dims=(1,), width=3, boundary=(left=0.0, right=1.0))Because every policy wraps, clamps, or skips indices up front, halo_size(grid) does not constrain width: a small halo on a bounded direction is fine. The output location matches the location of ψ, and ψ can be any input that supports the standard Oceananigans ψ[i, j, k] indexing contract (e.g. a Field or any AbstractOperation).
Examples
Filter a tracer on a 2D (xz) grid that is periodic in x and bounded in z. The boundary=:edge keyword explicitly overrides the default :shrink policy for the bounded z-direction; x is Periodic so the boundary spec is silently ignored for that direction regardless.
julia> using Oceananigans, Oceanostics
julia> grid = RectilinearGrid(size=(8, 8), x=(0, 1), z=(0, 1),
topology=(Periodic, Flat, Bounded));
julia> c = CenterField(grid);
julia> BoxFilter(c; dims=(1, 3), width=2, boundary=:edge) isa KernelFunctionOperation
true