1D water column example

Info

This example uses Oceananigans.jl and OceanBioME.jl. We recommend familiarizing yourself with their user interface if you intend to make changes to the physical model setup.

In this example we run a default Agate.jl-NiPiZD model inside a simple 1D water column model. The physical model setup is based on an example provided in the OceanBioME.jl documentation and represents an idealized 200m deep North Atlantic time series.

Loading dependencies

The example uses Agate.jl, Oceananigans.jl, and OceanBioME.jl for the ocean simulations. CairoMakie is used for plotting.

using Agate
using Agate.Models: NiPiZD
using Agate.Library.Light
using OceanBioME
using OceanBioME: Biogeochemistry
using Oceananigans
using Oceananigans.Units
using CairoMakie

const year = years = 365day

Ecosystem model

First, we construct our ecosystem model. Here, we use a default 2 phytoplankton, 2 zooplankton Agate.jl-NiPiZD ecosystem model.

N2P2ZD = NiPiZD.construct()

Forcings

Second, we define the model physical forcings. In this example, mixed layer depth (MLD) forces the physical mixing (diffusivity), while PAR influences plankton photosynthesis.

#diffusivity
@inline function diffusivity(x, y, z, t)
    H(t, t₀, t₁) = ifelse(t₀ < t < t₁, 1.0, 0.0)
    function fmld1(t)
        return H(t, 50days, year) *
               (1 / (1 + exp(-(t - 100days) / 5days))) *
               (1 / (1 + exp((t - 330days) / 25days)))
    end
    function MLD(t)
        return -(
            10 +
            340 * (
                1 - fmld1(year - eps(year)) * exp(-mod(t, year) / 25days) -
                fmld1(mod(t, year))
            )
        )
    end
    return 1e-2 * (1 + tanh((z - MLD(t)) / 10)) / 2 + 1e-4
end

#irradiance
@inline function seasonal_PAR(x, y, z, t)
    PAR⁰ =
        60 *
        (1 - cos((t + 15days) * 2π / year)) *
        (1 / (1 + 0.2 * exp(-((mod(t, year) - 200days) / 50days)^2))) + 2
    return PAR⁰ * exp(0.2 * z)
end

#plots
t_range = 0.0:days:(365.0 * days)  # Time range from 0 to 365 days
z_range = -200.0:10.0:0.0  # Depth range from -200m to 0m
x, y, z = 0.0, 0.0, 0.0
κₜ_values = [diffusivity(x, y, z, t) for t in t_range, z in z_range]
PAR_values = [seasonal_PAR(x, y, z, t) for t in t_range, z in z_range]

fig_forcing = Figure(; resolution=(800, 600), fontsize=14)
ax1 = Axis(fig_forcing[1, 1]; xlabel="Time (days)", ylabel="Depth (m)", title="irradiance")
CairoMakie.heatmap!(ax1, t_range ./ days, z_range, PAR_values; colormap=:viridis)

ax2 = Axis(fig_forcing[2, 1]; xlabel="Time (days)", ylabel="Depth (m)", title="diffusivity")
CairoMakie.heatmap!(ax2, t_range ./ days, z_range, κₜ_values; colormap=:viridis)

fig_forcing

Physical model

grid = RectilinearGrid(; size=(1, 1, 25), extent=(20meters, 20meters, 200meters))

bgc_model = Biogeochemistry(
    N2P2ZD(); light_attenuation=FunctionFieldPAR(; grid, PAR_f=seasonal_PAR)
)

full_model = NonhydrostaticModel(;
    grid,
    clock=Clock(; time=0.0),
    closure=ScalarDiffusivity(; ν=diffusivity, κ=diffusivity),
    biogeochemistry=bgc_model,
)

Initial conditions

set!(full_model; N=7.0, P1=0.01, P2=0.01, Z1=0.05, Z2=0.05, D=0.0) # mmol N / m³

Simulation

filename = "N2P2ZD_column.jld2"

simulation = Simulation(full_model; Δt=20minutes, stop_time=1year)

simulation.output_writers[:profiles] = JLD2Writer(
    full_model,
    full_model.tracers;
    filename=filename,
    schedule=TimeInterval(1day),
    overwrite_existing=true,
)

run!(simulation)
┌ Warning: Attempting to store typeof(Main.var"##225".diffusivity).
│ JLD2 only stores functions by name.
│  This may not be useful for anonymous functions.
└ @ JLD2 ~/.julia/packages/JLD2/SgtOb/src/data/writing_datatypes.jl:447
[ Info: Initializing simulation...
[ Info:     ... simulation initialization complete (17.350 seconds)
[ Info: Executing initial time step...
[ Info:     ... initial time step complete (9.967 seconds).
[ Info: Simulation is stopping after running for 2.550 minutes.
[ Info: Simulation time 365 days equals or exceeds stop time 365 days.

Plotting

#Load time series data
timeseries = NamedTuple{keys(full_model.tracers)}(
    FieldTimeSeries(filename, "$field") for field in keys(full_model.tracers)
)

timeseries_keys = keys(timeseries)

#Filter keys for P, Z, N, and D fields
P_keys = filter(k -> startswith(string(k), "P"), timeseries_keys)
Z_keys = filter(k -> startswith(string(k), "Z"), timeseries_keys)
N_key = :N
D_key = :D

#Combine all keys into a single list for iteration
all_keys = [P_keys..., Z_keys..., N_key, D_key]

#Create figure with appropriate size
fig = Figure(; size=(800, 1200), fontsize=16)

#Plot all fields
for (i, key) in enumerate(all_keys)
    x_nodes, y_nodes, z_nodes = nodes(timeseries[key])
    z_vals = collect(z_nodes)
    times = collect(timeseries[key].times / days)

    ax = Axis(fig[i, 1]; title="$(key) concentration (mmol N / m³)",
              xlabel="Time (days)", ylabel="z (m)", limits=((0, 365), (-200, 0)))
    hm = heatmap!(ax, times, z_vals,
                  Float32.(interior(timeseries[key], 1, 1, :, :)');
                  colormap=:viridis, rasterize=true)  # Rasterize for smaller output
    Colorbar(fig[i, 2], hm)
end

#Save figure
save("N2P2ZD_column.png", fig)

fig  # Display the figure

This page was generated using Literate.jl.