Calibration of RiBasedVerticalDiffusivity to five LESbrary simulations

Install dependencies

using Pkg
pkg"add ParameterEstimocean, Oceananigans, CairoMakie"
using Oceananigans
using Oceananigans.Units
using ParameterEstimocean
using LinearAlgebra, CairoMakie, DataDeps, Distributions

using Oceananigans.TurbulenceClosures: RiBasedVerticalDiffusivity

Using LESbrary data

ParameterEstimocean.jl provides paths to synthetic observations derived from high-fidelity large eddy simulations. In this example, we illustrate calibration of a turbulence parameterization to one of these simulations:

cases = ["free_convection",
         "strong_wind_weak_cooling",
         "weak_wind_strong_cooling",
         "strong_wind",
         "strong_wind_no_rotation"]

datapaths = [@datadep_str("two_day_suite_1m/$(case)_instantaneous_statistics.jld2") for case in cases]

times = [2hours, 12hours, 24hours]
field_names = (:b, :u, :v)
transformation = ZScore()
regrid = (1, 1, 32)

observations = [SyntheticObservations(path; field_names, times, transformation, regrid)
                for path in datapaths]
5-element Vector{SyntheticObservations{NamedTuple{(:b, :v, :u), Tuple{Oceananigans.OutputReaders.FieldTimeSeries{Oceananigans.Grids.Center, Oceananigans.Grids.Center, Oceananigans.Grids.Center, Oceananigans.OutputReaders.InMemory, Tuple{Colon, Colon, Colon}, OffsetArrays.OffsetArray{Float64, 4, Array{Float64, 4}}, Oceananigans.Grids.RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded, Float64, Float64, Float64, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, Oceananigans.Architectures.CPU}, Float64, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}}, Vector{Float64}}, Oceananigans.OutputReaders.FieldTimeSeries{Oceananigans.Grids.Center, Oceananigans.Grids.Face, Oceananigans.Grids.Center, Oceananigans.OutputReaders.InMemory, Tuple{Colon, Colon, Colon}, OffsetArrays.OffsetArray{Float64, 4, Array{Float64, 4}}, Oceananigans.Grids.RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded, Float64, Float64, Float64, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, Oceananigans.Architectures.CPU}, Float64, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}}, Vector{Float64}}, Oceananigans.OutputReaders.FieldTimeSeries{Oceananigans.Grids.Face, Oceananigans.Grids.Center, Oceananigans.Grids.Center, Oceananigans.OutputReaders.InMemory, Tuple{Colon, Colon, Colon}, OffsetArrays.OffsetArray{Float64, 4, Array{Float64, 4}}, Oceananigans.Grids.RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded, Float64, Float64, Float64, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, Oceananigans.Architectures.CPU}, Float64, Oceananigans.BoundaryConditions.FieldBoundaryConditions{Nothing, Nothing, Nothing, Nothing, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}, Oceananigans.BoundaryConditions.BoundaryCondition{Oceananigans.BoundaryConditions.Flux, Nothing}}, Vector{Float64}}}}, Tuple{Symbol, Symbol, Symbol}, Oceananigans.Grids.RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded, Float64, Float64, Float64, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, OffsetArrays.OffsetVector{Float64, StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}}}, Oceananigans.Architectures.CPU}, Vector{Float64}, String, NamedTuple{(:parameters, :grid, :coriolis, :buoyancy, :closure), Tuple{NamedTuple{(:LESbrary_jl_commit_SHA1, :name, :thermocline_type, :buoyancy_flux, :momentum_flux, :temperature_flux, :coriolis_parameter, :thermal_expansion_coefficient, :gravitational_acceleration, :boundary_condition_θ_top, :boundary_condition_θ_bottom, :boundary_condition_u_top, :boundary_condition_u_bottom, :surface_layer_depth, :thermocline_width, :N²_surface_layer, :N²_thermocline, :N²_deep, :dθdz_surface_layer, :dθdz_thermocline, :dθdz_deep, :θ_surface, :θ_transition, :θ_deep, :z_transition, :z_deep, :k_transition, :k_deep), Tuple{SubString{String}, String, String, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Float64, Int64, Int64}}, NamedTuple{(:Nx, :Ny, :Nz, :Hx, :Hy, :Hz, :Lx, :Ly, :Lz, :Δxᶠᵃᵃ, :Δxᶜᵃᵃ, :xᶠᵃᵃ, :xᶜᵃᵃ, :Δyᵃᶠᵃ, :Δyᵃᶜᵃ, :yᵃᶠᵃ, :yᵃᶜᵃ, :Δzᵃᵃᶠ, :Δzᵃᵃᶜ, :zᵃᵃᶠ, :zᵃᵃᶜ), Tuple{Int64, Int64, Int64, Int64, Int64, Int64, Float64, Float64, Float64, Float64, Float64, Vector{Float64}, Vector{Float64}, Float64, Float64, Vector{Float64}, Vector{Float64}, Float64, Float64, Vector{Float64}, Vector{Float64}}}, NamedTuple{(:f,), Tuple{Float64}}, NamedTuple{(:model,), Tuple{NamedTuple{(:gravitational_acceleration, :constant_salinity, :equation_of_state), Tuple{Float64, Float64, NamedTuple{(:α, :β), Tuple{Float64, Float64}}}}}}, NamedTuple{(:Cν, :ν, :Cκ, :κ), Tuple{Float64, Float64, NamedTuple{(:T, :c₀, :c₁, :c₂), NTuple{4, Float64}}, NamedTuple{(:T, :c₀, :c₁, :c₂), NTuple{4, Float64}}}}}}, Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}}}}:
 SyntheticObservations with fields (:b, :v, :u)
