Open In Colab   Open in Kaggle

Tutorial 5: Vertical Structure of the Atmosphere#

Week 1, Day 5, Climate Modeling

Content creators: Jenna Pearson

Content reviewers: Yunlong Xu, Will Gregory, Peter Ohue, Derick Temfack, Zahra Khodakaramimaghsoud, Peizhen Yang, Younkap Nina Duplex, Ohad Zivan, Chi Zhang

Content editors: Brodie Pearson, Abigail Bodner, Ohad Zivan, Chi Zhang

Production editors: Wesley Banfield, Jenna Pearson, Chi Zhang, Ohad Zivan

Our 2023 Sponsors: NASA TOPS and Google DeepMind

Tutorial Objectives#

In this tutorial students will run a one-dimensional radiative equilibrium model that predicts the global mean atmospheric temperature as a function of height. Much of the code shown here was taken from The Climate Laboratory by Brian Rose. Students are encouraged to visit this website for more tutorials and background on these models.

By the end of this tutorial students will be able to:

  • Implement a 1-D model that predicts atmospheric temperature as a function of height using the python package climlab.

  • Understand how this model builds off of the energy balance models developed in the previous tutorials.

Setup#

# installations ( uncomment and run this cell ONLY when using google colab or kaggle )

# note the conda install takes quite a while, but conda is REQUIRED to properly download the
# dependencies (that are not just python packages)

# !pip install condacolab &> /dev/null
# import condacolab
# condacolab.install()

# !mamba install -c anaconda cftime xarray numpy &> /dev/null    # for decoding time variables when opening datasets
# !mamba install -c conda-forge metpy climlab &> /dev/null
# imports

# google colab users, if you get an xarray error, please run this cell again

import xarray as xr  # used to manipulate data and open datasets
import numpy as np  # used for algebra/arrays
import urllib.request  # used to download data from the internet
import climlab  # one of the models we are using
import matplotlib.pyplot as plt  # used for plotting
import metpy  # used to make Skew T Plots of temperature and pressure
from metpy.plots import SkewT  # plotting function used widely in climate science
import pooch
import os
import tempfile
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/climlab/radiation/cam3.py:46: UserWarning: Cannot import and initialize compiled Fortran extension, CAM3 module will not be functional.
  warnings.warn('Cannot import and initialize compiled Fortran extension, CAM3 module will not be functional.')
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/climlab/convection/emanuel_convection.py:14: UserWarning: Cannot import EmanuelConvection fortran extension, this module will not be functional.
  warnings.warn('Cannot import EmanuelConvection fortran extension, this module will not be functional.')

Figure settings#

Hide code cell source
# @title Figure settings
import ipywidgets as widgets  # interactive display

%config InlineBackend.figure_format = 'retina'
plt.style.use(
    "https://raw.githubusercontent.com/ClimateMatchAcademy/course-content/main/cma.mplstyle"
)

Helper functions#

Hide code cell source
# @title Helper functions


def pooch_load(filelocation=None, filename=None, processor=None):
    # this is different for each day
    shared_location = "/home/jovyan/shared/Data/tutorials/W1D4_ClimateModeling"
    user_temp_cache = tempfile.gettempdir()

    if os.path.exists(os.path.join(shared_location, filename)):
        file = os.path.join(shared_location, filename)
    else:
        file = pooch.retrieve(
            filelocation,
            known_hash=None,
            fname=os.path.join(user_temp_cache, filename),
            processor=processor,
        )

    return file

Video 1: Vertical Structure of the Atmosphere#

Section 1: Setting up the Radiative Equilibrium Model Using Climlab#

The energy balance model we used earlier today was zero-dimensional, yielding only the global mean surface temperature. We might ask, is it possible to construct a similar, one-dimensional, model for an atmospheric column to estimate the global mean temperature profile (i.e., including the height/\(z\) dimension). Additionally, can we explicitly include the effects of different gases in this model, rather than just parametrizing their collective effects through a single parameter \(\tau\)? The answer is yes, we can!

This model is too complex to construct from scratch, as we did in the previous tutorials. Instead, we will use a model already available within the python package climlab.

