Dry atmosphere GCM with Held-Suarez forcing

The Held-Suarez setup (Held and Suarez, 1994) is a textbook example for a simplified atmospheric global circulation model configuration which has been used as a benchmark experiment for development of the dynamical cores (i.e., GCMs without continents, moisture or parametrization schemes of the physics) for atmospheric models. It is forced by a thermal relaxation to a reference state and damped by linear (Rayleigh) friction. This example demonstrates how

  • to set up a ClimateMachine-Atmos GCM configuration;
  • to select and save GCM diagnostics output.

To begin, we load ClimateMachine and a few miscellaneous useful Julia packages.

using Distributions
using Random
using StaticArrays
using UnPack

ClimateMachine specific modules needed to make this example work (e.g., we will need spectral filters, etc.).

using ClimateMachine
using ClimateMachine.Atmos
using ClimateMachine.Orientations
using ClimateMachine.ConfigTypes
using ClimateMachine.Diagnostics
using ClimateMachine.GenericCallbacks
using ClimateMachine.Mesh.Grids
using ClimateMachine.Mesh.Filters
using Thermodynamics.TemperatureProfiles
using ClimateMachine.SystemSolvers
using ClimateMachine.ODESolvers
using Thermodynamics
using ClimateMachine.TurbulenceClosures
using ClimateMachine.VariableTemplates
using ClimateMachine.BalanceLaws

import ClimateMachine.BalanceLaws: source, prognostic_vars

ClimateMachine parameters are needed to have access to Earth's physical parameters.

using CLIMAParameters
using CLIMAParameters.Planet: MSLP, R_d, day, grav, cp_d, cv_d, planet_radius

We need to load the physical parameters for Earth to have an Earth-like simulation :).

struct EarthParameterSet <: AbstractEarthParameterSet end
const param_set = EarthParameterSet();

"""
    HeldSuarezForcingTutorial <: TendencyDef{Source}

Defines a forcing that parametrises radiative and frictional effects using
Newtonian relaxation and Rayleigh friction, following Held and Suarez (1994)
"""
struct HeldSuarezForcingTutorial <: TendencyDef{Source} end

prognostic_vars(::HeldSuarezForcingTutorial) = (Momentum(), Energy())

function held_suarez_forcing_coefficients(bl, args)
    @unpack state, aux = args
    @unpack ts = args.precomputed
    FT = eltype(state)

    # Parameters
    T_ref = FT(255)
    param_set = parameter_set(bl)

    _R_d = FT(R_d(param_set))
    _day = FT(day(param_set))
    _grav = FT(grav(param_set))
    _cp_d = FT(cp_d(param_set))
    _p0 = FT(MSLP(param_set))

    # Held-Suarez parameters
    k_a = FT(1 / (40 * _day))
    k_f = FT(1 / _day)
    k_s = FT(1 / (4 * _day))
    ΔT_y = FT(60)
    Δθ_z = FT(10)
    T_equator = FT(315)
    T_min = FT(200)
    σ_b = FT(7 / 10)

    # Held-Suarez forcing
    φ = latitude(bl, aux)
    p = air_pressure(ts)

    ##TODO: replace _p0 with dynamic surface pressure in Δσ calculations to account
    ##for topography, but leave unchanged for calculations of σ involved in T_equil
    σ = p / _p0
    exner_p = σ^(_R_d / _cp_d)
    Δσ = (σ - σ_b) / (1 - σ_b)
    height_factor = max(0, Δσ)
    T_equil = (T_equator - ΔT_y * sin(φ)^2 - Δθ_z * log(σ) * cos(φ)^2) * exner_p
    T_equil = max(T_min, T_equil)
    k_T = k_a + (k_s - k_a) * height_factor * cos(φ)^4
    k_v = k_f * height_factor
    return (k_v = k_v, k_T = k_T, T_equil = T_equil)
end

function source(::Energy, s::HeldSuarezForcingTutorial, atmos, args)
    @unpack state = args
    FT = eltype(state)
    @unpack ts = args.precomputed
    nt = held_suarez_forcing_coefficients(atmos, args)
    param_set = parameter_set(atmos)
    _cv_d = FT(cv_d(param_set))
    @unpack k_T, T_equil = nt
    T = air_temperature(ts)
    return -k_T * state.ρ * _cv_d * (T - T_equil)