├── times: [2 hrs, 12 hrs, 1 d]
├── grid: 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── path: "/home/runner/.julia/datadeps/two_day_suite_1m/free_convection_instantaneous_statistics.jld2"
├── metadata: (:parameters, :grid, :coriolis, :buoyancy, :closure)
└── transformation: Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}} with 3 entries
 SyntheticObservations with fields (:b, :v, :u)
├── times: [2 hrs, 12 hrs, 1 d]
├── grid: 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── path: "/home/runner/.julia/datadeps/two_day_suite_1m/strong_wind_weak_cooling_instantaneous_statistics.jld2"
├── metadata: (:parameters, :grid, :coriolis, :buoyancy, :closure)
└── transformation: Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}} with 3 entries
 SyntheticObservations with fields (:b, :v, :u)
├── times: [2 hrs, 12 hrs, 1 d]
├── grid: 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── path: "/home/runner/.julia/datadeps/two_day_suite_1m/weak_wind_strong_cooling_instantaneous_statistics.jld2"
├── metadata: (:parameters, :grid, :coriolis, :buoyancy, :closure)
└── transformation: Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}} with 3 entries
 SyntheticObservations with fields (:b, :v, :u)
├── times: [2 hrs, 12 hrs, 1 d]
├── grid: 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── path: "/home/runner/.julia/datadeps/two_day_suite_1m/strong_wind_instantaneous_statistics.jld2"
├── metadata: (:parameters, :grid, :coriolis, :buoyancy, :closure)
└── transformation: Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}} with 3 entries
 SyntheticObservations with fields (:b, :v, :u)
├── times: [2 hrs, 12 hrs, 1 d]
├── grid: 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── path: "/home/runner/.julia/datadeps/two_day_suite_1m/strong_wind_no_rotation_instantaneous_statistics.jld2"
├── metadata: (:parameters, :grid, :coriolis, :buoyancy, :closure)
└── transformation: Dict{Symbol, ParameterEstimocean.Transformations.Transformation{TimeIndices{UnitRange{Int64}}, Nothing, ZScore{Float64}}} with 3 entries

Let's take a look at the observations. We define a few plotting utilities along the way to use later in the example:

colorcycle = [:black, :red, :darkblue, :orange, :pink1, :seagreen, :magenta2]
markercycle = [:rect, :utriangle, :star5, :circle, :cross, :+, :pentagon]

function make_figure_axes(n=1)
    fig = Figure(resolution=(1200, n*400))
    axs = []
    for i = 1:n
        ax_b = Axis(fig[i, 1], xlabel = "Buoyancy \n[cm s⁻²]", ylabel = "z [m]")
        ax_u = Axis(fig[i, 2], xlabel = "x-velocity, u \n[cm s⁻¹]")
        ax_v = Axis(fig[i, 3], xlabel = "y-velocity, v \n[cm s⁻¹]")
        push!(axs, (ax_b, ax_u, ax_v))
    end
    n == 1 && (axs = first(axs))
    return fig, axs
end

function plot_fields!(axs, b, u, v, label, color, grid=first(observations).grid)
    z = znodes(Center, grid)
    # Note unit conversions below, eg m s⁻² -> cm s⁻²:
    lines!(axs[1], 1e2 * b, z; color, label)
    lines!(axs[2], 1e2 * u, z; color, label)
    lines!(axs[3], 1e2 * v, z; color, label)
    return nothing
end
plot_fields! (generic function with 2 methods)

And then plot the evolution of the observed fields,