The model we will first use is a radiative equilbrium model. Radiative equilibrium models consider different layers of the atmosphere. Each of these layers absorbs and emits radiation depending on its constituent gases, allowing the model to calculate the radiation budget for each layer as radiative energy is transferred between atmospheric layers, the Earth’s surface, and space. Radiative equilibrium is reached when each layer gains energy at the same rate as it loses energy. In this tutorial you will analyze the temperature profile of this new model once it has reached equilibrium.

To set up this model, we will need information about some of the mean properties of the atmosphere. We are going to download water vapor data from the Community Earth System Model, a global climate model that we will go into detail on in the next tutorial, to use a variable called specific humidity. Specific humidity is the mass of water vapor per mass of a unit block of air. This is useful because water vapor is an important greenhouse gas.

filename_sq = "cpl_1850_f19-Q-gw-only.cam.h0.nc"
url_sq = "https://osf.io/c6q4j/download/"

ds = xr.open_dataset(
    pooch_load(filelocation=url_sq, filename=filename_sq)
)  # ds = dataset
ds
Downloading data from 'https://osf.io/c6q4j/download/' to file '/tmp/cpl_1850_f19-Q-gw-only.cam.h0.nc'.
SHA256 hash of downloaded file: 006bce4cbf3616123ec4bd67403f73e68fa78e8e3c9e2684cb6cae6124b7ff0f
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
<xarray.Dataset> Size: 345MB
Dimensions:  (time: 240, lev: 26, lat: 96, lon: 144)
Coordinates:
  * lev      (lev) float64 208B 3.545 7.389 13.97 23.94 ... 929.6 970.6 992.6
  * time     (time) object 2kB 0001-02-01 00:00:00 ... 0021-01-01 00:00:00
  * lat      (lat) float64 768B -90.0 -88.11 -86.21 -84.32 ... 86.21 88.11 90.0
  * lon      (lon) float64 1kB 0.0 2.5 5.0 7.5 10.0 ... 350.0 352.5 355.0 357.5
Data variables:
    Q        (time, lev, lat, lon) float32 345MB ...
    gw       (lat) float64 768B ...
Attributes: (12/13)
    Conventions:               CF-1.0
    source:                    CAM
    case:                      cpl_1850_f19
    title:                     UNSET
    logname:                   br546577
    host:                      snow-30.rit.alba
    ...                        ...
    revision_Id:               $Id$
    initial_file:              b40.1850.track1.2deg.003.cam.i.0501-01-01-0000...
    topography_file:           /data/rose_scr/cesm_inputdata/atm/cam/topo/USG...
    history:                   Tue Feb 26 17:17:15 2019: ncrcat atm/hist/cpl_...
    NCO:                       4.6.8
    nco_openmp_thread_number:  1
# the specific humidity is stored in a variable called Q
ds.Q
<xarray.DataArray 'Q' (time: 240, lev: 26, lat: 96, lon: 144)> Size: 345MB
[86261760 values with dtype=float32]
Coordinates:
  * lev      (lev) float64 208B 3.545 7.389 13.97 23.94 ... 929.6 970.6 992.6
  * time     (time) object 2kB 0001-02-01 00:00:00 ... 0021-01-01 00:00:00
  * lat      (lat) float64 768B -90.0 -88.11 -86.21 -84.32 ... 86.21 88.11 90.0
  * lon      (lon) float64 1kB 0.0 2.5 5.0 7.5 10.0 ... 350.0 352.5 355.0 357.5
Attributes:
    mdims:         1
    units:         kg/kg
    long_name:     Specific humidity
    cell_methods:  time: mean
ds.time
<xarray.DataArray 'time' (time: 240)> Size: 2kB
array([cftime.DatetimeNoLeap(1, 2, 1, 0, 0, 0, 0, has_year_zero=True),
       cftime.DatetimeNoLeap(1, 3, 1, 0, 0, 0, 0, has_year_zero=True),
       cftime.DatetimeNoLeap(1, 4, 1, 0, 0, 0, 0, has_year_zero=True), ...,
       cftime.DatetimeNoLeap(20, 11, 1, 0, 0, 0, 0, has_year_zero=True),
       cftime.DatetimeNoLeap(20, 12, 1, 0, 0, 0, 0, has_year_zero=True),
       cftime.DatetimeNoLeap(21, 1, 1, 0, 0, 0, 0, has_year_zero=True)],
      dtype=object)