end

function source(::Momentum, s::HeldSuarezForcingTutorial, atmos, args)
    @unpack state, aux = args
    nt = held_suarez_forcing_coefficients(atmos, args)
    return -nt.k_v * projection_tangential(atmos, aux, state.ρu)
end
source (generic function with 30 methods)

Set initial condition

When using ClimateMachine, we need to define a function that sets the initial state of our model run. In our case, we use the reference state of the simulation (defined below) and add a little bit of noise. Note that the initial states includes a zero initial velocity field.

function init_heldsuarez!(problem, balance_law, state, aux, localgeo, time)
    FT = eltype(state)

    # Set initial state to reference state with random perturbation
    rnd = FT(1 + rand(Uniform(-1e-3, 1e-3)))
    state.ρ = aux.ref_state.ρ
    state.ρu = SVector{3, FT}(0, 0, 0)
    state.energy.ρe = rnd * aux.ref_state.ρe
end;

Initialize ClimateMachine

Before we do anything further, we need to initialize ClimateMachine. Among other things, this will initialize the MPI us.

ClimateMachine.init();
[1636503132.726607] [hpc-92-37:14388:0]       ib_verbs.h:84   UCX  ERROR ibv_exp_query_device(mlx5_0) returned 38: No space left on device

Setting the floating-type precision

ClimateMachine allows us to run a model with different floating-type precisions, with lower precision we get our results faster, and with higher precision, we may get more accurate results, depending on the questions we are after.

const FT = Float64;

Setup model configuration

Now that we have defined our forcing and initialization functions, and have initialized ClimateMachine, we can set up the model.

Set up a reference state

We start by setting up a reference state. This is simply a vector field that we subtract from the solutions to the governing equations to both improve numerical stability of the implicit time stepper and enable faster model spin-up. The reference state assumes hydrostatic balance and ideal gas law, with a pressure $p_r(z)$ and density $\rho_r(z)$ that only depend on altitude $z$ and are in hydrostatic balance with each other.

In this example, the reference temperature field smoothly transitions from a linearly decaying profile near the surface to a constant temperature profile at the top of the domain.

temp_profile_ref = DecayingTemperatureProfile{FT}(param_set)
ref_state = HydrostaticState(temp_profile_ref);

Set up a Rayleigh sponge layer

To avoid wave reflection at the top of the domain, the model applies a sponge layer that linearly damps the momentum equations.

domain_height = FT(30e3)               # height of the computational domain (m)
z_sponge = FT(12e3)                    # height at which sponge begins (m)
α_relax = FT(1 / 60 / 15)              # sponge relaxation rate (1/s)
exponent = FT(2)                       # sponge exponent for squared-sinusoid profile
u_relax = SVector(FT(0), FT(0), FT(0)) # relaxation velocity (m/s)
sponge = RayleighSponge{FT}(domain_height, z_sponge, α_relax, u_relax, exponent);

Set up turbulence models

In order to produce a stable simulation, we need to dissipate energy and enstrophy at the smallest scales of the developed flow field. To achieve this we set up diffusive forcing functions.

c_smag = FT(0.21);   # Smagorinsky constant
τ_hyper = FT(4 * 3600); # hyperdiffusion time scale
turbulence_model = SmagorinskyLilly(c_smag);
hyperdiffusion_model = DryBiharmonic(FT(4 * 3600));

Instantiate the model

The Held Suarez setup was designed to produce an equilibrated state that is comparable to the zonal mean of the Earth’s atmosphere.

physics = AtmosPhysics{FT}(
    param_set;
    ref_state = ref_state,
    turbulence = turbulence_model,
    hyperdiffusion = hyperdiffusion_model,
    moisture = DryModel(),
);

model = AtmosModel{FT}(
    AtmosGCMConfigType,
    physics;
    init_state_prognostic = init_heldsuarez!,
    source = (Gravity(), Coriolis(), HeldSuarezForcingTutorial(), sponge),
);

This concludes the setup of the physical model!

Set up the driver

We just need to set up a few parameters that define the resolution of the discontinuous Galerkin method and for how long we want to run our model setup.

