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 size is controlled by the N keyword: each filtered direction uses an N-point symmetric average centred on the current cell — i.e. N cells contribute to one filtered output value. N must be an odd integer ≥ 3, and refers to the size of the filter stencil, not the grid.

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

julia> using Oceananigans, Oceanostics

julia> grid = RectilinearGrid(size=(32, 32), x=(0, 1), z=(0, 1),
                              topology=(Periodic, Flat, Bounded));

julia> c = CenterField(grid);

julia> set!(c, (x, z) -> sin(2π * x) * z);

julia> c̄ = Field(BoxFilter(c; dims=(1, 3), N=5));

julia> size(c̄)
(32, 1, 32)

Boundary 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:

boundaryBehaviour
:shrink (default)Drop out-of-bounds offsets from sum and count (honest local average; stencil shrinks near walls).
:edgeReplicate 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:

julia> c̄_edge = Field(BoxFilter(c; dims=(1, 3), N=3, boundary=:edge));

julia> c̄_mixed = Field(BoxFilter(c; dims=(1, 3), N=3, boundary=(:shrink, (left=0.0, right=0.0))));

julia> size(c̄_edge) == size(c̄_mixed) == (32, 1, 32)
true

API reference

Oceanostics.Filters.BoxFilterType
BoxFilter(ψ; dims, N, 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).

N is the total number of grid points used by the filter stencil along each filtered direction — i.e. how many cells are averaged together to produce one filtered output value (e.g. N=3 is a 3-point running mean, N=5 is a 5-point running mean). N must be an odd integer ≥ 3 so the stencil is symmetric around the current cell. (This is the size of the filter stencil — not the size of the grid.)

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:Nd_grid of a direction (where Nd_grid is the number of cells along that direction) are handled per-direction. For Periodic directions offsets are always wrapped periodically, 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 near a wall. This is the default for Bounded directions.
  • :edge — replicate the boundary-cell value (reads ψ[1] or ψ[Nd_grid] for offsets past either end).
  • (left=a, right=b) — pad with constant a on the low-index side and b on the high-index side (a and b are 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), N=7, boundary=:edge)
BoxFilter(ψ; dims=(1, 2), N=7, boundary=(:shrink, :edge))
BoxFilter(ψ; dims=(1,),   N=7, boundary=(left=0.0, right=1.0))

Because every policy wraps, clamps, or skips indices up front, halo_size(grid) does not constrain N: 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).

For Periodic directions the stencil must span at most one period: N ≤ 2*Nd_grid + 1, where Nd_grid is the number of cells along that direction. This is enforced at construction time.

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), N=5, boundary=:edge) isa KernelFunctionOperation
true
source

Gaussian filter

The GaussianFilter computes a Gaussian-weighted local average of a field over one or more grid directions. Each stencil point at offset Δ from the current cell receives weight exp(-Δ²/(2σ²)), where σ is the standard deviation of the kernel in physical units (the same units as the grid spacing). The filter is always normalized: the weighted sum is divided by the sum of the surviving weights, so all boundary policies behave consistently.

σ is the only required parameter beyond dimsN is inferred per-direction from σ and the minimum cell spacing in that direction so the stencil extends roughly on each side of the current cell. To override, pass N explicitly: a single odd integer (≥ 3) applies to every filtered dim, or a tuple of odd integers sets one count per dim.

dims and boundary work identically to BoxFilter.

Basic usage

julia> c̄_gauss = Field(GaussianFilter(c; dims=(1, 3), σ=0.05));

julia> c̄_gauss isa Field
true

Pass N to override the default stencil:

julia> c̄_wide = Field(GaussianFilter(c; dims=(1,), σ=0.05, N=11));

julia> c̄_perdim = Field(GaussianFilter(c; dims=(1, 3), σ=0.05, N=(7, 11)));

julia> (c̄_wide isa Field, c̄_perdim isa Field)
(true, true)

API reference

Oceanostics.Filters.GaussianFilterType
GaussianFilter(ψ; dims, σ, N, boundary)

Return a KernelFunctionOperation that computes a Gaussian-weighted local average of ψ over the directions listed in dims.

σ is the standard deviation of the Gaussian kernel in physical units (the same units as the grid spacing). Internally the filter precomputes its weights once per direction in cell-offset units using σ_cells = σ / Δ, where Δ is the (uniform) grid spacing along that direction; each stencil point at cell offset Δi then receives weight exp(-Δi² / (2 σ_cells²)). The filter is normalized: the weighted sum is divided by the sum of the surviving weights, so all boundary policies behave consistently.

Uniform spacing required

Because the per-direction weights are precomputed assuming a single Δ, GaussianFilter only supports uniform spacing along each filtered direction. Non-uniform (stretched) directions raise an ArgumentError at construction time. Other directions on the same grid may be non-uniform — only the ones listed in dims need to be uniform. Use BoxFilter on stretched directions, since its weights do not depend on Δ.

N is the total number of grid points used by the filter stencil along each filtered direction — i.e. how many cells contribute to a single filtered output value. N must be an odd integer ≥ 3 so the stencil is symmetric around the current cell. (This is the size of the filter stencil — not the size of the grid.) If unspecified, N is inferred per-direction from σ and Δ so that the stencil extends roughly on each side of the current cell. To override, pass either a single odd integer (applied to every filtered dim) or a tuple with one odd-integer count per dim in dims (in the order the user passed them). For Periodic directions the stencil must span at most one period (N ≤ 2*Nd_grid + 1, where Nd_grid is the number of cells along that direction); this is enforced at construction time.

See BoxFilter for the dims and boundary keyword documentation.

Examples

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> GaussianFilter(c; dims=(1, 3), σ=0.1) isa KernelFunctionOperation
true
source