fig, axs = make_figure_axes()

for (i, obs) in enumerate(observations)
    Nt = length(obs.times)
    t = obs.times[end]
    fields = map(name -> interior(obs.field_time_serieses[name][Nt], 1, 1, :), field_names)
    plot_fields!(axs, fields..., "t = " * prettytime(t), colorcycle[i])
end

[axislegend(ax, position=:rb, merge=true, labelsize=10) for ax in axs]

Calibration

ri_based_closure = RiBasedVerticalDiffusivity()

simulation = ensemble_column_model_simulation(observations;
                                              Nensemble = 60,
                                              architecture = CPU(),
                                              tracers = (:b, :e),
                                              closure = ri_based_closure)
Simulation of HydrostaticFreeSurfaceModel{CPU, RectilinearGrid}(time = 0 seconds, iteration = 0)
├── Next time step: 1 second
├── Elapsed wall time: 0 seconds
├── Wall time per iteration: NaN years
├── Stop time: 1 day
├── Stop iteration : Inf
├── Wall time limit: Inf
├── Callbacks: OrderedDict with 4 entries:
│   ├── stop_time_exceeded => Callback of stop_time_exceeded on IterationInterval(1)
│   ├── stop_iteration_exceeded => Callback of stop_iteration_exceeded on IterationInterval(1)
│   ├── wall_time_limit_exceeded => Callback of wall_time_limit_exceeded on IterationInterval(1)
│   └── nan_checker => Callback of NaNChecker for u on IterationInterval(100)
├── Output writers: OrderedDict with no entries
└── Diagnostics: OrderedDict with no entries

The simulation is initialized with neutral boundary conditions and a default time-step, which we modify for our particular problem:

Qᵘ = simulation.model.velocities.u.boundary_conditions.top.condition
Qᵇ = simulation.model.tracers.b.boundary_conditions.top.condition
N² = simulation.model.tracers.b.boundary_conditions.bottom.condition

simulation.Δt = 20minutes

for (i, obs) in enumerate(observations)
    view(Qᵘ, :, i) .= obs.metadata.parameters.momentum_flux
    view(Qᵇ, :, i) .= obs.metadata.parameters.buoyancy_flux
    view(N², :, i) .= obs.metadata.parameters.N²_deep
end

We identify a subset of the closure parameters to calibrate by specifying parameter names and prior distributions:

priors = (ν₀   = lognormal(mean=0.01, std=0.005),
          κ₀   = lognormal(mean=0.1,  std=0.05),
          Ri₀ν = Normal(-0.5, 1.0),
          Ri₀κ = Normal(-0.5, 1.0),
          Riᵟν = lognormal(mean=1.0,  std=0.5),
          Riᵟκ = lognormal(mean=1.0,  std=0.5))

free_parameters = FreeParameters(priors)
FreeParameters with 6 parameters
├── names: (:ν₀, :κ₀, :Ri₀ν, :Ri₀κ, :Riᵟν, :Riᵟκ)
├── priors: Dict{Symbol, Any}
│   ├──   ν₀ => LogNormal{Float64}(μ=-4.716741961645196, σ=0.47238072707743883)
│   ├──   κ₀ => LogNormal{Float64}(μ=-2.4141568686511503, σ=0.47238072707743883)
│   ├── Ri₀ν => Normal{Float64}(μ=-0.5, σ=1.0)
│   ├── Ri₀κ => Normal{Float64}(μ=-0.5, σ=1.0)
│   ├── Riᵟν => LogNormal{Float64}(μ=-0.11157177565710488, σ=0.47238072707743883)
│   └── Riᵟκ => LogNormal{Float64}(μ=-0.11157177565710488, σ=0.47238072707743883)
└── dependent parameters: Dict{Symbol, Any}

TODO: explain the meaning of each parameter The prior information comes from experience, prior calibration runs, and educated guesses.