Coordinates:
  * time     (time) object 2kB 0001-02-01 00:00:00 ... 0021-01-01 00:00:00
Attributes:
    long_name:  time
    bounds:     time_bnds

however, we want an annual average profile:

# take global, annual average using a weighting (ds.gw) that is calculated based on the model grid - and is similar, but not identical, to a cosine(latitude) weighting

weight_factor = ds.gw / ds.gw.mean(dim="lat")
Qglobal = (ds.Q * weight_factor).mean(dim=("lat", "lon", "time"))
# print specific humidity profile
Qglobal
<xarray.DataArray (lev: 26)> Size: 208B
array([2.16104904e-06, 2.15879387e-06, 2.15121262e-06, 2.13630949e-06,
       2.12163684e-06, 2.11168002e-06, 2.09396914e-06, 2.10589390e-06,
       2.42166155e-06, 3.12595653e-06, 5.01369691e-06, 9.60746488e-06,
       2.08907654e-05, 4.78823747e-05, 1.05492451e-04, 2.11889055e-04,
       3.94176751e-04, 7.10734458e-04, 1.34192099e-03, 2.05153261e-03,
       3.16844784e-03, 4.96883408e-03, 6.62218037e-03, 8.38350326e-03,
       9.38620899e-03, 9.65030544e-03])
Coordinates:
  * lev      (lev) float64 208B 3.545 7.389 13.97 23.94 ... 929.6 970.6 992.6

Now that we have a global mean water vapor profile, we can define a model that has the same vertical levels as this water vapor data.

# use 'lev=Qglobal.lev' to create an identical vertical grid to water vapor data
mystate = climlab.column_state(lev=Qglobal.lev, water_depth=2.5)
mystate
AttrDict({'Ts': Field([288.]), 'Tatm': Field([200.  , 203.12, 206.24, 209.36, 212.48, 215.6 , 218.72, 221.84,
       224.96, 228.08, 231.2 , 234.32, 237.44, 240.56, 243.68, 246.8 ,
       249.92, 253.04, 256.16, 259.28, 262.4 , 265.52, 268.64, 271.76,
       274.88, 278.  ])})

To model the absorption and emission of different gases within each atmospheric layer, we use the Rapid Radiative Transfer Model, which is contained within the RRTMG module. We must first initialize our model using the water vapor .

radmodel = climlab.radiation.RRTMG(
    name="Radiation (all gases)",  # give our model a name!
    state=mystate,  # give our model an initial condition!
    specific_humidity=Qglobal.values,  # tell the model how much water vapor there is
    albedo=0.25,  # this the SURFACE shortwave albedo
    # set the timestep to one day (measured in seconds)
    timestep=climlab.constants.seconds_per_day,
)
radmodel
Downloading data from 'http://www.atmos.albany.edu/facstaff/brose/resources/climlab_data/ozone/apeozone_cam3_5_54.nc' to file '/home/runner/.cache/pooch/0e7ac27a9b97c3a843dcfdd7cd89a9b7-apeozone_cam3_5_54.nc'.
<climlab.radiation.rrtm.rrtmg.RRTMG at 0x7fbe70895610>

Let’s explore this initial state. Here Ts is the initial global mean surface temperature, and Tatm is the initial global mean air temperature profile.

radmodel.state
AttrDict({'Ts': Field([288.]), 'Tatm': Field([200.  , 203.12, 206.24, 209.36, 212.48, 215.6 , 218.72, 221.84,
       224.96, 228.08, 231.2 , 234.32, 237.44, 240.56, 243.68, 246.8 ,
       249.92, 253.04, 256.16, 259.28, 262.4 , 265.52, 268.64, 271.76,
       274.88, 278.  ])})

One of the perks of using this model is it’s ability to incorporate the radiative effects of individual greenhouse gases in different parts of the radiation spectrum, rather than using a bulk reduction in transmission of outgoing longwave radiation (as in our previous models).