poly_order = 5;                        ## discontinuous Galerkin polynomial order
n_horz = 2;                            ## horizontal element number
n_vert = 2;                            ## vertical element number
resolution = (n_horz, n_vert)
n_days = 0.1;                          ## experiment day number
timestart = FT(0);                     ## start time (s)
timeend = FT(n_days * day(param_set)); ## end time (s);

The next lines set up the spatial grid.

driver_config = ClimateMachine.AtmosGCMConfiguration(
    "HeldSuarez",
    poly_order,
    resolution,
    domain_height,
    param_set,
    init_heldsuarez!;
    model = model,
);
ClimateMachine.array_type() = Array
┌ Info: Model composition
│     physics = ClimateMachine.Atmos.AtmosPhysics{Float64,Main.##346.EarthParameterSet,ClimateMachine.Atmos.HydrostaticState{Thermodynamics.TemperatureProfiles.DecayingTemperatureProfile{Float64},Float64},ClimateMachine.Atmos.TotalEnergyModel,ClimateMachine.Atmos.DryModel,ClimateMachine.Atmos.Compressible,ClimateMachine.TurbulenceClosures.SmagorinskyLilly{Float64},ClimateMachine.TurbulenceConvection.NoTurbConv,ClimateMachine.TurbulenceClosures.DryBiharmonic{Float64},ClimateMachine.TurbulenceClosures.NoViscousSponge,ClimateMachine.Atmos.NoPrecipitation,ClimateMachine.Atmos.NoRadiation,ClimateMachine.Atmos.NoTracers,ClimateMachine.Atmos.NoLSForcing}(Main.##346.EarthParameterSet(), ClimateMachine.Atmos.HydrostaticState{Thermodynamics.TemperatureProfiles.DecayingTemperatureProfile{Float64},Float64}(Thermodynamics.TemperatureProfiles.DecayingTemperatureProfile{Float64}(290.0, 220.0, 8484.271021693852), 0.0, true), ClimateMachine.Atmos.TotalEnergyModel(), ClimateMachine.Atmos.DryModel(), ClimateMachine.Atmos.Compressible(), ClimateMachine.TurbulenceClosures.SmagorinskyLilly{Float64}(0.21), ClimateMachine.TurbulenceConvection.NoTurbConv(), ClimateMachine.TurbulenceClosures.DryBiharmonic{Float64}(14400.0), ClimateMachine.TurbulenceClosures.NoViscousSponge(), ClimateMachine.Atmos.NoPrecipitation(), ClimateMachine.Atmos.NoRadiation(), ClimateMachine.Atmos.NoTracers(), ClimateMachine.Atmos.NoLSForcing())
│     problem = ClimateMachine.Atmos.AtmosProblem{Tuple{ClimateMachine.Atmos.AtmosBC{ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip},ClimateMachine.Atmos.Insulating,ClimateMachine.Atmos.Impermeable,ClimateMachine.Atmos.OutflowPrecipitation,ClimateMachine.Atmos.ImpermeableTracer,ClimateMachine.TurbulenceConvection.NoTurbConvBC},ClimateMachine.Atmos.AtmosBC{ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip},ClimateMachine.Atmos.Insulating,ClimateMachine.Atmos.Impermeable,ClimateMachine.Atmos.OutflowPrecipitation,ClimateMachine.Atmos.ImpermeableTracer,ClimateMachine.TurbulenceConvection.NoTurbConvBC}},typeof(Main.##346.init_heldsuarez!),typeof(ClimateMachine.Atmos.atmos_problem_init_state_auxiliary)}((ClimateMachine.Atmos.AtmosBC{ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip},ClimateMachine.Atmos.Insulating,ClimateMachine.Atmos.Impermeable,ClimateMachine.Atmos.OutflowPrecipitation,ClimateMachine.Atmos.ImpermeableTracer,ClimateMachine.TurbulenceConvection.NoTurbConvBC}(ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip}(ClimateMachine.Atmos.FreeSlip()), ClimateMachine.Atmos.Insulating(), ClimateMachine.Atmos.Impermeable(), ClimateMachine.Atmos.OutflowPrecipitation(), ClimateMachine.Atmos.ImpermeableTracer(), ClimateMachine.TurbulenceConvection.NoTurbConvBC()), ClimateMachine.Atmos.AtmosBC{ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip},ClimateMachine.Atmos.Insulating,ClimateMachine.Atmos.Impermeable,ClimateMachine.Atmos.OutflowPrecipitation,ClimateMachine.Atmos.ImpermeableTracer,ClimateMachine.TurbulenceConvection.NoTurbConvBC}(ClimateMachine.Atmos.Impenetrable{ClimateMachine.Atmos.FreeSlip}(ClimateMachine.Atmos.FreeSlip()), ClimateMachine.Atmos.Insulating(), ClimateMachine.Atmos.Impermeable(), ClimateMachine.Atmos.OutflowPrecipitation(), ClimateMachine.Atmos.ImpermeableTracer(), ClimateMachine.TurbulenceConvection.NoTurbConvBC())), Main.##346.init_heldsuarez!, ClimateMachine.Atmos.atmos_problem_init_state_auxiliary)
│     orientation = ClimateMachine.Orientations.SphericalOrientation()
│     source = DispatchedTuples.DispatchedTuple{Tuple{Tuple{ClimateMachine.Atmos.Momentum,ClimateMachine.Atmos.Gravity},Tuple{ClimateMachine.Atmos.Momentum,ClimateMachine.Atmos.Coriolis},Tuple{ClimateMachine.Atmos.Momentum,Main.##346.HeldSuarezForcingTutorial},Tuple{ClimateMachine.Atmos.Energy,Main.##346.HeldSuarezForcingTutorial},Tuple{ClimateMachine.Atmos.Momentum,ClimateMachine.Atmos.RayleighSponge{Float64}}},DispatchedTuples.NoDefaults} with 5 entries:
│   ClimateMachine.Atmos.Momentum() => ClimateMachine.Atmos.Gravity()
│   ClimateMachine.Atmos.Momentum() => ClimateMachine.Atmos.Coriolis()
│   ClimateMachine.Atmos.Momentum() => Main.##346.HeldSuarezForcingTutorial()
│   ClimateMachine.Atmos.Energy() => Main.##346.HeldSuarezForcingTutorial()
│   ClimateMachine.Atmos.Momentum() => ClimateMachine.Atmos.RayleighSponge{Float64}(30000.0, 12000.0, 0.0011111111111111111, [0.0, 0.0, 0.0], 2.0)
│   default => ()
│ 
│     data_config = nothing
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/driver_configs.jl:188

PDE: ∂_t Y_i + (∇•F_1(Y))_i + (∇•F_2(Y,G)))_i = (S(Y,G))_i
┌──────────┬────────────────────────────┬──────────────────────────────────────────────────────────────────────────────┬────────────────────────────────────────────────────────────────┐
│ Equation │           Flux{FirstOrder} │                                                            Flux{SecondOrder} │                                                         Source │
│    (Y_i) │                      (F_1) │                                                                        (F_2) │                                                            (S) │
├──────────┼────────────────────────────┼──────────────────────────────────────────────────────────────────────────────┼────────────────────────────────────────────────────────────────┤
│     Mass │                   (Advect) │                                                                           () │                                                             () │
│ Momentum │ (Advect, PressureGradient) │                                        (ViscousStress, HyperdiffViscousFlux) │ (Gravity, Coriolis, HeldSuarezForcingTutorial, RayleighSponge) │
│   Energy │         (Advect, Pressure) │ (ViscousFlux, DiffEnthalpyFlux, HyperdiffEnthalpyFlux, HyperdiffViscousFlux) │                                    (HeldSuarezForcingTutorial) │
└──────────┴────────────────────────────┴──────────────────────────────────────────────────────────────────────────────┴────────────────────────────────────────────────────────────────┘

┌ Info: Establishing Atmos GCM configuration for HeldSuarez
│     precision               = Float64
│     horiz polynomial order  = 5
│     vert polynomial order   = 5
│     horiz cutoff order      = nothing
│     vert cutoff order       = nothing
│     # horiz elem            = 2
│     # vert elems            = 2
│     domain height           = 3.00e+04 m
│     MPI ranks               = 1
│     min(Δ_horz)             = 416156.89 m
│     min(Δ_vert)             = 1762.09 m
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/driver_configs.jl:418

The next lines set up the time stepper. Since the resolution in the vertical is much finer than in the horizontal, the 'stiff' parts of the PDE will be in the vertical. Setting splitting_type = HEVISplitting() will treat vertical acoustic waves implicitly, while all other dynamics are treated explicitly.

ode_solver_type = ClimateMachine.IMEXSolverType(
    splitting_type = HEVISplitting(),
    implicit_model = AtmosAcousticGravityLinearModel,
    implicit_solver = ManyColumnLU,
    solver_method = ARK2GiraldoKellyConstantinescu,
);

solver_config = ClimateMachine.SolverConfiguration(
    timestart,
    timeend,
    driver_config,
    Courant_number = FT(0.1),
    ode_solver_type = ode_solver_type,
    init_on_cpu = true,
    CFL_direction = HorizontalDirection(),
    diffdir = HorizontalDirection(),
);
┌ Info: Initializing HeldSuarez
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/solver_configs.jl:185

Set up spectral exponential filter

After every completed time step we apply a spectral filter to remove remaining small-scale noise introduced by the numerical procedures. This assures that our run remains stable.

filterorder = 10;
filter = ExponentialFilter(solver_config.dg.grid, 0, filterorder);
cbfilter = GenericCallbacks.EveryXSimulationSteps(1) do
    Filters.apply!(
        solver_config.Q,
        AtmosFilterPerturbations(model),
        solver_config.dg.grid,
        filter,
        state_auxiliary = solver_config.dg.state_auxiliary,
    )
    nothing
end;

Setup diagnostic output

Choose frequency and resolution of output, and a diagnostics group (dgngrp) which defines output variables. This needs to be defined in Diagnostics.

interval = "1000steps";
_planet_radius = FT(planet_radius(param_set));
info = driver_config.config_info;
boundaries = [
    FT(-90.0) FT(-180.0) _planet_radius
    FT(90.0) FT(180.0) FT(_planet_radius + info.domain_height)
];
resolution = (FT(10), FT(10), FT(1000)); # in (deg, deg, m)
interpol = ClimateMachine.InterpolationConfiguration(
    driver_config,
    boundaries,
    resolution,
);

dgngrps = [
    setup_dump_state_diagnostics(
        AtmosGCMConfigType(),
        interval,
        driver_config.name,
        interpol = interpol,
    ),
    setup_dump_aux_diagnostics(
        AtmosGCMConfigType(),
        interval,
        driver_config.name,
        interpol = interpol,
    ),
];
dgn_config = ClimateMachine.DiagnosticsConfiguration(dgngrps);

Run the model

Finally, we can run the model using the physical setup and solvers from above. We use the spectral filter in our callbacks after every time step, and collect the diagnostics output.

result = ClimateMachine.invoke!(
    solver_config;
    diagnostics_config = dgn_config,
    user_callbacks = (cbfilter,),
    check_euclidean_distance = true,
);
┌ Info: Starting HeldSuarez
│     dt              = 1.21690e+02
│     timeend         =  8640.00
│     number of steps = 71
│     norm(Q)         = 7.7109879974110906e+13
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Driver.jl:802
┌ Info: Update
│     simtime =   121.69 /  8640.00
│     wallclock = 00:01:09
│     efficiency (simtime / wallclock) =   1.7622
│     wallclock end (estimated) = 01:21:43
│     norm(Q) = 7.7109979932426641e+13
└ @ ClimateMachine.Callbacks /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Callbacks/Callbacks.jl:75
┌ Info: Finished
│     norm(Q)            = 7.7119802040592359e+13
│     norm(Q) / norm(Q₀) = 1.0001286743862756e+00
│     norm(Q) - norm(Q₀) = 9.9220664814531250e+09
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Driver.jl:853
┌ Info: Euclidean distance
│     norm(Q - Qe)            = 1.3065283973625212e+11
│     norm(Q - Qe) / norm(Qe) = 1.6943650961947175e-03
└ @ ClimateMachine /central/scratch/climaci/climatemachine-docs/1426/climatemachine-docs/src/Driver/Driver.jl:869

References


This page was generated using Literate.jl.