Offline filtering implementation
The offline Lagrangian filter equations, which find Lagrangian filtered tracer(s) $f^*$ (see Lagrangian averaging for a definition) are solved after the original Oceananigans simulation (or, feasibly, using any simulation output worked into the same format as Oceananigans native output) on saved data. Data should be at a temporal resolution that resolves the high frequency motions to be filtered. Velocities and the tracer(s) $f$ to be filtered need to be provided. The post-processing filter step runs similarly to an Oceananigans simulation, using the Oceananigans infrastructure to solve the filtering PDEs.
The offline filter uses mostly the same functions as the online filter to define filtered fields and their forcings, but in this case most of the process is 'under the hood', as the user only needs to provide the simulation data and specify the configuration. An example is given in offline_filter_geostrophic_adjustment.jl, and more detail is given in how it works.
Input data
The original data can be a JLD2 or NetCDF file of Oceananigans output, given by original_data_filename. It must contain the velocities (velocity_names) and the variables to be filtered (var_names_to_filter), saved often enough to resolve the motions being filtered.
- JLD2 files written by Oceananigans'
JLD2Writercan be used directly. - NetCDF files should be written by Oceananigans'
NetCDFWriter(or have the same metadata), since the grid and the location of each variable are read from the file's attributes. The time coordinate must be calledtime.
The data is read frame by frame directly from the original file, so memory use doesn't depend on the length of the dataset, and no intermediate copy of the data is written (see How it works).
Example
A short example of how to implement offline filtering on a GPU is given below:
using OceananigansLagrangianFilter
using Oceananigans.Units
using CUDA
# Define the filter configuration
filter_config = OfflineFilterConfig(original_data_filename = "my_simulation.jld2", # Where the original simulation output is
output_filename = "my_filtered_simulation.jld2" # Where to save the filtered output
var_names_to_filter = ("T", "b"), # Which variables to filter
velocity_names = ("u","v"), # Velocities to use for Lagrangian filtering
architecture = GPU(), # CPU() or GPU()
Δt = 20minutes, # Time step of filtering simulation
T_out = 1hour, # How often to output filtered data
N = 2, # Order of Butterworth filter
freq_c = 1e-4/2, # Cut-off frequency of Butterworth filter
output_netcdf = false, # Whether to output filtered data to a netcdf file in addition to .jld2
delete_intermediate_files = true, # Delete the individual output of the forward and backward passes
compute_mean_velocities = true, # Whether to compute the mean velocities
compute_Eulerian_filter = true) # Whether to compute the Eulerian filter for comparison
# Run the offline filter
run_offline_Lagrangian_filter(filter_config)
# The filtered data is now saved to `my_filtered_simulation.jld2`Eulerian filter
With lagrangian = false, the filtered variables aren't advected, so the same filter is applied in time at fixed points: the Eulerian filter. Its outputs are named _Eulerian_filtered, e.g. b_Eulerian_filtered. The maps and mean velocities can still be computed, but nothing is regridded to the mean position. This is solved in the same way as the Lagrangian filter, so it suits large datasets, and it works with any frequency scale.
For a quick comparison with the Lagrangian filter, compute_Eulerian_filter = true instead filters the saved filter output in post-processing, so only at the output times T_out (see Helpful tips).
Boundary relaxation
The filtered variables can be relaxed towards their local equilibrium: the values they would have if the fields (and the frequency scale) had been constant along Lagrangian trajectories, or constant in time at fixed points for the Eulerian filter (lagrangian = false). For a normalised filter, the filtered fields are then equal to the unfiltered fields, and the mean velocities to the velocities. These are also the values the filtered variables are initialised to. This is useful, for example, near open boundaries, where fluid enters the domain without a filtered history. relaxation_mask_func takes the non-flat coordinates and relaxation_params, and should be 1 for full relaxation and 0 for none. relaxation_params must include the relaxation timescale:
# Relax within about 1 km of the boundaries at x = 0 and x = L
relaxation_mask(x, z, p) = exp(-x / p.width) + exp(-(p.L - x) / p.width)
config = OfflineFilterConfig(...,
relaxation_mask_func = relaxation_mask,
relaxation_params = (; width = 1kilometers, L = 10kilometers, timescale = 1hour))Frequency scale
The cutoff frequency can vary in space, and in time, through a frequency scale $M$ that multiplies the filter's frequencies locally (see Varying cutoff frequency). frequency_scale_func takes the non-flat coordinates and frequency_scale_params:
frequency_scale(x, z, p) = 1 + 0.5 * sin(2π * x / p.L)
config = OfflineFilterConfig(original_data_filename = "my_simulation.jld2",
var_names_to_filter = ("T",),
velocity_names = ("u", "w"),
N = 2, freq_c = 1e-4,
frequency_scale_func = frequency_scale,
frequency_scale_params = (; L = 10kilometers))$M$ can also depend on other fields in the data, which are read in with extra_auxiliary_var_names and named in frequency_scale_dependencies. Their values at the cell centre are passed after the coordinates:
# The effective inertial frequency f + zeta/2 (Kunze, 1985), relative to f, with a floor so that M stays positive
frequency_scale(x, z, zeta, p) = max(1 + zeta / (2 * p.f), p.min_scale)
config = OfflineFilterConfig(...,
extra_auxiliary_var_names = ("zeta",),
frequency_scale_func = frequency_scale,
frequency_scale_dependencies = (:zeta,),
frequency_scale_params = (; f = 1e-4, min_scale = 0.1))- $M$ must be finite and positive. This is checked at every cell centre when the config is built, but with dependencies only for the initial fields, when the filtered variables are initialised. Make sure it stays positive throughout the run, e.g. by using
absorexpof the fields rather than the fields themselves. frequency_scale_funcis called inside the filter's kernels, so it must be GPU-compatible to run on a GPU.- Fields in
frequency_scale_dependenciesmust be invar_names_to_filterorextra_auxiliary_var_names, and at cell centres. They can't be the velocities: save what the frequency scale needs (e.g. the vorticity) as its own variable in the data. - $M$ can't depend on the filtered fields of the same run (see Varying cutoff frequency): to use a filtered field, filter it in a first run and read it in with
extra_auxiliary_var_namesin a second. - Choose
Δtfor the largest local cutoff during the run, and leave enough time for spin-up at the smallest one. - The time shift is not defined. The Eulerian filter in post-processing (
compute_Eulerian_filter = true) uses the same $M$ at fixed points if it only depends on the coordinates. Withfrequency_scale_dependencies, $M$ varies in time and it isn't computed: for the Eulerian counterpart, run the filter again withlagrangian = false.