Let’s display ‘absorber_vmr’, which contains the volume mixing ratio’s of each gas used in the radiative transfer model (these are pre-defined; and do not include the water vapor we used as a model input above). The volume mixing ratio describes the fraction of molecules in the air that are a given gas. For example, \(21\%\) of air is oxygen and so it’s volumn mixing ratio is 0.21.

radmodel.absorber_vmr
{'CO2': 0.000348,
 'CH4': 1.65e-06,
 'N2O': 3.06e-07,
 'O2': 0.21,
 'CFC11': 0.0,
 'CFC12': 0.0,
 'CFC22': 0.0,
 'CCL4': 0.0,
 'O3': array([7.52507018e-06, 8.51545793e-06, 7.87041289e-06, 5.59601020e-06,
        3.46128454e-06, 2.02820936e-06, 1.13263102e-06, 7.30182697e-07,
        5.27326553e-07, 3.83940962e-07, 2.82227214e-07, 2.12188506e-07,
        1.62569291e-07, 1.17991442e-07, 8.23582543e-08, 6.25738219e-08,
        5.34457156e-08, 4.72688637e-08, 4.23614749e-08, 3.91392482e-08,
        3.56025264e-08, 3.12026770e-08, 2.73165152e-08, 2.47190016e-08,
        2.30518624e-08, 2.22005071e-08])}

To look at carbon dioxide (CO2) in a more familiar unit, parts per million (by volume), we can convert and print the new value.

radmodel.absorber_vmr["CO2"] * 1e6
348.0

We can also look at all the available diagnostics of our model:

diag_ds = climlab.to_xarray(radmodel.diagnostics)
diag_ds
<xarray.Dataset> Size: 4kB
Dimensions:           (depth: 1, lev: 26, lev_bounds: 27, depth_bounds: 2)
Coordinates:
  * depth             (depth) float64 8B 1.25
  * lev               (lev) float64 208B 3.545 7.389 13.97 ... 929.6 970.6 992.6
  * depth_bounds      (depth_bounds) float64 16B 0.0 2.5
  * lev_bounds        (lev_bounds) float64 216B 0.0 5.467 10.68 ... 981.6 1e+03
