Coupled Sea Breeze
Overview
This sea breeze simulation consists of an atmosphere above ocean and land thermal slabs. The difference in heating between the land and ocean components drives circulation: cool ocean air flows towards the land at the surface while warm air over land rises and flows over the ocean.
In this tutorial we demonstrate the coupling of three component models (atmosphere, ocean, and land) to drive the sea breeze. The primary parts of the ClimaCoupler interface are used and discussed.
Load utilities for running coupled simulation
include("../CoupledSims/coupled_sim.jl")
Set random seed for reproducibility
Random.seed!(1234)
Model Initialization
Component Models
Component models are the building blocks of coupled models. They are often developed independently from one another and can be executed by themselves as "standalone" simulations. The coupler is used to combine these components into coupled simulations. Importantly, coupled simulations can re-use tendency methods developed for standalone simulations, maximizing code reuse and minimizing the necessary code that must be specialized for a coupled run–only special boundary conditions must be written. This is achieved by multiple dispatch, where methods that deal with boundaries dispatch off of a coupled boundary type. Here, the atmosphere has special boundary conditions for coupling while the ocean and land tendencies are unaltered. See the atmospheric model page for more details.
In a more mature CliMA ecosystem, the following include statements would be replaced by using
statements for the relevant component packages.
include("atmos_rhs.jl")
include("ocean_rhs.jl")
include("land_rhs.jl")
# model parameters
const atm_T_ini = FT(270.0)
const MSLP = FT(1e5)
const grav = FT(9.8)
const R_d = FT(287.058)
const γ = FT(1.4)
const C_p = FT(R_d * γ / (γ - 1))
const C_v = FT(R_d / (γ - 1))
const R_m = R_d
cpl_parameters = (
# atmos parameters
atm_μ = FT(0.0001), # diffusion coefficient
atm_T_top = FT(280.0), # fixed temperature at the top of the domain_atm
atm_T_ini = atm_T_ini, # initial condition of at temperature (isothermal) [K]
MSLP = MSLP, # mean sea level pressure
grav = grav, # gravitational constant
R_d = R_d, # R dry (gas constant / mol mass dry air)
γ = γ, # heat capacity ratio
C_p = C_p, # heat capacity at constant pressure
C_v = C_v, # heat capacity at constant volume
R_m = R_m, # moist R, assumed to be dry
# land slab parameters
lnd_h = FT(0.5), # depth of slab layer [m]
lnd_ρ = FT(1500), # density [kg m^-3]
lnd_c = FT(800), # specific heat [J K^-1 kg^-1]
lnd_T_ini = FT(260.0), # initial condition of at temperature (isothermal) [K]
# ocean slab parameters
ocn_h = FT(0.5), # depth of slab layer [m]
ocn_ρ = FT(1025), # density [kg m^-3]
ocn_c = FT(3850), # specific heat [J K^-1 kg^-1]
ocn_T_ini = FT(260.0), # initial condition of at temperature (isothermal) [K]
# coupling parameters
C_H = FT(0.0015),
)
# DSS callback
function make_dss_func()
function _dss!(x::CC.Fields.Field)
CC.Spaces.weighted_dss!(x)
end
function _dss!(::Any)
nothing
end
dss_func(Y, t, integrator) = foreach(_dss!, CC.Fields._values(Y))
return dss_func
end
dss_func = make_dss_func()
dss_callback = DiffEqCallbacks.FunctionCallingCallback(dss_func, func_start = true)
Initialization
The coupled simulation synchronizes the component models at a coupling time step, Δt_cpl
. Within that step, components may substep - each component specifies a number of substeps to take within Δt_cpl
: atm_nsteps, ocn_nsteps, lnd_nsteps
.
Component model states are initialized via the initialization methods each component would use in standalone mode. These states will be modified to reflect the full coupled system before executing the simulation.
@info "Init Models and Maps"
t_start, t_end = (0.0, 1e4)
Δt_coupled = 0.1
saveat = 10.0
atm_nsteps, ocn_nsteps, lnd_nsteps = (5, 1, 1)
# Initialize Models
atm_Y_default, atm_bc, atm_domain = atm_init(
xmin = -500,
xmax = 500,
zmin = 0,
zmax = 1000,
npoly = 4,
helem = 20,
velem = 20,
bc = (ρθ = (bottom = CoupledFlux(), top = ZeroFlux()),),
)
ocn_Y_default, ocn_domain = ocn_init(xmin = -500, xmax = 0, helem = 10, npoly = 0)
lnd_Y_default, lnd_domain = lnd_init(xmin = 0, xmax = 500, helem = 10, npoly = 0)
Remapping
Because models may live on different grids, remapping is necessary at the boundaries. Maps between coupled components must be constructed for each interacting pair. Remapping utilities are imported from ClimaCore.Operators
.
atm_boundary = CC.Spaces.level(atm_domain.hv_face_space, CC.Utilities.PlusHalf(0))
maps = (
atmos_to_ocean = CC.Operators.LinearRemap(ocn_domain, atm_boundary),
atmos_to_land = CC.Operators.LinearRemap(lnd_domain, atm_boundary),
ocean_to_atmos = CC.Operators.LinearRemap(atm_boundary, ocn_domain),
land_to_atmos = CC.Operators.LinearRemap(atm_boundary, lnd_domain),
)
# initialize coupling fields
atm_T_sfc =
CC.Operators.remap(maps.ocean_to_atmos, ocn_Y_default.T_sfc) .+
CC.Operators.remap(maps.land_to_atmos, lnd_Y_default.T_sfc) # masked arrays; regrid to atm grid
atm_F_sfc = CC.Fields.zeros(atm_boundary)
ocn_F_sfc = CC.Fields.zeros(ocn_domain)
lnd_F_sfc = CC.Fields.zeros(lnd_domain)
Simulations
Each component is wrapped as a Sim
, which contains both the model (tendency) and the time-stepping information (solver, step size, etc). Sims are the standard structures that the coupler works with, enabling dispatch of coupler methods. Here, we create three simulations: AtmosSim
, OceanSim
, and LandSim
.
atm_Y = CC.Fields.FieldVector(Yc = atm_Y_default.Yc, ρw = atm_Y_default.ρw, F_sfc = atm_F_sfc)
atm_p = (cpl_p = cpl_parameters, T_sfc = atm_T_sfc, bc = atm_bc)
atmos = AtmosSim(atm_Y, t_start, Δt_coupled / atm_nsteps, t_end, CTS.RK4(), atm_p, saveat, dss_callback)
ocn_Y = CC.Fields.FieldVector(T_sfc = ocn_Y_default.T_sfc)
ocn_p = (cpl_parameters, F_sfc = ocn_F_sfc)
ocean = OceanSim(ocn_Y, t_start, Δt_coupled / ocn_nsteps, t_end, CTS.RK4(), ocn_p, saveat)
lnd_Y = CC.Fields.FieldVector(T_sfc = lnd_Y_default.T_sfc)
lnd_p = (cpl_parameters, F_sfc = lnd_F_sfc)
land = LandSim(lnd_Y, t_start, Δt_coupled / lnd_nsteps, t_end, CTS.RK4(), lnd_p, saveat)
Additionally, we create a coupled simulation that contains the component simulations and the coupled time-stepping information.
struct AOLCoupledSim{A <: AtmosSim, O <: OceanSim, L <: LandSim, C <: CouplerState} <: AbstractCoupledSim
# Atmosphere Simulation
atmos::A
# Ocean Simulation
ocean::O
# Land Simulation
land::L
# Coupler storage
coupler::C
end
step!
is a key method within the Sims interface. It advances a simulation to the specified t_stop
, with that simulation advancing by its own internal step size to reach the specified time. Each simulation type should specify its own step method, allowing components to have different time integration backends. Here, all components are using SciMLBase integrators and can share the same step!
method.
function step!(sim::AbstractSim, t_stop)
Δt = t_stop - sim.integrator.t
SciMLBase.step!(sim.integrator)
end
The Coupler
The CouplerState
is a coupling struct used to store pointers or copies of the shared boundary information. All components are coupled by updating or accessing data in this CouplerState
; component models do not directly interface with one another, only through the coupler.
After creating the CouplerState
object, coupled fields can be registered index the coupler via the coupler_add_field!
method. This field is then accessible by coupler_get
methods and can be updated via the coupler_put!
methods.
Similarly, the coupler_add_map!
method registers remapping operators in the coupler. To provide automatic remapping, there is a strict name convention for remap operators: a map from SimA to SimB (where ClimaCoupler.name
returns :simA
and :simB
, respectively) must be named simA_to_simB
so that the correct operator can be used.
Here, the models are coupled through heat transfer at the surface. This heat flux is computed by a bulk formula:
\[F_{sfc} = c_p \rho_1 C_H |u_1| (\theta_{sfc} - \theta_{atm1})\]
where $\theta_{sfc}$ is the potential temperature at the land or ocean surface, $\theta_{atm1}$ is the potential temperature at the lowest atmospheric level, $c_p$ is the specific heat, $C_H = 0.0015$ is the bulk transfer coefficient for sensible heat, and $|u_1|$ is the near-surface atmospheric wind speed. We assume that the potential temperature is defined with respect to the surface pressure, so that $\theta_{sfc} = T_{sfc}$.
coupler = CouplerState(Δt_coupled)
coupler_add_field!(coupler, :T_sfc_ocean, ocean.integrator.u.T_sfc; write_sim = ocean)
coupler_add_field!(coupler, :T_sfc_land, land.integrator.u.T_sfc; write_sim = land)
coupler_add_field!(coupler, :F_sfc, atmos.integrator.u.F_sfc; write_sim = atmos)
for (name, map) in pairs(maps)
coupler_add_map!(coupler, name, map)
end
sim = AOLCoupledSim(atmos, ocean, land, coupler)
Coupled Time Integration
Finally, the execution sequence of the component models must be specified. This is currently done explicitly with a combination of step!
, coupler_pull!
, and coupler_push!
methods. The coupler_pull!
and coupler_push!
methods receive and send coupled field info from the coupler, respectively. They must be written for each component simulation, and are simply collections of coupler_get
and coupler_put!
methods for each component.
Here, the atmosphere steps forward first and then sends updated fields to the coupler. The ocean and land (which are not coupled to each other) then retreive the updated coupled information, advance and send their own updates to the coupler.
Because the models exchange fluxes only at the coupled timestep, the surface flux is accumulated over the coupled time-step coupling time step, Δt_cpl
\[F_{integ} = \int_{\Delta t_{coupler}} F_{sfc} dt\]
where $F_{integ}$ has units of $J m^{-2}$.
function cpl_run(simulation::AOLCoupledSim)
@info "Run model"
(; atmos, ocean, land, coupler) = simulation
Δt_coupled = coupler.Δt_coupled
# coupler stepping
for t in ((t_start + Δt_coupled):Δt_coupled:t_end)
# Atmos
coupler_pull!(atmos, coupler)
step!(atmos, t)
coupler_push!(coupler, atmos)
# Ocean
coupler_pull!(ocean, coupler)
step!(ocean, t)
coupler_push!(coupler, ocean)
# Land
coupler_pull!(land, coupler)
step!(land, t)
coupler_push!(coupler, land)
end
@info "Simulation Complete"
end
# Run simulation
cpl_run(sim)
References
# Post-processing
JLD2.save(joinpath(path, "lastsim.jld2"), "coupledsim", sim) #hide
Plot atmospheric potential temperature [K] throughout the simulation
Plot atmospheric vertical velocity [m/s] throughout the simulation
Plot atmospheric longitudinal velocity [m/s] throughout the simulation
This page was generated using Literate.jl.