calibration = InverseProblem(observations, simulation, free_parameters)
InverseProblem{ConcatenatedOutputMap} with free parameters (:ν₀, :κ₀, :Ri₀ν, :Ri₀κ, :Riᵟν, :Riᵟκ)
├── observations: Vector{<:SyntheticObservations} of (:b, :v, :u) on 1×1×32 RectilinearGrid{Float64, Oceananigans.Grids.Periodic, Oceananigans.Grids.Periodic, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 3×3×3 halo
├── simulation: Simulation on 60×5×32 RectilinearGrid{Float64, Oceananigans.Grids.Flat, Oceananigans.Grids.Flat, Oceananigans.Grids.Bounded} on Oceananigans.Architectures.CPU with 0×0×3 halo with Δt=1200.0
├── free_parameters: (:ν₀, :κ₀, :Ri₀ν, :Ri₀κ, :Riᵟν, :Riᵟκ)
└── output map: ConcatenatedOutputMap

Next, we calibrate, using a relatively large noise to reflect our uncertainty about how close the observations and model can really get,

eki = EnsembleKalmanInversion(calibration; pseudo_stepping=ConstantConvergence(0.8))
iterate!(eki; iterations = 10)
@show eki.iteration_summaries[end]
IterationSummary(iteration=10, pseudotime=0.199614, pseudo_Δt=0.0243704) for 60 particles and 6 parameters
                              ν₀ |         κ₀ |       Ri₀ν |       Ri₀κ |       Riᵟν |       Riᵟκ | 
       ensemble_mean:  5.096e-02 |  1.033e-01 |  2.075e-01 | -3.888e-01 |  1.330e+00 |  6.157e-01 | 
       best particle:  1.222e-01 |  7.122e-02 | -4.547e-01 |  1.191e-01 |  8.492e-01 |  4.881e-01 | error = 3.250437e-01
      worst particle:  4.876e-02 |  2.012e-01 | -3.381e-01 |  2.952e-01 |  4.886e-01 |  7.894e-01 | error = 4.526354e-01
             minimum:  3.193e-02 |  3.685e-02 | -2.066e+00 | -2.801e+00 |  4.687e-01 |  1.509e-01 | 
             maximum:  1.222e-01 |  3.415e-01 |  2.195e+00 |  1.498e+00 |  5.633e+00 |  1.796e+00 | 
   ensemble_variance:  1.088e-03 |  2.769e-03 |  5.213e-01 |  7.817e-01 |  2.286e+05 |  1.838e+02 | 

Results

To analyze the results, we build a new simulation with just one ensemble member to evaluate some utilities for analyzing the results:

Nt = length(first(observations).times)
Niter = length(eki.iteration_summaries) - 1
modeled_time_serieses = calibration.time_series_collector.field_time_serieses

observed, modeled = [], []
for (c, obs) in enumerate(observations)
    push!(observed, map(name -> interior(obs.field_time_serieses[name][Nt], 1, 1, :), field_names))
    push!(modeled,  map(name -> interior(  modeled_time_serieses[name][Nt], 1, c, :), field_names))
end

function compare_model_observations(model_label="modeled")
    fig, axs = make_figure_axes(length(observations))
    for (c, obs) in enumerate(observations)
        plot_fields!(axs[c], observed[c]..., "observed at t = " * prettytime(times[end]), :black)
        plot_fields!(axs[c], modeled[c]..., model_label, :blue)
        [axislegend(ax, position=:rb, merge=true, labelsize=10) for ax in axs[c]]
    end
    return fig
end
compare_model_observations (generic function with 2 methods)

Now we execute forward runs for the initial ensemble mean,

initial_parameters = eki.iteration_summaries[0].ensemble_mean
forward_run!(calibration, initial_parameters)
fig = compare_model_observations("modeled after 0 iterations")
[ Info: Initializing simulation...
[ Info:     ... simulation initialization complete (1.229 ms)
[ Info: Executing initial time step...
[ Info:     ... initial time step complete (4.512 ms).
[ Info: Simulation is stopping. Model time 1 day has hit or exceeded simulation stop time 1 day.

and the final ensemble mean, representing our "best" parameter set,

best_parameters = eki.iteration_summaries[end].ensemble_mean
forward_run!(calibration, best_parameters)
fig = compare_model_observations("modeled after $Niter iterations")
[ Info: Initializing simulation...
[ Info:     ... simulation initialization complete (1.174 ms)
[ Info: Executing initial time step...
[ Info:     ... initial time step complete (4.502 ms).
[ Info: Simulation is stopping. Model time 1 day has hit or exceeded simulation stop time 1 day.

Parameter evolution

To understand how results changed over the EKI iterations, we look at the evolution of the ensemble means,

ensemble_means = NamedTuple(n => map(summary -> summary.ensemble_mean[n], eki.iteration_summaries)
                            for n in calibration.free_parameters.names)

fig = Figure()
ax = Axis(fig[1, 1], xlabel = "Ensemble Kalman iteration", ylabel = "Parameter value")

for (i, name) in enumerate(calibration.free_parameters.names)
    label = string(name)
    marker = markercycle[i]
    color = colorcycle[i]
    scatterlines!(ax, 0:Niter, parent(ensemble_means[name]); marker, color, label)
end

axislegend(ax, position=:rb)


This page was generated using Literate.jl.