Data variables: (12/26)
    OLR               (depth) float64 8B 0.0
    OLRclr            (depth) float64 8B 0.0
    OLRcld            (depth) float64 8B 0.0
    TdotLW            (lev) float64 208B 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
    TdotLW_clr        (lev) float64 208B 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0
    LW_sfc            (depth) float64 8B 0.0
    ...                ...
    SW_flux_up        (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
    SW_flux_down      (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
    SW_flux_net       (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
    SW_flux_up_clr    (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
    SW_flux_down_clr  (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
    SW_flux_net_clr   (lev_bounds) float64 216B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0

For example to look at OLR,

radmodel.OLR
Field([0.])

Note. the OLR is currently 0 as we have not ran the model forward in time, so it has not calculated any radiation components.

Questions 1: Climate Connection#

  1. Why do you think all gases, except ozone and water vapor, are represented by single values in the model?

Click for solution

Coding Exercises 1#

  1. On the same graph, plot the annual mean specific humidity profile and ozone profiles.

fig, ax = plt.subplots()
# multiply Qglobal by 1000 to put in units of grams water vapor per kg of air
_ = ...
# multiply by 1E6 to get units of ppmv = parts per million by volume
_ = ...

# pressure decreases logarithmically with height in the atmosphere
# invert the axis so the largest value of pressure is lowest
ax.invert_yaxis()
# set y axis to a log scale
_ = ...

ax.set_ylabel("Pressure (hPa)")
ax.set_xlabel("Specific humidity (g/kg)")

# turn on the grid lines
_ = ...

# turn on legend
_ = ...
../../../_images/4bd76487af415c10104d5d680c25848961ae3587083a1350319d27e5017e3ac8.png

Click for solution

Example output:

Solution hint

Section 2: Getting Data to Compare to the Model#

Before we run our model forward, we will download a reanalysis product from NCEP to get a sense of what the real global mean atmospheric temperature profile looks like. We will compare this profile to our model runs later.

filename_ncep_air = "air.mon.1981-2010.ltm.nc"
url_ncep_air = "https://osf.io/w6cd5/download/"
ncep_air = xr.open_dataset(
    pooch_load(filelocation=url_ncep_air, filename=filename_ncep_air)
)  # ds = dataset

# this is the long term monthly means (note only 12 time steps)
ncep_air.air
Downloading data from 'https://osf.io/w6cd5/download/' to file '/tmp/air.mon.1981-2010.ltm.nc'.
SHA256 hash of downloaded file: e653932e5153d1b06f04eff66b788014b5d9f4aa1f8fe0f954077e5c79f2c183
Use this value as the 'known_hash' argument of 'pooch.retrieve' to ensure that the file hasn't changed if it is downloaded again in the future.
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/xarray/coding/times.py:995: SerializationWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using cftime.datetime objects instead, reason: dates out of range
  dtype = _decode_cf_datetime_dtype(data, units, calendar, self.use_cftime)
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/xarray/coding/times.py:170: SerializationWarning: Ambiguous reference date string: 1-1-1 00:00:0.0. The first value is assumed to be the year hence will be padded with zeros to remove the ambiguity (the padded reference date string is: 0001-1-1 00:00:0.0). To remove this message, remove the ambiguity by padding your reference date strings with zeros.
  warnings.warn(warning_msg, SerializationWarning)
/opt/hostedtoolcache/Python/3.9.18/x64/lib/python3.9/site-packages/xarray/core/indexing.py:557: SerializationWarning: Unable to decode time axis into full numpy.datetime64 objects, continuing using cftime.datetime objects instead, reason: dates out of range
  array = array.get_duck_array()
<xarray.DataArray 'air' (time: 12, level: 17, lat: 73, lon: 144)> Size: 9MB
[2144448 values with dtype=float32]
Coordinates:
  * level    (level) float32 68B 1e+03 925.0 850.0 700.0 ... 50.0 30.0 20.0 10.0
  * lat      (lat) float32 292B 90.0 87.5 85.0 82.5 ... -82.5 -85.0 -87.5 -90.0
  * lon      (lon) float32 576B 0.0 2.5 5.0 7.5 10.0 ... 350.0 352.5 355.0 357.5
  * time     (time) object 96B 0001-01-01 00:00:00 ... 0001-12-01 00:00:00
Attributes:
    long_name:     Monthly Long Term Mean of Air temperature
    units:         degC
    precision:     2
    var_desc:      Air Temperature
    level_desc:    Pressure Levels
    statistic:     Long Term Mean
    parent_stat:   Mean
    valid_range:   [-200.  300.]
    actual_range:  [-89.722336  41.616005]
    dataset:       NCEP Reanalysis Derived Products
# need to take the average over space and time
# the grid cells are not the same size moving towards the poles, so we weight by the cosine of latitude to compensate for this
coslat = np.cos(np.deg2rad(ncep_air.lat))
weight = coslat / coslat.mean(dim="lat")

Tglobal = (ncep_air.air * weight).mean(dim=("lat", "lon", "time"))
Tglobal
<xarray.DataArray (level: 17)> Size: 68B
array([ 15.179084  ,  11.207003  ,   7.8383274 ,   0.21994135,
        -6.4483433 , -14.888848  , -25.570469  , -39.36969   ,
       -46.797905  , -53.652245  , -60.56356   , -67.006065  ,
       -65.53293   , -61.48664   , -55.853584  , -51.593952  ,
       -43.21999   ], dtype=float32)
Coordinates:
  * level    (level) float32 68B 1e+03 925.0 850.0 700.0 ... 50.0 30.0 20.0 10.0

Below we will define two helper funcitons to visualize the profiles output from our model with a SkewT plot. This is common way to plot atmospheric temperature in climate science, and the metpy package has a built in function to make this easier.

# to setup the skewT and plot observations
def make_skewT():
    fig = plt.figure(figsize=(9, 9))
    skew = SkewT(fig, rotation=30)
    skew.plot(
        Tglobal.level,
        Tglobal,
        color="black",
        linestyle="-",
        linewidth=2,
        label="Observations",
    )
    skew.ax.set_ylim(1050, 10)
    skew.ax.set_xlim(-90, 45)
    # Add the relevant special lines
    # skew.plot_dry_adiabats(linewidth=1.5, label = 'dry adiabats')
    # skew.plot_moist_adiabats(linewidth=1.5, label = 'moist adiabats')
    # skew.plot_mixing_lines()
    skew.ax.legend()
    skew.ax.set_xlabel("Temperature (degC)", fontsize=14)
    skew.ax.set_ylabel("Pressure (hPa)", fontsize=14)
    return skew
# to add a model derived profile to the skewT figure
def add_profile(skew, model, linestyle="-", color=None):
    line = skew.plot(
        model.lev,
        model.Tatm - climlab.constants.tempCtoK,
        label=model.name,
        linewidth=2,
    )[0]
    skew.plot(
        1000,
        model.Ts - climlab.constants.tempCtoK,
        "o",
        markersize=8,
        color=line.get_color(),
    )
    skew.ax.legend()
skew = make_skewT()
../../../_images/b04d64b608abd2042fa91568cb089d9b518914ec9b2c87300eec02874d06caaf.png

SkewT (also known as SkewT-logP) plots are generally used for much more complex reasons than we will use here. However, one of the benefits of this plot that we will utilize is the fact that pressure decreases approximately logarithmically with height. Thus, with a logP axis, we are showing information that is roughly linear in height, making the plots more intuitive.

Section 3: Running the Radiative Equilibrium Model Forward in Time#

We can run this model over many time steps, just like the simple greenhouse model, but now we can examine the behavior of the temperature profile rather than just the surface temperature.

There is no need to write out a function to step our model forward - climlab already has this feature. We will use this function to run our model to equilibrium (i.e., until OLR is balanced by ASR).

# take a single step forward to the diagnostics are updated and there is some energy imbalance
radmodel.step_forward()

# run the model to equilibrium (the difference between ASR and OLR is a very small number)
while np.abs(radmodel.ASR - radmodel.OLR) > 0.001:
    radmodel.step_forward()
#  check the energy budget to make sure we are really at equilibrium
radmodel.ASR - radmodel.OLR
Field([0.0009879])

Now let’s can compare this to observations.

skew = make_skewT()
add_profile(skew, radmodel)
skew.ax.set_title("Pure Radiative Equilibrium", fontsize=18)
Text(0.5, 1.0, 'Pure Radiative Equilibrium')
../../../_images/05ec58a41d4a62035870f2406f1dcf2d778f6210040026998403d0fb24185576.png

Questions 3: Climate Connection#

  1. The profile from our model does not match observations well. Can you think of one component we might be missing?

  2. What effect do you think the individual gases play in determining this profile and why?

Click for solution

Coding Exercises 3#

  1. Create a second model called ‘Radiation (no H20)’ that lacks water vapor. Then re-create the plot above, but add on this extra profile without water vapor.

# make an exact clone of our existing model
radmodel_noH2O = climlab.process_like(radmodel)
# change the name of our new model
radmodel_noH2O.name = ...

# set the water vapor profile to all zeros
radmodel_noH2O.specific_humidity *= 0.0

# run the model to equilibrium
radmodel_noH2O.step_forward()
while np.abs(radmodel_noH2O.ASR - radmodel_noH2O.OLR) > 0.01:
    radmodel_noH2O.step_forward()

# create skewT plot
skew = make_skewT()

# add profiles for both models to plot
for model in [...]:
    ...
../../../_images/b04d64b608abd2042fa91568cb089d9b518914ec9b2c87300eec02874d06caaf.png

Click for solution

Example output:

Solution hint

Summary#

In this tutorial, you’ve learned how to use the python package climlab to construct a one-dimensional radiative equilibrium model, and run it forward in time to predict the global mean atmospheric temperature profile. You’ve also visualized these results through SkewT plots.

Resources#

Data from this tutorial can be accessed for specific humidity here and reanalysis temperature here.